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