Tuesday, December 18, 2012

Select All the on focus in WPF textbox

WPF has made it possible to create beautiful and compelling user interfaces in .NET - but there are a few times when the built-in user experience behavior of some of the controls isn't quite what you'd expect.
One of these areas happens to be the focus behavior of text boxes.

The Problem

A common behavior in most Windows applications that users are accustomed to(often without realizing it), is that upon focus entering a textbox, the text is automatically selected. This makes it possible to click or tab in to a textbox and immediately start typing - and content will be automatically replaced. This behavior is part of standard Windows text boxes ... but not the WPF TextBox control.
The approach to resolving this that may occur to you, is to handle focus events on all TextBox control instances and call the convenient SelectAll() method to highlight all content. Unfortunately, it's not quite that simple. Beyond the fact that wiring every TextBox control in your application gets old fast, there are a number of subtle edge cases where this approach fails.
We're going to look at how we can restore this behavior to text box controls in your WPF applications with as little pain as possible - and make it work they way users would expect - in all cases.

The Approach

The first issue we're going to tackle is how to respond to focus behavior on all text boxes throughout an application. It may occur to you to create a global style for text box controls and use an EventSetter to subscribe to Control.GetFocus. Unfortunately, styles have a number of limitations that make them less than ideal for this purpose. You can't apply multiple styles to a single control in WPF - which means that text box styles throughout your application would need to replicate this behavior. The best you can do is create a common style that all other TextBox styles are based on. This is a fragile and awkward approach at best - and at worst it becomes problematic if one of the derived styles needs to apply its own processing on GetFocus.
Fortunately there's a better way: the EventManager class. EventManager provides us with a way to globally subscribe to events for a particular type of control, rather than on an instance-by-instance basis. As it turns out, a convenient place to put this code is in your Application class' OnStartup() method:

protected override void OnStartup(StartupEventArgs e)
{
    EventManager.RegisterClassHandler(typeof(TextBox), UIElement.GotFocusEvent,
                                     
new RoutedEventHandler(SelectAllText), true);
   
    base.OnStartup(e);
}

private static void SelectAllText(object sender, RoutedEventArgs e)
{
    var textBox = e.OriginalSource as TextBox;
    if (textBox != null)
        textBox.SelectAll();
}

So we're done now, right? Well ... not quite. A little experimentation will show that while using the keyboard to set focus works well, using the mouse doesn't work as expected: when clicked, the selection initially appears, and then promptly vanishes. The reason for this is that the TextBox control internally responds to the MouseDown event, which fires after GotFocus. The text box relies on that event to place the insertion caret at the location in the text box where the user clicked. Since all of this happens GotFocus, the selection we set is lost. We're going to fix that.

The Ultimate Solution

The solution presented here is a little different from that above - basically, we're going to divide focus processing into two separation mechanisms - one that deals with focus changes from the keyboard, and another that responds to focus established by clicking the mouse.
Dealing with keyboard focus is easy - WPF has a UIElement.GotKeyboardFocus event that we can respond to instead of the more generalized Control.GotFocus. The mouse is a little bit trickier. First, we don't want to break the default click handling that TextBox implements - so we're going to instead use the PreviewMouseLeftButtonDown event to intercept clicks before the control responds to them. Preview events, also known as tunneling events, are routed events where the direction of the route travels from the application root towards the element that raised the event and is reported as the source in event data. We can use the preview mouse event to selectively handle the mouse click only in those cases when the focus is not yet transferred to the text box.
Now that we've established the approach, let's look at the code:

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        EventManager.RegisterClassHandler(typeof(TextBox), UIElement.PreviewMouseLeftButtonDownEvent,
           new MouseButtonEventHandler(SelectivelyHandleMouseButton), true);
        EventManager.RegisterClassHandler(typeof(TextBox), UIElement.GotKeyboardFocusEvent,
          new RoutedEventHandler(SelectAllText), true);

        base.OnStartup(e);
    }

    private static void SelectivelyHandleMouseButton(object sender, MouseButtonEventArgs e)
    {
        var textbox = (sender as TextBox);
        if (textbox != null && !textbox.IsKeyboardFocusWithin)
        {
            if( e.OriginalSource.GetType().Name == "TextBoxView" )
            {
                e.Handled = true;
                textbox.Focus();
            }
        }
    }

    private static void SelectAllText(object sender, RoutedEventArgs e)
    {
        var textBox = e.OriginalSource as TextBox;
        if (textBox != null)
            textBox.SelectAll();
    }
}

So let's break it down. In the OnStartup() method we globally register the GotKeyBoardFocus and PreviewMouseLeftButtonDown events to the handlers below.
The SelectAllText() method is the same as before. The SelectivelyHandleMouseButton() method provides the code that corrects the focus behavior on clicks. First, it checks that we're actually dealing with a TextBox control by casting the sender parameter to the appropriate type. If keyboard focus is not yet in that text box (which we check using the IsKeyboardFocusWithin property), it then marks the event as handled, and calls textbox.Focus() to transfer focus (which will trigger the GotKeyboardFocus event for us).
There's only one last bit of code here that deserves an explanation, the line:

   if( e.OriginalSource.GetType().Name == "TextBoxView" )

The last edge case we need to handle is when the user clicks in the "chrome" portion of a TextBox - things like the border or scroll bars. It would be really awkward to select the text each time the user clicked the scrollbar. To fix this, we need to test that the mouse click originated in the "textual" portion of the control - which happens to be implemented by the TextBoxView class. WPF is helps us out by notifying us of the original source of the event via the RoutedEventArgs.OriginalSource property. Unfortunately, this class is not public outside of WPF infrastructure code - so we have to test for it by it's name. This is by far the least attractive aspect of the code shown above ... but sadly, it's the only reliable solution I've found so far.

Conclusion

So let's look at what we've learned so far.
  1. The built-in WPF TextBox control doesn't automatically select text when it receives focus.
  2. We can globally respond to events from any WPF control using the EventManager.RegisterGlobalHandler() method.
  3. We can use the OnStartup() method of the Application class to centrally register our handlers when the application loads.
  4. Focus behavior in WPF varies slightly depending on whether the control receives focus via the mouse or keyboard.
  5. We can use the PreviewXXX family of events to intercept clicks and supply our own event handling before the TextBox controls responds.
  6. We can use the RoutedEventArgs.OriginalSource property to avoid highlighting a selection when the original click events occurs within a non-content area of the TextBox control.
Put this all together, and we have an elegant, centralized solution to selecting the text inside the TextBox control when it receives focus.