How to automatically scroll ScrollViewer - only if the user did not change scroll position
Asked Answered
S

10

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.

Sillabub answered 6/6, 2010 at 15:48 Comment(0)
E
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();
        }
    }
}
Endocrine answered 7/6, 2010 at 22:44 Comment(1)
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.Did
G
79

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);
        }
    }
Girosol answered 11/10, 2013 at 9:54 Comment(4)
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.Lateral
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?Crus
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 #25762295Did
You, my friend, are a hero!Gauss
C
37

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>
Camillacamille answered 12/2, 2014 at 20:7 Comment(7)
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.Markman
This is excellent. It's always good to have attached properties that work cleanly.Sitnik
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.Foliolate
Works perfectly for my needs. Thanks for this!Beccafico
Can be be used with a ListView?Is there any way to attach this to the ScrollViewer of the ListView?Merrillmerrily
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)Cressler
You need to add a reference to your extensions namespace. For example: xmlns:extensionProperties="clr-namespace:YOUREXTENSIONNAMESPACE"Ad
E
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();
        }
    }
}
Endocrine answered 7/6, 2010 at 22:44 Comment(1)
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.Did
P
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.

Paragraph answered 9/5, 2014 at 9:55 Comment(0)
S
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();
    }
Sinkage answered 4/7, 2016 at 7:20 Comment(0)
O
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

Off answered 5/1, 2015 at 16:47 Comment(1)
В английском языке на этом сайте / English only on this website. And you need to fix your code (indentation).Gunnar
D
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

Denotative answered 29/11, 2019 at 16:11 Comment(0)
T
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);
    } 
Tatyanatau answered 23/7, 2016 at 16:2 Comment(0)
B
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);
    }
}
Beccafico answered 13/3, 2017 at 18:5 Comment(0)
P
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.

Paoting answered 29/5, 2021 at 18:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.