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?
Step 2 : After creating TextBox, set the ScrollBars property of the TextBox provided by the TextBox class. // Set ScrollBars property Mytextbox. ScrollBars = ScrollBars.
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; } } } }
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);
}
}
}
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();
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With