How to scroll to the bottom of a ScrollViewer auto

2019-01-10 22:56发布

I've got a TextBlock whose content is data bound to a string property of the ViewModel. This TextBlock has a ScrollViewer wrapped around it.

What I want to do is every time the logs change, the ScrollViewer will scroll to the bottom. Ideally I want something like this:

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

I don't want to use Code Behind! The solution I'm looking for should be using only binding and/or Xaml.

标签: c# wpf xaml
6条回答
我想做一个坏孩纸
2楼-- · 2019-01-10 23:03

You can either create an attached property or a behavior to achieve what you want without using code behind. Either way you will still need to write some code.

Here is an example of using attached property.

Attached Property

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 Binding

<ScrollViewer local:Helper.AutoScroll="{Binding IsLogsChangedPropertyInViewModel}" .../>

You will need to create a boolean property IsLogsChangedPropertyInViewModel and set it to true when the string property is changed.

Hope this helps! :)

查看更多
We Are One
3楼-- · 2019-01-10 23:04

I was using @Roy T. 's answer, however I wanted the added stipulation that if you scrolled back in time, but then added text, the scroll view should auto scroll to bottom.

I used this:

private static void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
    var scrollViewer = sender as ScrollViewer;

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

in place of the SizeChanged event.

查看更多
forever°为你锁心
4楼-- · 2019-01-10 23:10

From Geoff's Blog on ScrollViewer AutoScroll Behavior.

Add this class:

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);
            }
        }
    }
}

This code depends Blend Behaviors, which require a reference to System.Windows.Interactivity. See help on adding System.Windows.Interactivity.

If you install the MVVM Light NuGet package, you can add a reference here:

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

Ensure that you have this property in your header, which points to System.Windows.Interactivity.dll:

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

Add a Blend Behavior into the ScrollViewer:

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

Example:

<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> 

We have to add a definition for the namespace, or else it won't know where to find the C# class we have just added. Add this property into the <Window> tag. If you are using ReSharper, it will automatically suggest this for you.

xmlns:implementation="clr-namespace:MyAttachedBehaviors"

Now, if all goes well, the text in the box will always scroll down to the bottom.

The example XAML given will print the contents of the bound property LogText to the screen, which is perfect for logging.

查看更多
聊天终结者
5楼-- · 2019-01-10 23:16

Here is a slight variation.

This will scroll to the bottom both when the scroll viewer height (viewport) and the height of it's scroll presenter's content (extent) change.

It's based on Roy T's answer but I wasn't able to comment so I have posted as an answer.

    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);
        }
    }
查看更多
放荡不羁爱自由
6楼-- · 2019-01-10 23:24

It is easy, examples:

yourContronInside.ScrollOwner.ScrollToEnd (); 
yourContronInside.ScrollOwner.ScrollToBottom ();
查看更多
劫难
7楼-- · 2019-01-10 23:29

Answer updated 2017-12-13, now uses the ScrollChanged event and checks if the size of extent changes. More reliable and doesn't interfere with manual scrolling

I know this question is old, but I've got an improved implementation:

  • No external dependencies
  • You only need to set the property once

The code is heavily influenced by Both Justin XL's and Contango's solutions

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);
    }
}

Usage:

<ScrollViewer n:AutoScrollBehavior.AutoScroll="True" > // Where n is the XML namespace 
查看更多
登录 后发表回答