28

I have a WPF application that contains a multiline TextBox that is being used to display debugging text output.

How can I set the TextBox so that as text is appended to the box, it will automatically scroll to the bottom of the textbox?

  • I'm using the MVVM pattern.
  • Ideally a pure XAML approach would be nice.
  • The TextBox itself is not necessarily in focus.
H.B.
  • 166,899
  • 29
  • 327
  • 400
Scott Ferguson
  • 7,690
  • 7
  • 41
  • 64
  • http://stackoverflow.com/questions/1192335/automatic-vertical-scroll-bar-in-wpf-textblock similar topic – Tamas Ionut Apr 10 '12 at 22:36
  • 1
    @TamasIonut Yes, I was aware of that question, sadly the solutions provided do not automatically scroll to the bottom of the textbox. – Scott Ferguson Apr 10 '12 at 22:39
  • WPF should provides an easier way to enable this, something like the answer below ScrollOnTextChanged="True" – Phuc Nov 06 '20 at 06:46

6 Answers6

49

The answer provided by @BojinLi works well. After reading through the answer linked to by @GazTheDestroyer however, I decided to implement my own version for the TextBox, because it looked cleaner.

To summarize, you can extend the behavior of the TextBox control by using an attached property. (Called ScrollOnTextChanged)

Using it is simple:

<TextBox src:TextBoxBehaviour.ScrollOnTextChanged="True" VerticalScrollBarVisibility="Auto" />

Here is the TextBoxBehaviour class:

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;

namespace MyNamespace
{
    public class TextBoxBehaviour
    {
        static readonly Dictionary<TextBox, Capture> _associations = new Dictionary<TextBox, Capture>();

        public static bool GetScrollOnTextChanged(DependencyObject dependencyObject)
        {
            return (bool)dependencyObject.GetValue(ScrollOnTextChangedProperty);
        }

        public static void SetScrollOnTextChanged(DependencyObject dependencyObject, bool value)
        {
            dependencyObject.SetValue(ScrollOnTextChangedProperty, value);
        }

        public static readonly DependencyProperty ScrollOnTextChangedProperty =
            DependencyProperty.RegisterAttached("ScrollOnTextChanged", typeof (bool), typeof (TextBoxBehaviour), new UIPropertyMetadata(false, OnScrollOnTextChanged));

        static void OnScrollOnTextChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
        {
            var textBox = dependencyObject as TextBox;
            if (textBox == null)
            {
                return;
            }
            bool oldValue = (bool) e.OldValue, newValue = (bool) e.NewValue;
            if (newValue == oldValue)
            {
                return;
            }
            if (newValue)
            {
                textBox.Loaded += TextBoxLoaded;
                textBox.Unloaded += TextBoxUnloaded;
            }
            else
            {
                textBox.Loaded -= TextBoxLoaded;
                textBox.Unloaded -= TextBoxUnloaded;
                if (_associations.ContainsKey(textBox))
                {
                    _associations[textBox].Dispose();
                }
            }
        }

        static void TextBoxUnloaded(object sender, RoutedEventArgs routedEventArgs)
        {
            var textBox = (TextBox) sender;
            _associations[textBox].Dispose();
            textBox.Unloaded -= TextBoxUnloaded;
        }

        static void TextBoxLoaded(object sender, RoutedEventArgs routedEventArgs)
        {
            var textBox = (TextBox) sender;
            textBox.Loaded -= TextBoxLoaded;
            _associations[textBox] = new Capture(textBox);
        }

        class Capture : IDisposable
        {
            private TextBox TextBox { get; set; }

            public Capture(TextBox textBox)
            {
                TextBox = textBox;
                TextBox.TextChanged += OnTextBoxOnTextChanged;
            }

            private void OnTextBoxOnTextChanged(object sender, TextChangedEventArgs args)
            {
                TextBox.ScrollToEnd();
            }

            public void Dispose()
            {
                TextBox.TextChanged -= OnTextBoxOnTextChanged;
            }
        }

    }
}
Scott Ferguson
  • 7,690
  • 7
  • 41
  • 64
  • This is by far the simplest and easiest answer I have ever found for this question. – Moshe Katz Jun 06 '12 at 22:05
  • Could you extend your answer to explain what the `_associations` are for and why you have chosen to internally create a disposable object? I *think* I have managed to implement a similar solution with much less code by just registering/unregistering an event handler with the `TextBox` whenever the attached property is set to `true` and `false`, respectively, and it seems to work flawlessly, but this is the first time I've ever written an attached property, so maybe I'm missing something important? – O. R. Mapper Sep 08 '12 at 16:12
  • @O.R.Mapper how many Autocomplete Textboxes are you using? If you only use one, then you could implement this dependency property without the _associations Dictionary. Feel free to post your solution as an answer, it might be helpful to someone else too. – Scott Ferguson Sep 10 '12 at 04:17
  • @ScottFerguson: Autocomplete? Always scroll to end? Anyway, I'm using several text boxes. See my answer for a solution with an attached property that seems to behave just as expected without storing any `_associations` directory and without the need of a `Capture` class. Please do point out any problems in my code; I'm no WPF expert by any means and it's well possible I'm missing some problem in my solution. – O. R. Mapper Sep 10 '12 at 06:51
  • @ScottFerguson I have this: but it doesn't work... why? – Piero Alberto Jan 14 '15 at 09:48
  • How can I bind to this property? `` is not working. – Stacked Nov 07 '15 at 22:17
  • Duplicate answer, same answer posted in following link. https://stackoverflow.com/questions/2006729/how-can-i-have-a-listbox-auto-scroll-when-a-new-item-is-added – Ashish-BeJovial Nov 22 '18 at 17:43
10

This solution is inspired by Scott Ferguson's solution with the attached property, but avoids storing an internal dictionary of associations and thereby has somewhat shorter code:

    using System;
    using System.Windows;
    using System.Windows.Controls;

    namespace AttachedPropertyTest
    {
        public static class TextBoxUtilities
        {
            public static readonly DependencyProperty AlwaysScrollToEndProperty = DependencyProperty.RegisterAttached("AlwaysScrollToEnd",
                                                                                                                      typeof(bool),
                                                                                                                      typeof(TextBoxUtilities),
                                                                                                                      new PropertyMetadata(false, AlwaysScrollToEndChanged));

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

            public static bool GetAlwaysScrollToEnd(TextBox textBox)
            {
                if (textBox == null) {
                    throw new ArgumentNullException("textBox");
                }

                return (bool)textBox.GetValue(AlwaysScrollToEndProperty);
            }

            public static void SetAlwaysScrollToEnd(TextBox textBox, bool alwaysScrollToEnd)
            {
                if (textBox == null) {
                    throw new ArgumentNullException("textBox");
                }

                textBox.SetValue(AlwaysScrollToEndProperty, alwaysScrollToEnd);
            }

            private static void TextChanged(object sender, TextChangedEventArgs e)
            {
                ((TextBox)sender).ScrollToEnd();
            }
        }
    }

As far as I can tell, it behaves exactly as desired. Here's a test case with several text boxes in a window that allows the attached AlwaysScrollToEnd property to be set in various ways (hard-coded, with a CheckBox.IsChecked binding and in code-behind):

Xaml:

    <Window x:Class="AttachedPropertyTest.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="AttachedPropertyTest" Height="800" Width="300"
        xmlns:local="clr-namespace:AttachedPropertyTest">
        <Window.Resources>
            <Style x:Key="MultiLineTB" TargetType="TextBox">
                <Setter Property="IsReadOnly" Value="True"/>
                <Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
                <Setter Property="Height" Value="60"/>
                <Setter Property="Text" Value="{Binding Text, ElementName=tbMaster}"/>
            </Style>
        </Window.Resources>

        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

            <TextBox Background="LightYellow" Name="tbMaster" Height="150" AcceptsReturn="True"/>

            <TextBox Style="{StaticResource MultiLineTB}" Grid.Row="1" local:TextBoxUtilities.AlwaysScrollToEnd="True"/>
            <TextBox Style="{StaticResource MultiLineTB}" Grid.Row="2"/>
            <TextBox Style="{StaticResource MultiLineTB}" Grid.Row="3" Name="tb3" local:TextBoxUtilities.AlwaysScrollToEnd="True"/>
            <TextBox Style="{StaticResource MultiLineTB}" Grid.Row="4" Name="tb4"/>
            <CheckBox Grid.Column="1" Grid.Row="4" IsChecked="{Binding (local:TextBoxUtilities.AlwaysScrollToEnd), Mode=TwoWay, ElementName=tb4}"/>
            <Button Grid.Row="5" Click="Button_Click"/>
        </Grid>
    </Window>

Code-Behind:

    using System;
    using System.Windows;
    using System.Windows.Controls;

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

            void Button_Click(object sender, RoutedEventArgs e)
            {
                TextBoxUtilities.SetAlwaysScrollToEnd(tb3, true);
            }
        }
    }
Community
  • 1
  • 1
O. R. Mapper
  • 20,083
  • 9
  • 69
  • 114
  • How should these work ? The TextBox don't have any AlwaysScrollToEnd Property. I can't see any AlwaysScrollToEnd Property in youre code, too. Sorry never make my own attached properties. – MR.ABC Mar 19 '13 at 21:35
  • @Abc: The `AlwaysScrollToEnd` attached property is declared in the statement starting with `public static readonly DependencyProperty AlwaysScrollToEndProperty ...`. You can use it in Xaml like this: ``, i.e. as if the text box had such a property. Also look at the example code included in the answer to see more of this. Please [read up on attached properties](http://www.wpftutorial.net/DependencyProperties.html#attached) to learn more about this concept :-) – O. R. Mapper Mar 19 '13 at 21:45
  • I had used a wrong namepsace, that was my problem. VS had thrown a error. – MR.ABC Mar 19 '13 at 22:02
  • it doens't work for me.. I have this why? – Piero Alberto Jan 14 '15 at 09:54
  • 1
    @PieroAlberto: Have you somehow restricted the height of your `TextBox`? Otherwise, it will just grow vertically and no scrolling will be required within the control, but bottom part of the `TextBox` might grow out of its parent's bounding box and get clipped. – O. R. Mapper Jan 15 '15 at 12:23
  • @O.R.Mapper oh, thanks man!!! :) now it works! Only one last question: I can't see the scrollbar, I can scroll only with the mouse wheel. How can I see the scrollbar? – Piero Alberto Jan 15 '15 at 12:36
  • 1
    @PieroAlberto: Use the [`VerticalScrollBarVisibility` property](http://msdn.microsoft.com/en-us/library/system.windows.controls.primitives.textboxbase.verticalscrollbarvisibility%28v=vs.110%29.aspx). – O. R. Mapper Jan 15 '15 at 12:41
  • your version requires something in the Control class, which isn't very MVVM friendly. – Simon_Weaver Feb 27 '15 at 03:51
  • @Simon_Weaver: Would you mind to clarify, please? What "something in the Control class" are you referring to? Do you mean the `System.Windows.Controls.Control` class? What is required in that class? – O. R. Mapper Feb 27 '15 at 08:19
  • @O.R.Mapper sorry that wasn't a great explanation. I meant you need to write code in the class deriving from `Control` which might be a `UserControl` or `Window`. If you're using MVVM you wouldn't be writing event handler and ideally not manipulating controls in code behind. Scott's solution that you referred to only needs Xaml to be changed. – Simon_Weaver Feb 27 '15 at 08:23
  • @Simon_Weaver: I don't understand. Scott's solution does register event handlers with the control (see e.g. `textBox.Loaded += TextBoxLoaded;`). That's how the attached property has to work internally. If the implementation of the attached property does not access the control somehow, no amount of attached properties in Xaml would be of any help. – O. R. Mapper Feb 27 '15 at 08:32
  • @O.R.Mapper actually looking at your code more closely I think I was confused by the Button_Click event, which appears to be redundant (since the property is already attached). So I think I thought your code required explicitly hooking up in the code-behind which is looks now like it doesn't. Sorry for confusing both of us – Simon_Weaver Feb 27 '15 at 08:40
  • @Simon_Weaver: Ooh, I see - the `Button_Click` is a part of the minimal demo to check how the code works; it was meant to demonstrate how to set the attached property from code-behind. Looking at it again, I am not sure now why that property is already set in Xaml, as well, for the same `TextBox`. Maybe I'll find that needs a little correction. – O. R. Mapper Feb 27 '15 at 08:46
  • Perfect. I was looking for something that I could control via a CheckBox, and this fit the bill. Thanks! – Mike Loux Jul 10 '15 at 16:10
4

Hmm this seemed like an interesting thing to implement so I took a crack at it. From some goggling it doesn't seem like there is a straight forward way to "tell" the Textbox to scroll itself to the end. So I thought of it a different way. All framework controls in WPF have a default Style/ControlTemplate, and judging by the looks of the Textbox control there must be a ScrollViewer inside which handles the scrolling. So, why not just work with a local copy of the default Textbox ControlTemplate and programmaticlly get the ScrollViewer. I can then tell the ScrollViewer to scroll its Contents to the end. Turns out this idea works.

Here is the test program I wrote, could use some refactoring but you can get the idea by looking at it:

Here is the XAML:

<Window x:Class="WpfApplication3.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:WpfApplication3="clr-namespace:WpfApplication3"
        Title="MainWindow" Height="350" Width="525">
  <Window.Resources>
    <!--The default Style for the Framework Textbox-->
    <SolidColorBrush x:Key="DisabledForegroundBrush" Color="#888" />
    <SolidColorBrush x:Key="DisabledBackgroundBrush" Color="#EEE" />
    <SolidColorBrush x:Key="WindowBackgroundBrush" Color="#FFF" />
    <SolidColorBrush x:Key="SolidBorderBrush" Color="#888" />
    <ControlTemplate x:Key="MyTextBoxTemplate" TargetType="{x:Type TextBoxBase}">
      <Border x:Name="Border" CornerRadius="2" Padding="2" Background="{StaticResource WindowBackgroundBrush}"
              BorderBrush="{StaticResource SolidBorderBrush}" BorderThickness="1">
        <ScrollViewer Margin="0" x:Name="PART_ContentHost" />
      </Border>
      <ControlTemplate.Triggers>
        <Trigger Property="IsEnabled" Value="False">
          <Setter TargetName="Border" Property="Background" Value="{StaticResource DisabledBackgroundBrush}" />
          <Setter TargetName="Border" Property="BorderBrush" Value="{StaticResource DisabledBackgroundBrush}" />
          <Setter Property="Foreground" Value="{StaticResource DisabledForegroundBrush}" />
        </Trigger>
      </ControlTemplate.Triggers>
    </ControlTemplate>
    <Style x:Key="MyTextBox" TargetType="{x:Type TextBoxBase}">
      <Setter Property="SnapsToDevicePixels" Value="True" />
      <Setter Property="OverridesDefaultStyle" Value="True" />
      <Setter Property="KeyboardNavigation.TabNavigation" Value="None" />
      <Setter Property="FocusVisualStyle" Value="{x:Null}" />
      <Setter Property="MinWidth" Value="120" />
      <Setter Property="MinHeight" Value="20" />
      <Setter Property="AllowDrop" Value="true" />
      <Setter Property="Template" Value="{StaticResource MyTextBoxTemplate}"></Setter>
    </Style>

  </Window.Resources>
  <Grid>
    <WpfApplication3:AutoScrollTextBox x:Name="textbox" TextWrapping="Wrap" Style="{StaticResource MyTextBox}"
                                       VerticalScrollBarVisibility="Visible" AcceptsReturn="True" Width="100" Height="100">test</WpfApplication3:AutoScrollTextBox>
  </Grid>
</Window>

And the code behind:

using System;
using System.Windows;
using System.Windows.Controls;

namespace WpfApplication3
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            for (int i = 0; i < 10; i++)
            {
                textbox.AppendText("Line " + i + Environment.NewLine);
            }
        }
    }

    public class AutoScrollTextBox : TextBox
    {
        protected override void OnTextChanged(TextChangedEventArgs e)
        {
            base.OnTextChanged(e);
            // Make sure the Template is in the Visual Tree: 
            // http://stackoverflow.com/questions/2285491/wpf-findname-returns-null-when-it-should-not
            ApplyTemplate();
            var template = (ControlTemplate) FindResource("MyTextBoxTemplate");
            var scrollViewer = template.FindName("PART_ContentHost", this) as ScrollViewer;
            //SelectionStart = Text.Length;
            scrollViewer.ScrollToEnd();
        }
    }
}
Bojin Li
  • 5,769
  • 2
  • 24
  • 37
3

A more portable way might be to use an attached property such as in this similar question for listbox.

(Just set VerticalOffset when the Text property changes)

Community
  • 1
  • 1
GazTheDestroyer
  • 20,722
  • 9
  • 70
  • 103
3

Similar answer to the other answers, but without the statics events and control dictionary. (IMHO, static events are best avoided if possible).

public class ScrollToEndBehavior
{
    public static readonly DependencyProperty OnTextChangedProperty =
                DependencyProperty.RegisterAttached(
                "OnTextChanged",
                typeof(bool),
                typeof(ScrollToEndBehavior),
                new UIPropertyMetadata(false, OnTextChanged)
                );

    public static bool GetOnTextChanged(DependencyObject dependencyObject)
    {
        return (bool)dependencyObject.GetValue(OnTextChangedProperty);
    }

    public static void SetOnTextChanged(DependencyObject dependencyObject, bool value)
    {
        dependencyObject.SetValue(OnTextChangedProperty, value);
    }

    private static void OnTextChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        var textBox = dependencyObject as TextBox;
        var newValue = (bool)e.NewValue;

        if (textBox == null || (bool)e.OldValue == newValue)
        {
            return;
        }

        TextChangedEventHandler handler = (object sender, TextChangedEventArgs args) =>
            ((TextBox)sender).ScrollToEnd();

        if (newValue)
        {
            textBox.TextChanged += handler;
        }
        else
        {
            textBox.TextChanged -= handler;
        }
    }
}

This is just an alternative to the other posted solutions, which were among the best I found after looking for awhile (i.e. concise & mvvm).

Steve Kinyon
  • 805
  • 1
  • 7
  • 15
1

The problem with the "ScrollToEnd" method is that the TextBox has to be visible, or it won't scroll.

Therefore a better method is to set the TextBox Selection property to end of document:

  static void tb_TextChanged(object sender, TextChangedEventArgs e)
  {
     TextBox tb = sender as TextBox;
     if (tb == null)
     {
        return;
     }

     // set selection to end of document
     tb.SelectionStart = int.MaxValue;
     tb.SelectionLength = 0;         
  }

BTW, the memory leak handling in the first example is likely unnecessary. The TextBox is the publisher and the static Attached Property event handler is the subscriber. The publisher keeps a reference to the subscriber which can keep the subscriber alive (not the other way around.) So if a TextBox goes out of scope, so will the reference to the static event handler (i.e., no memory leak.)

So hooking up the Attached Property can be handled simpler:

  static void OnAutoTextScrollChanged
      (DependencyObject obj, DependencyPropertyChangedEventArgs args)
  {
     TextBox tb = obj as TextBox;
     if (tb == null)
     {
        return;
     }

     bool b = (bool)args.NewValue;

     if (b)
     {
        tb.TextChanged += tb_TextChanged;
     }
     else
     {
        tb.TextChanged -= tb_TextChanged;
     }
  }
Community
  • 1
  • 1
countzero
  • 51
  • 3