Inconsistency in WPF command routing behavior depending on the UI focus state

后端 未结 3 485
暖寄归人
暖寄归人 2020-12-31 04:05

I have a RoutedUICommand command which can be fired in two different ways:

  • directly via ICommand.Execute upon a button click event;
相关标签:
3条回答
  • 2020-12-31 04:24

    To elaborate on Noseratio's answer, RoutedCommand implements ICommand explicitly but also has its own Execute and CanExcute methods that take an additional target parameter. When you call RoutedCommand's explicit implementation of ICommand.Execute and ICommand.CanExcute, it will call its own version of these functions passing null as the target. If target is null, it will default to using Keyboard.FocusedElement. If target is still null after that (ie nothing has focus), the main body of the function is skipped and it just returns false. See the RoutedCommand source code on line 146 and 445.

    If you know the command is a RoutedCommand you can get around the focus issue by calling RoutedCommand.Execute(object, IInputElement) instead and provide a target. Here's a relevant extension method I wrote:

    public static void TryExecute(this ICommand command, object parameter, IInputElement target)
    {
        if (command == null) return;
    
        var routed = command as RoutedCommand;
        if (routed != null)
        {
            if (routed.CanExecute(parameter, target))
                routed.Execute(parameter, target);
        }
        else if (command.CanExecute(parameter))
            command.Execute(parameter);
    }
    

    For custom controls, I would typically call it like Command.TryExecute(parameter, this).

    0 讨论(0)
  • 2020-12-31 04:41

    Okay, I'll try to describe the issue, as I understand it. Let's start with a quote from the MSDN section with FAQ (Why are WPF commands not used?):

    Additionally, the command handler that the routed event is delivered to is determined by the current focus in the UI. This works fine if the command handler is at the window level, because the window is always in the focus tree of the currently focused element, so it gets called for command messages. However, it does not work for child views who have their own command handlers unless they have the focus at the time. Finally, only one command handler is ever consulted with routed commands.

    Please pay attention to the line:

    who have their own command handlers unless they have the focus at the time.

    It is clear that when the focus is not, the command will not be executed. Now the question is: what is the documentation mean focus? This refers to the type of focus? I remind there are two types of focus: logical and keyboard focus.

    Now let a quote from here:

    The element within the Windows focus scope that has logical focus will be used as the command target. Note that it's the windows focus scope not the active focus scope. And it's logical focus not keyboard focus. When it comes to command routing FocusScopes remove any item you place them on and it's child elements from the command routing path. So if you create a focus scope in your app and want a command to route in to it you will have to set the command target manually. Or, you can not use FocusScopes other than for toolbars, menus etc and handle the container focus problem manually.

    According to these sources, it is possible to assume that the focus must be active, i.e. an element that can be used with keyboard focus, for example: TextBox.

    To further investigate, I am a little changed your example (XAML section):

    <StackPanel Margin="20,20,20,20">
        <StackPanel.CommandBindings>
            <CommandBinding Command="local:MainWindow.MyCommand" CanExecute="CanExecuteCommmand" Executed="CommandExecuted"/>
        </StackPanel.CommandBindings>
        
        <TextBox Name="textBoxOutput" Focusable="True" IsTabStop="True" Height="150" Text="WPF TextBox&#x0a;"/>
    
        <Menu>
            <MenuItem Header="Sample1" Command="local:MainWindow.MyCommand" />
            <MenuItem Header="Sample2" />
            <MenuItem Header="Sample3" />
        </Menu>
    
        <Button FocusManager.IsFocusScope="True" 
                Name="btnTest" Focusable="False" 
                IsTabStop="False" 
                Content="Test (ICommand.Execute)" 
                Click="btnTest_Click" Width="200"/>
        
        <Button FocusManager.IsFocusScope="True" 
                Content="Test (Command property)"
                Command="local:MainWindow.MyCommand" Width="200"/>
        
        <Button FocusManager.IsFocusScope="True" 
                Name="btnClearFocus" Focusable="False" 
                IsTabStop="False" Content="Clear Focus"
                Click="btnClearFocus_Click" Width="200"
                Margin="138,0,139,0"/>
    </StackPanel>
    

    I added the command in StackPanel and added Menu control. Now, if you click to clear focus, controls associated with the command, will not be available:

    enter image description here

    Now, if we click on the button Test (ICommand.Execute) we see the following:

    enter image description here

    Keyboard focus is set on the Window, but the command still does not run. Once again, remember the note, the above:

    Note that it's the windows focus scope not the active focus scope.

    He does not have an active focus, so the command does not work. It will only work if the focus is active, set to TextBox:

    enter image description here

    Let's go back to your original example.

    Clearly, the first Button does not cause the command, without the active focus. The only difference is that in this case, the second button is not disabled because there is no active focus, so clicking on it, we call the command directly. Perhaps, this is explained by a string of MSDN quotes:

    This works fine if the command handler is at the window level, because the window is always in the focus tree of the currently focused element, so it gets called for command messages.

    I think, I found another source that should explain this strange behavior. Quote from here:

    Menu items or toolbar buttons are by default placed within a separate FocusScope (for the menu or toolbar respectively). If any such items trigger routed commands, and they do not have a command target already set, then WPF always looks for a command target by searching the element that has keyboard focus within the containing window (i.e. the next higher-up focus scope).

    So WPF does NOT simply look up the command bindings of the containing window, as you'd intuitively expect, but rather always looks for a keyboard-focused element to set as the current command target! Apparently the WPF team took the quickest route here to make built-in commands such as Copy/Cut/Paste work with windows that contain multiple text boxes or the like; unfortunately they broke every other command along the way.

    And here's why: if the focused element within the containing window cannot receive keyboard focus (say, it's a non-interactive image), then ALL menu items and toolbar buttons are disabled -- even if they don't require any command target to execute! The CanExecute handler of such commands is simply ignored.

    Apparently the only workaround for problem #2 is to explicitly set the CommandTarget of any such menu items or toolbar buttons to the containing window (or some other control).

    0 讨论(0)
  • 2020-12-31 04:44

    JoeGaggler, a colleague of mine, has apparently found the reason for this behavior:

    I think I found it using reflector: if the command target is null (i.e. keyboard focus is null), then the ICommandSource uses itself (not the window) as the command target, which ultimately hits the CommandBinding for the window (this is why the declarative binding works).

    I'm making this answer a community wiki, so I don't get credits for his research.

    0 讨论(0)
提交回复
热议问题