45

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.

Stewbob
  • 16,759
  • 9
  • 63
  • 107
JiBéDoublevé
  • 4,124
  • 4
  • 36
  • 57
  • 3
    any specific reason of no code behind? – Haris Hasan Dec 03 '11 at 19:13
  • 5
    You are right but in my opinion MVVM only suggests that your Business Logic (View Model) shouldn't be mixed with your UI(View). Scroll Viewer is UI/View if we put some code in code behind to move ScrollViewer to bottom it won't be against MVVM because we are just playing with UI – Haris Hasan Dec 03 '11 at 19:22
  • @Haris: I understand that and agree with you, but I'm not sure the OP does. – Kent Boogaart Dec 03 '11 at 20:04
  • 3
    @Kent Boogaart I want a MVVM answer for three reasons: 1- I'm using the MVVM pattern, therefore the first kind of answer I want to find is MVVM. 2- I found code behind answers in Google or StackOverflow before asking for MVVM. So I wouldn't have asked for an answer knowing that I'll almost have code behind solutions 3- It's is only when I'll know all the different kind of possibilities that I'll be able to make the right chose, won't I? Don't worry, I'm not a zealot ;) – JiBéDoublevé Dec 04 '11 at 10:33
  • 2
    MVVM doesn`t deny code behind. I think the point of comments of @Harris and @Kent was that there`s no any significant reason to write huge constructs in XAML or helper classes just to avoid a single line of view-specific code in code behind. – icebat Dec 05 '11 at 07:28

6 Answers6

56

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! :)

Justin XL
  • 38,763
  • 7
  • 88
  • 133
  • Unfortunately, I just couldn't get this to work on my machine, using VS 2012 + MVVM Light. My guess is that it probably depends on a reference which is undocumented. I have posted an answer from Geoff's Blog which worked for me. – Contango Sep 05 '14 at 15:13
  • 1
    Julien XL's answer works perfectly for me. I have VS 2012 + Mahapps. I don't use MVVM Light – fredericrous Feb 05 '15 at 18:24
  • 1
    @Zougi It works for me aswell, VS2015 + MVVM Light. Gread solution! – LueTm Jul 27 '16 at 10:12
  • 1
    Works great, I did have to add a custom OnPropertyChanged event to change the bool value to true when the string binding changes – JohnChris Nov 29 '17 at 11:11
  • I'm using a scrollviewer inside a itemscontrol, this solution did not work. Roy T.'s solution worked for me instead. – Skelvir Dec 10 '19 at 08:47
  • This solution spontaneously stops scrolling to bottom. – Jana Andropov Apr 29 '21 at 16:45
39

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 
Chris Johnsson
  • 468
  • 4
  • 13
Roy T.
  • 9,429
  • 2
  • 48
  • 70
  • I see a null-reference-exception waiting to happen if this property is put on anything that isn't a ScrollViewer. – Jeff B Jan 05 '17 at 17:38
  • Where exactly? I'm using the Elvis operator and 'as' casting in cases where I expect a ScrollViewer. – Roy T. Jan 09 '17 at 09:35
  • 3
    @RoyT inside the `AutoScrollPropertyChanged` method, the `else` can be reached when `scrollViewer` is null, not only when `scrollViewer` is not `null`and `NewValue` is `false` – cgijbels Apr 10 '17 at 15:22
  • 1
    I implemented the accepted answer first. Then I saw this answer looked a lot simpler in implementation as I didn't have to have a Boolean value that changes. Works great, using this from now on – JohnChris Nov 29 '17 at 11:15
  • 1
    Tested and can confirm that this works fine on .NET Core 3.1 – SNag Nov 12 '20 at 19:54
  • Nice! Using this implementation you can easily bind AutoScroll feature to CheckBox IsChecked value directly in XAML. – Mischo5500 Feb 11 '21 at 11:14
  • Sorry for the old bump, but I'm getting a Null reference when adding this to a ListView. Anyone know how to make it work on a listview? :) – Frederik May 10 '22 at 13:30
12

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.

Community
  • 1
  • 1
Contango
  • 76,540
  • 58
  • 260
  • 305
  • If it helps anybody using your sample, you can get the System.Windows.Interactivity.dll for .NET 4.5 from Visual Studio 2012 or 2013 install folders if you installed Blend with either of those versions since Blend comes with them. – Alex Marshall Sep 11 '14 at 00:26
  • @Alex Marshall. You are absolutely correct, thanks for adding this note. When I was using MVVM Light I couldn't get this to work until I used the exact `System.Windows.Interactivity.dll` that was supplied with MVVM Light (as noted in the answer). If other MVVM frameworks were used, or even code behind, then this would probably work just fine. In other words, you cant add multiple versions of this `.dll` to your project, if your MVVM framework already includes. – Contango Jun 12 '15 at 06:47
  • Could you add the part of the XAML where you set up the resource? – Mark W Jul 22 '15 at 14:28
  • @Mark W I use this all the time, it works very nicely. Just updated the answer, try it now. – Contango Jul 22 '15 at 15:18
  • 1
    This is not entirely true anymore as of 2021. The implementation of the Behavior is correct, but to use the currently correct namespace and dll, see https://stackoverflow.com/questions/20743961/the-name-interaction-does-not-exist-in-the-namespace-http-schemas-microsoft/61547718#61547718 – Peter Centellini Sep 03 '21 at 16:17
4

It is easy, examples:

yourContronInside.ScrollOwner.ScrollToEnd (); 
yourContronInside.ScrollOwner.ScrollToBottom ();
  • 1
    Consider expanding your answer to explain to the asker _why_ this achieves the desired result, possibly linking to documentation. As is, this is only marginally useful. – Joshua Dwire Oct 04 '13 at 16:20
  • 1
    That doesn't seem like an MVVM solution to me. The trick is doing it without using code behind.. – ecth Dec 19 '16 at 12:03
  • 1
    Why... Why isn't this just the answer its literally so simple, and you even have people in here freaking out.. @ecth in your XAML make an event listener for ScrollChanged and literally Put inside that event scrollViewName.ScrollToBottom(); Works Perfectly. – PandaDev Feb 08 '21 at 05:45
  • That is exactly what I mean by non-MVVM. It's plain WPF, true. But if you want to separate the view and view model, you don't create events in the xaml and handle them in the code-behind. Your code-behind is just a constructor with InitializeComponent(). You bind stuff to commands in your view model. – ecth Feb 08 '21 at 15:10
0

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);
        }
    }
Troto
  • 35
  • 7
-1

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.

Massimiliano Kraus
  • 3,638
  • 5
  • 27
  • 47
cabusto
  • 9
  • 1