41

I would like to create the following behaviour in a ScrollViewer that wraps ContentControl:
When the ContentControl height grows , the ScrollViewer should automatically scroll to the end. This is easy to achive by using ScrollViewer.ScrollToEnd().
However, if the user uses the scroll bar, the automatic scrolling shouldn't happen anymore. This is similar to what happens in VS output window for example.

The problem is to know when a scrolling has happened because of user scrolling and when it happened because the content size changed. I tried to play with the ScrollChangedEventArgsof ScrollChangedEvent, but couldn't get it to work.

Ideally, I do not want to handle all possible Mouse and keyboard events.

Elad
  • 19,079
  • 18
  • 62
  • 71

10 Answers10

78

You can use ScrollChangedEventArgs.ExtentHeightChange to know if a ScrollChanged is due to a change in the content or to a user action... When the content is unchanged, the ScrollBar position sets or unsets the auto-scroll mode. When the content has changed you can apply auto-scrolling.

Code behind:

    private Boolean AutoScroll = true;

    private void ScrollViewer_ScrollChanged(Object sender, ScrollChangedEventArgs e)
    {
        // User scroll event : set or unset auto-scroll mode
        if (e.ExtentHeightChange == 0)
        {   // Content unchanged : user scroll event
            if (ScrollViewer.VerticalOffset == ScrollViewer.ScrollableHeight)
            {   // Scroll bar is in bottom
                // Set auto-scroll mode
                AutoScroll = true;
            }
            else
            {   // Scroll bar isn't in bottom
                // Unset auto-scroll mode
                AutoScroll = false;
            }
        }

        // Content scroll event : auto-scroll eventually
        if (AutoScroll && e.ExtentHeightChange != 0)
        {   // Content changed and auto-scroll mode set
            // Autoscroll
            ScrollViewer.ScrollToVerticalOffset(ScrollViewer.ExtentHeight);
        }
    }
Alexandru Lache
  • 477
  • 2
  • 14
KBH
  • 781
  • 1
  • 5
  • 2
  • 3
    I wanted this behavior with a TextBox and it turned out to be easiest to use this code and embed the TextBox in a ScrollViewer rather than trying to use the TextBox's built-in scrolling. – David Minor Nov 07 '13 at 22:26
  • 1
    Thanks, I found this very useful getting my ScrollViewer to automatically scroll depending on the content of my TextBlock. I did make some minor modifications, like using `private bool AutoScroll = true` and putting it inside the method. `private Boolean AutoScroll = true` caused an "Invalid expression term 'private'" error. Question, is this "valid WPF style"? Or does not using binding break the "spirit" of WPF? – InvalidBrainException Jul 10 '14 at 17:22
  • I tried to make a simpler solution but ended up pretty much like this one. Still, I put the AutoScroll variable within the hander instead of outside, see http://stackoverflow.com/questions/25761795/scrollviewer-scrolltoend-only-works-while-debugging – Roland Sep 10 '14 at 13:02
  • You, my friend, are a hero! – LordWilmore Mar 11 '16 at 15:31
36

Here is an adaptation from several sources.

public class ScrollViewerExtensions
    {
        public static readonly DependencyProperty AlwaysScrollToEndProperty = DependencyProperty.RegisterAttached("AlwaysScrollToEnd", typeof(bool), typeof(ScrollViewerExtensions), new PropertyMetadata(false, AlwaysScrollToEndChanged));
        private static bool _autoScroll;

        private static void AlwaysScrollToEndChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            ScrollViewer scroll = sender as ScrollViewer;
            if (scroll != null)
            {
                bool alwaysScrollToEnd = (e.NewValue != null) && (bool)e.NewValue;
                if (alwaysScrollToEnd)
                {
                    scroll.ScrollToEnd();
                    scroll.ScrollChanged += ScrollChanged;
                }
                else { scroll.ScrollChanged -= ScrollChanged; }
            }
            else { throw new InvalidOperationException("The attached AlwaysScrollToEnd property can only be applied to ScrollViewer instances."); }
        }

        public static bool GetAlwaysScrollToEnd(ScrollViewer scroll)
        {
            if (scroll == null) { throw new ArgumentNullException("scroll"); }
            return (bool)scroll.GetValue(AlwaysScrollToEndProperty);
        }

        public static void SetAlwaysScrollToEnd(ScrollViewer scroll, bool alwaysScrollToEnd)
        {
            if (scroll == null) { throw new ArgumentNullException("scroll"); }
            scroll.SetValue(AlwaysScrollToEndProperty, alwaysScrollToEnd);
        }

        private static void ScrollChanged(object sender, ScrollChangedEventArgs e)
        {
            ScrollViewer scroll = sender as ScrollViewer;
            if (scroll == null) { throw new InvalidOperationException("The attached AlwaysScrollToEnd property can only be applied to ScrollViewer instances."); }

            // User scroll event : set or unset autoscroll mode
            if (e.ExtentHeightChange == 0) { _autoScroll = scroll.VerticalOffset == scroll.ScrollableHeight; }

            // Content scroll event : autoscroll eventually
            if (_autoScroll && e.ExtentHeightChange != 0) { scroll.ScrollToVerticalOffset(scroll.ExtentHeight); }
        }
    }

Use it in your XAML like so:

<ScrollViewer Height="230" HorizontalScrollBarVisibility="Auto" extensionProperties:ScrollViewerExtension.AlwaysScrollToEnd="True">
    <TextBlock x:Name="Trace"/>
</ScrollViewer>
Magikos
  • 381
  • 3
  • 3
  • 3
    Works perfectly. Scrolls automatically when scrolled to the bottom (either from initial setup or when restored by the user). Stays fixed when the user scroll position is anything but the bottom. Nice aggregate of information. +1 also for attached properties that can be added to my toolkit and reduce repetitive code-behind. – cod3monk3y Aug 17 '14 at 15:30
  • 3
    This is excellent. It's always good to have attached properties that work cleanly. – Lewis Heslop Aug 24 '15 at 08:09
  • 3
    There is a mistake in this answer. The `_autoScroll` field is static, which means if this class is used more than once, the state will cross usages. That state needs to be tied explicitly to the `ScrollViewer`. Also, ReSharper reports equality comparisons between floating-point types, which is a no-no. – NathanAldenSr Jul 16 '16 at 03:18
  • 1
    Works perfectly for my needs. Thanks for this! – rmirabelle Mar 13 '17 at 19:00
  • Can be be used with a ListView?Is there any way to attach this to the ScrollViewer of the ListView? – Snippy Valson Nov 21 '18 at 08:53
  • 2
    Getting a "error MC3000: ''extensionProperties' is an undeclared prefix" error, something must be missing in the above code (that is perhaps obvious to more experienced wpf'ers) – AnOttawan Jan 14 '19 at 11:33
  • You need to add a reference to your extensions namespace. For example: `xmlns:extensionProperties="clr-namespace:YOUREXTENSIONNAMESPACE"` – Aleksey Pavlov Aug 22 '23 at 13:36
12

This code will automatically scroll to end when the content grows if it was previously scrolled all the way down.

XAML:

<Window x:Class="AutoScrollTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Height="300" Width="300">
    <ScrollViewer Name="_scrollViewer">
        <Border BorderBrush="Red" BorderThickness="5" Name="_contentCtrl" Height="200" VerticalAlignment="Top">
        </Border>
    </ScrollViewer>
</Window>

Code behind:

using System;
using System.Windows;
using System.Windows.Threading;

namespace AutoScrollTest
{
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();

            DispatcherTimer timer = new DispatcherTimer();
            timer.Interval = new TimeSpan(0, 0, 2);
            timer.Tick += ((sender, e) =>
                {
                    _contentCtrl.Height += 10;

                    if (_scrollViewer.VerticalOffset == _scrollViewer.ScrollableHeight)
                    {
                        _scrollViewer.ScrollToEnd();
                    }
                });
            timer.Start();
        }
    }
}
Wallstreet Programmer
  • 9,567
  • 3
  • 37
  • 52
  • 51
    This code will check every 2 seconds, all day long, if there is something to scroll. This is both slower and less efficient than the event driven solutions below. – Roland Sep 10 '14 at 12:57
5

Here is a method I have used with good results. Based on two dependency properties. It avoids code behind and timers as shown in the other answer.

public static class ScrollViewerEx
{
    public static readonly DependencyProperty AutoScrollProperty =
        DependencyProperty.RegisterAttached("AutoScrollToEnd", 
            typeof(bool), typeof(ScrollViewerEx), 
            new PropertyMetadata(false, HookupAutoScrollToEnd));

    public static readonly DependencyProperty AutoScrollHandlerProperty =
        DependencyProperty.RegisterAttached("AutoScrollToEndHandler", 
            typeof(ScrollViewerAutoScrollToEndHandler), typeof(ScrollViewerEx));

    private static void HookupAutoScrollToEnd(DependencyObject d, 
            DependencyPropertyChangedEventArgs e)
    {
        var scrollViewer = d as ScrollViewer;
        if (scrollViewer == null) return;

        SetAutoScrollToEnd(scrollViewer, (bool)e.NewValue);
    }

    public static bool GetAutoScrollToEnd(ScrollViewer instance)
    {
        return (bool)instance.GetValue(AutoScrollProperty);
    }

    public static void SetAutoScrollToEnd(ScrollViewer instance, bool value)
    {
        var oldHandler = (ScrollViewerAutoScrollToEndHandler)instance.GetValue(AutoScrollHandlerProperty);
        if (oldHandler != null)
        {
            oldHandler.Dispose();
            instance.SetValue(AutoScrollHandlerProperty, null);
        }
        instance.SetValue(AutoScrollProperty, value);
        if (value)
            instance.SetValue(AutoScrollHandlerProperty, new ScrollViewerAutoScrollToEndHandler(instance));
    }

This uses a handler defined as.

public class ScrollViewerAutoScrollToEndHandler : DependencyObject, IDisposable
{
    readonly ScrollViewer m_scrollViewer;
    bool m_doScroll = false;

    public ScrollViewerAutoScrollToEndHandler(ScrollViewer scrollViewer)
    {
        if (scrollViewer == null) { throw new ArgumentNullException("scrollViewer"); }

        m_scrollViewer = scrollViewer;
        m_scrollViewer.ScrollToEnd();
        m_scrollViewer.ScrollChanged += ScrollChanged;
    }

    private void ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        // User scroll event : set or unset autoscroll mode
        if (e.ExtentHeightChange == 0) 
        { m_doScroll = m_scrollViewer.VerticalOffset == m_scrollViewer.ScrollableHeight; }

        // Content scroll event : autoscroll eventually
        if (m_doScroll && e.ExtentHeightChange != 0) 
        { m_scrollViewer.ScrollToVerticalOffset(m_scrollViewer.ExtentHeight); }
    }

    public void Dispose()
    {
        m_scrollViewer.ScrollChanged -= ScrollChanged;
    }

Then simply use this in XAML as:

<ScrollViewer VerticalScrollBarVisibility="Auto" 
              local:ScrollViewerEx.AutoScrollToEnd="True">
    <TextBlock x:Name="Test test test"/>
</ScrollViewer>

With local being a namespace import at the top of XAML file in question. This avoids the static bool seen in other answers.

nietras
  • 3,949
  • 1
  • 34
  • 38
3

What about using the "TextChanged" event of the TextBox and the ScrollToEnd() method?

 private void consolebox_TextChanged(object sender, TextChangedEventArgs e)
    {
        this.consolebox.ScrollToEnd();
    }
hoetz
  • 2,368
  • 4
  • 26
  • 58
2
bool autoScroll = false;

        if (e.ExtentHeightChange != 0)
        {   
            if (infoScroll.VerticalOffset == infoScroll.ScrollableHeight - e.ExtentHeightChange)
            { 
                autoScroll = true;
            }
            else
            {   
                autoScroll = false;
            }
        }
        if (autoScroll)
        {   
            infoScroll.ScrollToVerticalOffset(infoScroll.ExtentHeight);
        }

Вот так вроде-бы привельнее чем у Wallstreet Programmer

Mariusz Jamro
  • 30,615
  • 24
  • 120
  • 162
  • 2
    В английском языке на этом сайте / English only on this website. And you need to fix your code (indentation). – Mörre Jan 05 '15 at 17:04
2

On Windows builds 17763 and newer, one can set VerticalAnchorRatio="1" on the ScrollViewer and that's it.

HOWEVER: There's a bug that is still open: https://github.com/Microsoft/microsoft-ui-xaml/issues/562

michaelosthege
  • 621
  • 5
  • 15
0

In windows 10, .ScrollToVerticalOffset is obsolete. so I use ChangeView like this.

TextBlock messageBar;
ScrollViewer messageScroller; 

    private void displayMessage(string message)
    {

                messageBar.Text += message + "\n";

                double pos = this.messageScroller.ExtentHeight;
                messageScroller.ChangeView(null, pos, null);
    } 
Jaeyoon Jeong
  • 569
  • 4
  • 8
0

Previous answer rewritten to work with floating point comparison. Be aware that this solution, though simple, will PREVENT the user from scrolling as soon as the content is scrolled to the bottom.

private bool _should_auto_scroll = true;
private void ScrollViewer_OnScrollChanged(object sender, ScrollChangedEventArgs e) {
    if (Math.Abs(e.ExtentHeightChange) < float.MinValue) {
        _should_auto_scroll = Math.Abs(ScrollViewer.VerticalOffset - ScrollViewer.ScrollableHeight) < float.MinValue;
    }
    if (_should_auto_scroll && Math.Abs(e.ExtentHeightChange) > float.MinValue) {
        ScrollViewer.ScrollToVerticalOffset(ScrollViewer.ExtentHeight);
    }
}
rmirabelle
  • 6,268
  • 7
  • 45
  • 42
0

Based on the second answer, why can't it just be:

private void ScrollViewer_ScrollChanged(Object sender, ScrollChangedEventArgs e)
{
    if (e.ExtentHeightChange != 0)
    {
        ScrollViewer.ScrollToVerticalOffset(ScrollViewer.ExtentHeight);
    }
}

I have tested it on my application and it works.

teamclouday
  • 121
  • 1
  • 7