Click here to Skip to main content
15,881,866 members
Articles / Desktop Programming / WPF

Using the WPF FocusScope

Rate me:
Please Sign up or sign in to vote.
4.97/5 (22 votes)
26 Jul 2009MIT5 min read 154.5K   3.8K   46   11
Explains why WPF seems to break if you try to use FocusScope, and provides a simple solution.

Introduction

Often, it is useful to maintain a separate focus for different parts of the user interface. For example, when you have a tab control with different data entry fields on each page, it makes sense to remember the focus for each individual page. The default WPF behavior is to reset the focus to the first child element whenever the active page changes - highly annoying if you switch tab pages using the Ctrl+Tab shortcut to look at something you entered on another page, and then when you Ctrl+Tab back to the tab page you were editing, you always have to fetch the mouse or press Tab N times to get back to the text box you were editing.

The solution to this problem is to remember the logical focus inside each tab page, and restore keyboard focus to the appropriate control when the page is activated again.

Screenshot Test Application

KeyboardNavigationMode.Once - A Solution?

You just set KeyboardNavigation.ControlTabNavigation="Once" on the GroupBox, and it suddenly starts working - as long as you only navigate using the keyboard. But in my usecase, it is a requirement to also restore the previous focus when the GroupBox is clicked with the mouse. Unfortunately, WPF doesn't provide any API for the magic behind KeyboardNavigationMode.Once; it seems to be impossible to programmatically restore focus without using Reflection to access WPF's internals (I hope someone proves me wrong on this).
If you feel adventurous, call KeyboardNavigation.GetActiveElement using Reflection and skip the rest of this article (the example code does this for the leftmost group box).

Focus Scope - A Solution?

Wait - separate logical focus from keyboard focus? WPF already does that!
It seems that we could simply set FocusManager.IsFocusScope="True", and WPF would do the hard work for us. Unfortunately, this has some horrible side effects.

The MSDN thread "A FocusScope Nightmare (Bug?)" captures my initial reaction quite well.

  • Why does this seemingly innocent change totally cripple WPF routed commands?
  • Did I run into a WPF bug?
  • How do I get out of this nightmare?

This article explains why the focus scopes in WPF work like they do; and it presents a simple solution that makes them work like we want.

What are the Problems with the WPF FocusScope?

  • Routed Commands do not work inside focus scopes.
  • It causes other controls to think they still have focus. In the screenshot at the beginning of the article, two text boxes display a caret.
  • Several controls like buttons and checkboxes will move focus somewhere else when pressed.

For What was FocusScope Designed?

Microsoft uses FocusScope in WPF to create a temporary secondary focus. Every ToolBar and Menu in WPF has its own focus scope.

With this knowledge, we can clearly see why we have those problems:

A toolbar button should not execute commands on itself, but on whatever had focus before the toolbar was clicked. To accomplish this, routed commands ignore the focus from focus scopes and use the 'main' logical focus instead.
This explains why routed commands don't work inside focus scopes.

Why does the large text box in the test application screenshot still display a caret? I don't know the answer to this - but why shouldn't it? Granted, the text box doesn't have the keyboard focus (the small text box in the WPF focus scope has that); but it still has the main logical focus in the active Window and is the receiver of all routed commands.

Why does the keyboard focus move to the large text box when you tab to the CheckBox in the WPF focus scope and press Space to toggle it?

Well, this is exactly what you expect when you click a menu item or a toolbar: the keyboard focus should return to the main focus. All ButtonBase-derived controls will do this.

How Does the WPF FocusScope Work Under the Covers?

If you don't know focus scopes yet: you can turn any control into a focus scope by setting the attached property FocusManager.IsFocusScope to true. The default styles of ToolBar and Menu do this; there isn't any magic involved with those controls.

Each focus scope stores the logical focus in the attached property FocusManager.FocusedElement. When a control receives keyboard focus, WPF will look up its parent focus scope (the nearest parent with IsFocusScope turned on) and assign it to the FocusedElement property, giving the control logical focus within that scope.

A WPF Window itself is a focus scope, so the main logical focus simply is the FocusedElement property on the Window instance. Routed Commands simply execute on the main focus.

The Solution

Actually, now that we know what is happening, there is only a single problem to fix: we need to ensure that the main logical focus gets set.

WPF only gives a control the logical focus within the nearest parent focus scope. We will simply give it the logical focus within all parent focus scopes.

But we don't want to break ToolBars and Menus. Instead, we will implement the new focus logic as an Attached Behavior. This allows using our improved focus scope as easily as the existing: t:EnhancedFocusScope.IsEnhancedFocusScope="True".

If we encounter a focus scope that is not of our 'enhanced' kind, we will stop, just like WPF does.

C#
static void OnGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
    IInputElement focusedElement = e.NewFocus;
    for (DependencyObject d = focusedElement as DependencyObject; 
		d != null; d = VisualTreeHelper.GetParent(d)) {
        if (FocusManager.GetIsFocusScope(d)) {
            d.SetValue(FocusManager.FocusedElementProperty, focusedElement);
            if (!(bool)d.GetValue(IsEnhancedFocusScopeProperty)) {
                break;
            }
        }
    }
}

Basically, this converts the focus scope from being 'temporary' (like menus/toolbars) to a permanent focus scope.

Now, all that's left is restoring the focus, e.g. when an empty area on the GroupBox is clicked. This can be easily done using:

C#
IInputElement storedFocus = FocusManager.GetFocusedElement(groupBox);
if (storedFocus != null)
    Keyboard.Focus(storedFocus);

Conclusion

I hope focus scopes are less of a mystery to you after reading this article.

We didn't have to do a lot to get them working, but we still ended up both manually saving and reading the focus.  
This raises the question whether we should use the WPF focus scopes at all - we could have simply invented our own kind instead.

History

  • 27 July, 2009: Article published

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Germany Germany
I am the lead developer on the SharpDevelop open source project.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Raul Rangel21-Jan-11 4:52
Raul Rangel21-Jan-11 4:52 
GeneralMessage Closed Pin
16-Feb-17 13:49
Member 1299680216-Feb-17 13:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.