Strange focus behavior for simple WPF ItemsControl

2019-03-20 20:30发布

问题:

I'm seeing strange behavior when it comes to focus and keyboard navigation. In the example below I have a simple ItemsControl that has been templated so that it presents a list of CheckBoxes bound to the ItemsSource.

<ItemsControl FocusManager.IsFocusScope="True"
              ItemsSource="{Binding ElementName=TheWindow, Path=ListOStrings}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <CheckBox Content="{Binding}" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

For some strange reason the FocusManager.IsFocusScope="True" assignment causes keyboard focus to fail to be set when checking a checkbox via a mouse click and for focus to jump out of the ItemsControl when a check box is checked using the space bar on the keyboard. Both symptoms seem to point to some strange navigation happening when the checkbox is checked but I'm having a hard time getting to the bottom of it.

This problem occurs if I set any parent element up the visual tree as a focus scope using this method. If I remove the FocusManager.IsFocusScope="True" then the problems go away. Unfortunately I'm seeing this problem in a larger project where I cannot just remove these focus scopes without worrying about other focus related consequences.

Could somebody explain to me the strange behavior I'm seeing? Is this a bug or am I just completely missing something?

回答1:

This article explains it very well: http://www.codeproject.com/KB/WPF/EnhancedFocusScope.aspx

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.

And this part covers the behavior you're seeing

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.



回答2:

@Meleak explained the problem very well. Please read the article http://www.codeproject.com/KB/WPF/EnhancedFocusScope.aspx to fully understand what the problem is and how to solve it. I will just add the complete implementation of IsEnhancedFocusScope attached behavior mentioned in the article:

public static class FocusExtensions
{
    private static bool SettingKeyboardFocus { get; set; }

    public static bool GetIsEnhancedFocusScope(DependencyObject element) {
        return (bool)element.GetValue(IsEnhancedFocusScopeProperty);
    }

    public static void SetIsEnhancedFocusScope(DependencyObject element, bool value) {
        element.SetValue(IsEnhancedFocusScopeProperty, value);
    }

    public static readonly DependencyProperty IsEnhancedFocusScopeProperty =
        DependencyProperty.RegisterAttached(
            "IsEnhancedFocusScope",
            typeof(bool),
            typeof(FocusExtensions),
            new UIPropertyMetadata(false, OnIsEnhancedFocusScopeChanged));

    private static void OnIsEnhancedFocusScopeChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e) {
        var item = depObj as UIElement;
        if (item == null)
            return;

        if ((bool)e.NewValue) {
            FocusManager.SetIsFocusScope(item, true);
            item.GotKeyboardFocus += OnGotKeyboardFocus;
        }
        else {
            FocusManager.SetIsFocusScope(item, false);
            item.GotKeyboardFocus -= OnGotKeyboardFocus;
        }
    }

    private static void OnGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) {
        if (SettingKeyboardFocus) {
            return;
        }

        var focusedElement = e.NewFocus as Visual;

        for (var d = focusedElement; d != null; d = VisualTreeHelper.GetParent(d) as Visual) {
            if (FocusManager.GetIsFocusScope(d)) {
                SettingKeyboardFocus = true;

                try {
                    d.SetValue(FocusManager.FocusedElementProperty, focusedElement);
                }
                finally {
                    SettingKeyboardFocus = false;
                }

                if (!(bool)d.GetValue(IsEnhancedFocusScopeProperty)) {
                    break;
                }
            }
        }
    }
}

In your XAML you just need to set this attached property instead of standard IsFocusScope property:

<ItemsControl my:FocusExtensions.IsEnhancedFocusScope="True"
              ItemsSource="{Binding ElementName=TheWindow, Path=ListOStrings}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <CheckBox Content="{Binding}" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

It will work as you expect the focus scope to work.