42

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.

1

10 Answers 10

80

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);
        }
    }
Sign up to request clarification or add additional context in comments.

6 Comments

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.
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?
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 stackoverflow.com/questions/25761795/…
You, my friend, are a hero!
This code behaves exactly has I required it. Thank you very much!
|
38

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>

6 Comments

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.
This is excellent. It's always good to have attached properties that work cleanly.
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.
Can be be used with a ListView?Is there any way to attach this to the ScrollViewer of the ListView?
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)
|
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();
        }
    }
}

1 Comment

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

Comments

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

Comments

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

1 Comment

В английском языке на этом сайте / English only on this website. And you need to fix your code (indentation).
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

Comments

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

Comments

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

Comments

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.