holder wpf xaml mvvm

wpf - holder - ¿Cómo creo un TextRolling TextBox?



placeholder xaml textbox (6)

Tengo una aplicación WPF que contiene un TextBox de líneas múltiples que se está utilizando para mostrar el resultado del texto de depuración.

¿Cómo puedo configurar el TextBox para que, cuando el texto se anexe al cuadro, se desplace automáticamente a la parte inferior del cuadro de texto?

  • Estoy usando el patrón MVVM.
  • Idealmente, un enfoque XAML puro sería agradable.
  • El TextBox en sí mismo no está necesariamente enfocado.

El problema con el método "ScrollTo End" es que el TextBox tiene que estar visible, o no se desplazará.

Por lo tanto, un mejor método es establecer la propiedad TextBox Selection al final del documento:

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

Por cierto, el manejo de fuga de memoria en el primer ejemplo es probablemente innecesario . TextBox es el publicador y el controlador de eventos de propiedad adjunta estático es el suscriptor. El editor mantiene una referencia al suscriptor que puede mantener vivo al suscriptor (y no al revés). Por lo tanto, si un TextBox queda fuera del alcance, también lo hará la referencia al controlador de eventos estáticos (es decir, sin pérdida de memoria).

Así que conectar la propiedad adjunta se puede manejar de manera más simple:

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


Esta solución está inspirada en la solución de Scott Ferguson con la propiedad adjunta, pero evita almacenar un diccionario interno de asociaciones y, por lo tanto, tiene un código algo más corto:

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

Por lo que puedo decir, se comporta exactamente como se desea. Aquí hay un caso de prueba con varios cuadros de texto en una ventana que permite establecer la propiedad AlwaysScrollToEnd adjunta de varias maneras (codificada, con un enlace CheckBox.IsChecked y en código subyacente):

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>

Código detrás:

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, esto parecía una cosa interesante de implementar, así que lo intenté. A partir de algunas gafas, no parece que haya una manera directa de "decirle" al cuadro de texto que se desplace hasta el final. Entonces lo pensé de otra manera. Todos los controles de marco en WPF tienen un Estilo / ControlTemplate predeterminado, y a juzgar por el aspecto del control de cuadro de texto debe haber un ScrollViewer dentro del cual se maneja el desplazamiento. Entonces, ¿por qué no solo trabajar con una copia local del TextBox ControlTemplate predeterminado y obtener programáticamente el ScrollViewer? Luego puedo decirle al ScrollViewer que desplace su Contenido hasta el final. Resulta que esta idea funciona.

Aquí está el programa de prueba que escribí, podría usar algunas refactorizaciones, pero puede obtener la idea mirándolas:

Aquí está el 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>

Y el código detrás:

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


La respuesta proporcionada por @BojinLi funciona bien. Sin embargo, después de leer la respuesta vinculada por @GazTheDestroyer, decidí implementar mi propia versión para TextBox, porque parecía más limpia.

Para resumir, puede extender el comportamiento del control TextBox utilizando una propiedad adjunta. (Llamado ScrollOnTextChanged)

Usarlo es simple:

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

Aquí está la clase TextBoxBehaviour:

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


Respuesta similar a las otras respuestas, pero sin los eventos estáticos y el diccionario de control. (En mi humilde opinión, es mejor evitar los eventos estáticos si es posible).

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

Esta es solo una alternativa a las otras soluciones publicadas, que se encontraban entre las mejores que encontré después de buscar por un tiempo (es decir, concisa y mvvm).


Una forma más portátil podría ser usar una propiedad adjunta, como en esta pregunta similar para listbox .

(Simplemente configure VerticalOffset cuando la propiedad Text cambie)