如何使用Xaml和绑定自动滚动到ScrollViewer的底部?

45

我有一个TextBlock,其内容通过数据绑定到ViewModel的字符串属性。这个TextBlock被一个ScrollViewer包裹。

我想要做的是,每次日志更改时,ScrollViewer都会滚动到底部。理想情况下,我想要这样做:

    <ScrollViewer ScrollViewer.HorizontalScrollBarVisibility="Auto"
                  ScrollPosition="{Binding Path=ScrollPosition}">
        <TextBlock Text="{Binding Path=Logs}"/>
    </ScrollViewer>

不想使用代码后端!我要找的解决方案应该只使用绑定和/或Xaml。


3
没有代码后台的特定原因吗? - Haris Hasan
5
您说得对,但我认为MVVM只是建议您的业务逻辑(视图模型)不应与您的UI(视图)混合。如果我们在代码后台中放置一些代码来将滚动查看器移动到底部,它并不违反MVVM,因为我们只是在处理UI。 - Haris Hasan
@Haris:我理解并同意你的观点,但我不确定原帖作者是否也这样认为。 - Kent Boogaart
3
@Kent Boogaart 我想要一个MVVM的答案,有三个原因:1- 我正在使用MVVM模式,所以我要找到的第一种答案是MVVM。2- 在询问MVVM之前,我在谷歌或StackOverflow上发现了代码后台的答案。因此,如果知道几乎只有代码后台的解决方案,我就不会寻求答案。3- 只有当我了解所有不同可能性时,我才能做出正确的选择,是吗?别担心,我不是狂热者 ;) - JiBéDoublevé
2
MVVM不否认代码后台。我认为@Harris和@Kent的评论的重点是,在XAML中编写大型结构或帮助类只是为了避免在代码后台中编写单行视图特定代码没有任何显着的理由。 - icebat
6个回答

56

你可以创建一个附加属性或者行为来实现你想要的功能,而不需要使用代码后台。无论哪种方式,你仍然需要编写一些代码。

这里是使用附加属性的示例。

附加属性

public static class Helper
{
    public static bool GetAutoScroll(DependencyObject obj)
    {
        return (bool)obj.GetValue(AutoScrollProperty);
    }

    public static void SetAutoScroll(DependencyObject obj, bool value)
    {
        obj.SetValue(AutoScrollProperty, value);
    }

    public static readonly DependencyProperty AutoScrollProperty =
        DependencyProperty.RegisterAttached("AutoScroll", typeof(bool), typeof(Helper), new PropertyMetadata(false, AutoScrollPropertyChanged));

    private static void AutoScrollPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var scrollViewer = d as ScrollViewer;

        if (scrollViewer != null && (bool)e.NewValue)
        {
            scrollViewer.ScrollToBottom();
        }
    }
}

Xaml绑定

<ScrollViewer local:Helper.AutoScroll="{Binding IsLogsChangedPropertyInViewModel}" .../>
你需要创建一个布尔属性 IsLogsChangedPropertyInViewModel 并当字符串属性被更改时将其设置为 true。
希望这可以帮到你! :)

不幸的是,我无法在我的机器上使用VS 2012 + MVVM Light使其工作。 我猜测它可能依赖于一个未记录的引用。 我已经发布了Geoff博客中对我有用的答案。 - Contango
1
Julien XL的答案对我来说完美无缺。我使用的是VS 2012 + Mahapps。我不使用MVVM Light。 - fredericrous
1
@Zougi 这对我也有效,VS2015 + MVVM Light。非常好的解决方案! - LueTm
1
非常好用,但我必须添加一个自定义的OnPropertyChanged事件来在字符串绑定更改时将布尔值更改为true。 - JohnChris
我在itemscontrol中使用了scrollviewer,但这个解决方案并没有起作用。Roy T.的解决方案对我很有帮助。 - Skelvir
此解决方案会自动停止滚动到底部。 - Jana Andropov

39

回答已于2017年12月13日更新,现在使用ScrollChanged事件并检查范围大小是否发生更改。更可靠,不会干扰手动滚动

我知道这个问题很老,但是我有一个改进的实现:

  • 没有外部依赖
  • 你只需要设置一次属性

该代码受到Justin XL和Contango解决方案的影响

public static class AutoScrollBehavior
{
    public static readonly DependencyProperty AutoScrollProperty =
        DependencyProperty.RegisterAttached("AutoScroll", typeof(bool), typeof(AutoScrollBehavior), new PropertyMetadata(false, AutoScrollPropertyChanged));


    public static void AutoScrollPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
        var scrollViewer = obj as ScrollViewer;
        if(scrollViewer != null && (bool)args.NewValue)
        {
            scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
            scrollViewer.ScrollToEnd();
        }
        else
        {
            scrollViewer.ScrollChanged-= ScrollViewer_ScrollChanged;
        }
    }

    private static void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        // Only scroll to bottom when the extent changed. Otherwise you can't scroll up
        if (e.ExtentHeightChange != 0)
        {
            var scrollViewer = sender as ScrollViewer;
            scrollViewer?.ScrollToBottom();
        }
    }

    public static bool GetAutoScroll(DependencyObject obj)
    {
        return (bool)obj.GetValue(AutoScrollProperty);
    }

    public static void SetAutoScroll(DependencyObject obj, bool value)
    {
        obj.SetValue(AutoScrollProperty, value);
    }
}

使用方法:

<ScrollViewer n:AutoScrollBehavior.AutoScroll="True" > // Where n is the XML namespace 

3
AutoScrollPropertyChanged方法中,当scrollViewer为空时,可以到达else,而不仅仅是在scrollViewer不为nullNewValuefalse时。 - cgijbels
1
我首先实现了被接受的答案。然后我发现这个答案在实现上看起来要简单得多,因为我不需要有一个会改变的布尔值。非常好用,从现在开始我会使用它。 - JohnChris
1
已测试并确认在.NET Core 3.1上可以正常工作。 - SNag
不错!使用这个实现,你可以在XAML中直接将AutoScroll功能绑定到CheckBox的IsChecked值。 - Mischo5500
抱歉这个问题有点旧了,但是我在将它添加到ListView时遇到了空引用的问题。有人知道如何在ListView上使它工作吗? :) - Frederik
显示剩余2条评论

12

来自Geoff博客关于ScrollViewer自动滚动行为的文章.

添加这个类:

namespace MyAttachedBehaviors
{
    /// <summary>
    ///     Intent: Behavior which means a scrollviewer will always scroll down to the bottom.
    /// </summary>
    public class AutoScrollBehavior : Behavior<ScrollViewer>
    {
        private double _height = 0.0d;
        private ScrollViewer _scrollViewer = null;

        protected override void OnAttached()
        {
            base.OnAttached();

            this._scrollViewer = base.AssociatedObject;
            this._scrollViewer.LayoutUpdated += new EventHandler(_scrollViewer_LayoutUpdated);
        }

        private void _scrollViewer_LayoutUpdated(object sender, EventArgs e)
        {
            if (Math.Abs(this._scrollViewer.ExtentHeight - _height) > 1)
            {
                this._scrollViewer.ScrollToVerticalOffset(this._scrollViewer.ExtentHeight);
                this._height = this._scrollViewer.ExtentHeight;
            }
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();

            if (this._scrollViewer != null)
            {
                this._scrollViewer.LayoutUpdated -= new EventHandler(_scrollViewer_LayoutUpdated);
            }
        }
    }
}

这段代码依赖于 Blend Behaviors,需要引用 System.Windows.Interactivity。请参考添加 System.Windows.Interactivity 的帮助

如果你安装了 MVVM Light NuGet 包,可以在此处添加引用:

packages\MvvmLightLibs.4.2.30.0\lib\net45\System.Windows.Interactivity.dll

请确保您的标头中包含此属性,该属性指向 System.Windows.Interactivity.dll

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

ScrollViewer中添加混合行为:

<i:Interaction.Behaviors>
    <implementation:AutoScrollBehavior />
</i:Interaction.Behaviors>

例子:

<GroupBox Grid.Row="2" Header ="Log">
    <ScrollViewer>
        <i:Interaction.Behaviors>
            <implementation:AutoScrollBehavior />
        </i:Interaction.Behaviors>
        <TextBlock Margin="10" Text="{Binding Path=LogText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextWrapping="Wrap"/>
    </ScrollViewer>
</GroupBox> 

我们需要为命名空间添加一个定义,否则它就不知道在哪里找到我们刚刚添加的C#类。将这个属性添加到<Window>标记中。如果你使用的是ReSharper,它会自动为你建议。

xmlns:implementation="clr-namespace:MyAttachedBehaviors"
现在,如果一切顺利,框中的文本将始终滚动到底部。
所提供的XAML示例将打印绑定属性LogText的内容到屏幕上,这非常适合记录日志。

如果你的示例对其他人有帮助,你可以从安装了 Blend 的 Visual Studio 2012 或 2013 版本的文件夹中获取 .NET 4.5 的 System.Windows.Interactivity.dll。因为 Blend 是随这些版本一起提供的。 - Alex Marshall
@Alex Marshall。您说得完全正确,感谢您添加这个注释。当我使用MVVM Light时,直到我使用了与MVVM Light提供的确切的“System.Windows.Interactivity.dll”(如答案中所述),我才能使其正常工作。如果使用其他MVVM框架,甚至是代码后台,则可能会正常工作。换句话说,如果您的MVVM框架已经包含了它,那么您不能将多个版本的此“.dll”添加到您的项目中。 - Contango
你能添加XAML的部分,设置资源吗? - Mark W
@Mark W 我经常使用这个,它非常好用。刚刚更新了答案,现在试试看。 - Contango
1
这个说法在2021年已经不完全正确了。Behavior的实现是正确的,但要使用当前正确的命名空间和dll,请参见https://dev59.com/rWIi5IYBdhLWcg3w_QmH#61547718。 - Peter Centellini

4

这很容易,以下是示例:

yourContronInside.ScrollOwner.ScrollToEnd (); 
yourContronInside.ScrollOwner.ScrollToBottom ();

1
考虑扩展您的答案,解释给提问者“为什么”这样做可以实现所需的结果,可能会链接到文档。目前,这只是略微有用的。 - Joshua Dwire
1
这对我来说似乎不是一个MVVM解决方案。诀窍在于不使用代码后台实现它。 - ecth
1
为什么这不就是答案呢,它真的很简单,而且你们甚至还有人在这里惊慌失措... @ecth 在你的 XAML 中为 ScrollChanged 添加一个事件监听器,然后在该事件中使用 scrollViewName.ScrollToBottom(); 即可完美解决。 - PandaDev
这就是我所说的非MVVM。它是纯WPF,没错。但如果你想要分离视图和视图模型,你不会在XAML中创建事件并在代码后台处理它们。你的代码后台只是一个带有InitializeComponent()的构造函数。你将东西绑定到视图模型中的命令。 - ecth

0

这里有一个小变化。

当滚动查看器的高度(视口)和其滚动呈现器的内容高度(范围)发生变化时,它将滚动到底部。

它基于Roy T的答案,但我无法评论,所以我已经发布了一个答案。

    public static class AutoScrollHelper
    {
        public static readonly DependencyProperty AutoScrollProperty =
            DependencyProperty.RegisterAttached("AutoScroll", typeof(bool), typeof(AutoScrollHelper), new PropertyMetadata(false, AutoScrollPropertyChanged));


        public static void AutoScrollPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            var scrollViewer = obj as ScrollViewer;
            if (scrollViewer == null) return;

            if ((bool) args.NewValue)
            {
                scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
                scrollViewer.ScrollToEnd();
            }
            else
            {
                scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
            }
        }

        static void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
        {
            // Remove "|| e.ViewportHeightChange < 0 || e.ExtentHeightChange < 0" if you want it to only scroll to the bottom when it increases in size
            if (e.ViewportHeightChange > 0 || e.ExtentHeightChange > 0 || e.ViewportHeightChange < 0 || e.ExtentHeightChange < 0)
            {
                var scrollViewer = sender as ScrollViewer;
                scrollViewer?.ScrollToEnd();
            }
        }

        public static bool GetAutoScroll(DependencyObject obj)
        {
            return (bool) obj.GetValue(AutoScrollProperty);
        }

        public static void SetAutoScroll(DependencyObject obj, bool value)
        {
            obj.SetValue(AutoScrollProperty, value);
        }
    }

-1
我正在使用 @Roy T. 的答案,但我想要一个额外的约束条件,即如果您向后滚动时间,然后添加文本,则滚动视图应自动向底部滚动。
我使用了这个:
private static void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
    var scrollViewer = sender as ScrollViewer;

    if (e.ExtentHeightChange > 0)
    {
        scrollViewer.ScrollToEnd();
    }    
}

使用SizeChanged事件的位置。


我在XAML中将其绑定为一种行为。就像这样:<ScrollViewer Name="Scroller" Margin="0" behaviors:AutoScrollBehavior.AutoScroll="True"> - cabusto

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接