I Thought He Came With You is Robert Ellison’s blog about software, marketing, politics, photography, time lapse and the occasional well deserved rant. Follow along with a monthly email, RSS or on Facebook. About 7,250,102,787 people have not visited yet so it might be your first time here. Suggested reading: Got It, or roll the dice.

WPF commands with nested focus scope

Here's a frustrating WPF scenario — you use the ApplicationCommands class to add Cut, Copy and Paste commands to toolbar and then put a TextBox on another toolbar. Click in the TextBox and the commands remain disabled. WTF, WPF?

The problem is with focus scopes. Your window is a focus scope and so are any menus or toolbars. This has the desirable property of allowing commands to target the control you were in immediately before invoking the command. You want paste to target the text box you're editing, not the menu item or button you clicked to request the paste.

So far so good. The problem is that the commanding system isn't smart enough to target the control with keyboard focus if it's in a nested focus scope. Remember that the window itself is a focus scope so our TextBox in a ToolBar (also a focus scope) is nested and immune to commands from our menu or toolbar.

Here's a simple window that demonstrates the problem:

<Window x:Class="WpfFocusScopeTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded">

    <Window.CommandBindings>
        <CommandBinding Command="Paste" x:Name="bindingPaste" PreviewCanExecute="bindingPaste_PreviewCanExecute" />
    </Window.CommandBindings>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" /><RowDefinition Height="Auto"  /><RowDefinition />
        </Grid.RowDefinitions>
        
        <Menu>
            <MenuItem Header="Edit"><MenuItem Command="Paste" Name="menuItemPaste"/></MenuItem>
        </Menu>
        
        <ToolBarTray Grid.Row="1">
            <ToolBar Band="0" BandIndex="0">
                <Button Command="Paste" Name="buttonPaste">Paste</Button>
            </ToolBar>
            <ToolBar Band="0" BandIndex="1">
                <TextBox Width="100" />
            </ToolBar>
        </ToolBarTray>

        <DockPanel Grid.Row="2">
            <TextBox />
        </DockPanel>

    </Grid>
</Window>

Ignore the PreviewCanExecute handler for now. If you run this window and click in the main TextBox the paste button and menu item are enabled. Click in the toolbar TextBox and pasting isn't an option. Well, Ctrl-V still works and there's a context menu but you know what I mean.

The problem can be fixed by adding a command binding for ApplicationCommands.Paste and handling the PreviewCanExecute event:

public partial class MainWindow : Window
{
    private DependencyObject _menuFocusScope;
    private DependencyObject _toolbarFocusScope;

    public MainWindow()
    {
        InitializeComponent();
    }

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        _menuFocusScope = FocusManager.GetFocusScope(menuItemPaste);
        _toolbarFocusScope = FocusManager.GetFocusScope(buttonPaste);
    }  

    private void bindingPaste_PreviewCanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
        IInputElement keyboardFocus = Keyboard.FocusedElement;

        if ((keyboardFocus != null) && (keyboardFocus != this))
        {
            DependencyObject pasteTargetAsDependencyObjeect = 
                keyboardFocus as DependencyObject;
            if (pasteTargetAsDependencyObjeect != null)
            {
                DependencyObject targetFocusScope = 
                    FocusManager.GetFocusScope(pasteTargetAsDependencyObjeect);

                if ((targetFocusScope != _menuFocusScope) && 
                    (targetFocusScope != _toolbarFocusScope) )
                {
                    menuItemPaste.CommandTarget = keyboardFocus;
                    buttonPaste.CommandTarget = keyboardFocus;
                }
            }
        }
    }
}

When the window loads we're making note of the focus scopes for the toolbar and menu. Then when PreviewCanExecute fires we check to see if the element with the keyboard focus is in a different focus scope (and also that the window doesn't have keyboard focus). We then set the CommandTarget for the menu item and button to the element that has keyboard focus.

A handler isn't required for CanExecute as the command will take care of this with respect to the new CommandTarget.

Run the window again and you'll see that the paste button is enabled for both of the TextBox controls. When you click the button (or menu item) our PreviewCanExecute handler ignores the new keyboard focus and the command is sent to the desired control. 

One drawback of this approach is that keyboard focus isn't returned to the TextBox after the command executes. The CommandTarget remains in place so you can keep pasting and the command remains enabled but you lose the visual cue that lets you know where the target is. I haven't figured out a clean approach to this yet. When I do, I'll update this post. Better yet, if you've figured it out leave a comment.

Add Comment

All comments are moderated to weed out spam. Email address is optional and is only used to display your Gravatar.