Reputation: 4210
I have a TextBox whose text can be changed programmatically via binding from its Text
property to a viewmodel property. This may for example happen as a result of a keypress (e.g. ↑ or ↓), but can also happen without any user input whatsoever. When this happens, it seems that any existing selection in the text box is removed. The behavior I desire is: If a text box has focus and all of the text is selected before the programmatic change (or if the text is empty), I want all of the text to be selected after the change. The text should however not be selected after a change caused by the user typing, since that would mean the user would just be replacing one character over and over again.
I have not found a way to accomplish this. Is it possible?
To be specific: I have set up a global event handler to select all text when a TextBox is focused, in order to allow users to more easily edit existing text in the TextBox if desired:
EventManager.RegisterClassHandler(
typeof(TextBox),
UIElement.GotFocusEvent,
new RoutedEventHandler((s, _) => (s as TextBox)?.SelectAll()));
However, in one of my views, tabbing out of TextBox A triggers an asynchronous action that changes the text in TextBox B (which is next in the tab order). This happens very quickly, but TextBox B gets focus before the text change happens, and thus the text is not selected. I would like the text that arrives in TextBox B to be selected so the user can more easily change it if desired.
Upvotes: 2
Views: 739
Reputation: 16991
I prefer implementing this kind of functionality in a Behavior
that can be added in XAML; this requires the System.Windows.Interactivity.WPF NuGet Package.
I haven't tested this fully because I'm not exactly sure how to replicate your "asynchronous action", but it seems to work for the "normal" programmatic value changes that I've tried.
If you you really don't want the Behavior
aspect of it, it should be fairly trivial to extract the event handling logic from it to use in whatever method you prefer.
Here is a short gif of it in action:
public class KeepSelectionBehavior : Behavior<TextBox>
{
private bool _wasAllTextSelected = false;
private int inputKeysDown = 0;
protected override void OnAttached()
{
base.OnAttached();
CheckSelection();
AssociatedObject.TextChanged += TextBox_TextChanged;
AssociatedObject.SelectionChanged += TextBox_SelectionChanged;
AssociatedObject.PreviewKeyDown += TextBox_PreviewKeyDown;
AssociatedObject.KeyUp += TextBox_KeyUp;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.TextChanged -= TextBox_TextChanged;
AssociatedObject.SelectionChanged -= TextBox_SelectionChanged;
AssociatedObject.PreviewKeyDown -= TextBox_PreviewKeyDown;
AssociatedObject.KeyUp -= TextBox_KeyUp;
}
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
if (_wasAllTextSelected && inputKeysDown == 0)
{
AssociatedObject.SelectAll();
}
CheckSelection();
}
private void TextBox_SelectionChanged(object sender, RoutedEventArgs e)
{
CheckSelection();
}
private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (IsInputKey(e.Key))
{
inputKeysDown++;
}
}
private void TextBox_KeyUp(object sender, KeyEventArgs e)
{
if (IsInputKey(e.Key))
{
inputKeysDown--;
}
}
private bool IsInputKey(Key key)
{
return
key == Key.Space ||
key == Key.Delete ||
key == Key.Back ||
(key >= Key.D0 && key <= Key.Z) ||
(key >= Key.Multiply && key <= Key.Divide) ||
(key >= Key.Oem1 && key <= Key.OemBackslash);
}
private void CheckSelection()
{
_wasAllTextSelected = AssociatedObject.SelectionLength == AssociatedObject.Text.Length;
}
}
You can use it like this:
<Window
x:Class="ScriptyBot.Client.TestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="TestWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<StackPanel>
<TextBox Name="TextBox1" Margin="20">
<i:Interaction.Behaviors>
<behaviors:KeepSelectionBehavior />
</i:Interaction.Behaviors>
</TextBox>
</StackPanel>
</Window>
I'm testing it with a simple DispatchTimer
that updates the text every second:
public partial class TestWindow : Window
{
private DispatcherTimer timer;
public TestWindow()
{
InitializeComponent();
timer = new DispatcherTimer(DispatcherPriority.Normal);
timer.Interval = TimeSpan.FromSeconds(1);
timer.Tick += (sender, e) => { TextBox1.Text = DateTime.Now.ToString(); };
timer.Start();
}
}
By default, a Behavior
has to be applied to every control manually in XAML, which can be very annoying. If you instead use this base class for your Behavior
, you will be able to add it using a Style
. This also works with implicit Styles too, so you can set it once in app.xaml
, instead of manually for every control.
public class AttachableForStyleBehavior<TComponent, TBehavior> : Behavior<TComponent>
where TComponent : System.Windows.DependencyObject
where TBehavior : AttachableForStyleBehavior<TComponent, TBehavior>, new()
{
public static readonly DependencyProperty IsEnabledForStyleProperty =
DependencyProperty.RegisterAttached(name: "IsEnabledForStyle",
propertyType: typeof(bool),
ownerType: typeof(AttachableForStyleBehavior<TComponent, TBehavior>),
defaultMetadata: new FrameworkPropertyMetadata(false, OnIsEnabledForStyleChanged));
public bool IsEnabledForStyle
{
get => (bool)GetValue(IsEnabledForStyleProperty);
set => SetValue(IsEnabledForStyleProperty, value);
}
private static void OnIsEnabledForStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is UIElement uiElement)
{
var behaviors = Interaction.GetBehaviors(uiElement);
var existingBehavior = behaviors.FirstOrDefault(b => b.GetType() == typeof(TBehavior)) as TBehavior;
if ((bool)e.NewValue == false && existingBehavior != null)
{
behaviors.Remove(existingBehavior);
}
else if ((bool)e.NewValue == true && existingBehavior == null)
{
behaviors.Add(new TBehavior());
}
}
}
}
The declaration of the Behavior
class changes to look like this:
public class KeepSelectionBehavior : AttachableForStyleBehavior<TextBox, KeepSelectionBehavior>
And is applied like this (It can even be bound to a bool
and dynamically turned on and off!):
<Style TargetType="TextBox">
<Setter Property="KeepSelectionBehavior.IsEnabledForStyle" Value="True" />
</Style>
Personally, I prefer using the Style
based method anyway, even when adding it to a single, one-off, control. It is significantly less typing, and I don't have to remember how to define the xmlns
for the Interactions
or Behaviors
namespaces.
Upvotes: 2
Reputation: 169200
I would like the text that arrives in
TextBox
B to be selected so the user can more easily change it if desired.
Handle the TextChanged
event then. This event is raised whenever the Text
property is changed. Yoy may want to add a delay so the user can type without the text being selected on each key stroke:
private DateTime _last;
private void txt2_TextChanged(object sender, TextChangedEventArgs e)
{
if (DateTime.Now.Subtract(_last) > TimeSpan.FromSeconds(3))
{
TextBox tb = (TextBox)sender;
if (Keyboard.FocusedElement == tb)
tb.SelectAll();
}
_last = DateTime.Now;
}
Upvotes: 0