cmeeren
cmeeren

Reputation: 4210

Keep TextBox selection after text changes programmatically

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

Answers (2)

Bradley Uffner
Bradley Uffner

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:

Code in Action Animation

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

mm8
mm8

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

Related Questions