Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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

  • directly via ICommand.Execute upon a button click event;
  • using declarative syntax: <button Command="local:MainWindow.MyCommand" .../>.

The command is handled only by the top window:

<Window.CommandBindings>
    <CommandBinding Command="local:MainWindow.MyCommand" CanExecute="CanExecuteCommmand" Executed="CommandExecuted"/>
</Window.CommandBindings>

The first approach only works if there is a focused element in the window. The second one always does, regardless of the focus.

I've looked into BCL's ICommand.Execute implementation and found that the command doesn't get fired if Keyboard.FocusedElement is null, so this is by design. I'd still question that, because there might be a handler on the top level (like in my case) which still wants to receive commands, even if the app doesn't have a UI focus (e.g., I may want to call ICommand.Execute from an async task when it has received a socket message). Let it be so, it is still unclear to me why the second (declarative) approach always works regardless of the focus state.

What am I missing in my understanding of the WPF command routing? I'm sure this is 'not a bug but a feature'.

Below is the code. If you like to play with it, here's the full project. Click the first button - the command will get executed, because the focus is inside the TextBox. Click the second button - all is fine. Click the Clear Focus button. Now the first button (ICommand.Execute) doesn't execute the command, while the second one still does. You'd need to click into the TextBox to make the first button work again, so there's a focused element.

This is an artificial example, but it has real-life imlications. I'm going to post a related question about hosting WinForms controls with WindowsFormsHost ([EDITED] asked here), in which case Keyboard.FocusedElement is always null when the focus is inside WindowsFormsHost (effectively killing command execution via ICommand.Execute).

XAML code:

<Window x:Class="WpfCommandTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfCommandTest" 
        Title="MainWindow" Height="480" Width="640" Background="Gray">

    <Window.CommandBindings>
        <CommandBinding Command="local:MainWindow.MyCommand" CanExecute="CanExecuteCommmand" Executed="CommandExecuted"/>
    </Window.CommandBindings>

    <StackPanel Margin="20,20,20,20">
        <TextBox Name="textBoxOutput" Focusable="True" IsTabStop="True" Height="300"/>

        <Button FocusManager.IsFocusScope="True" Name="btnTest" Focusable="False" IsTabStop="False" Content="Test (ICommand.Execute)" Click="btnTest_Click" Width="200"/>
        <Button FocusManager.IsFocusScope="True" Focusable="False" IsTabStop="False" 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>

</Window>

C# code, most of it is related to the focus state logging:

using System;
using System.Windows;
using System.Windows.Input;

namespace WpfCommandTest
{
    public partial class MainWindow : Window
    {
        public static readonly RoutedUICommand MyCommand = new RoutedUICommand("MyCommand", "MyCommand", typeof(MainWindow));
        const string Null = "null";

        public MainWindow()
        {
            InitializeComponent();
            this.Loaded += (s, e) => textBoxOutput.Focus(); // set focus on the TextBox
        }

        void CanExecuteCommmand(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = true;
        }

        void CommandExecuted(object sender, ExecutedRoutedEventArgs e)
        {
            var routedCommand = e.Command as RoutedCommand;
            var commandName = routedCommand != null ? routedCommand.Name : Null;
            Log("*** Executed: {0} ***, {1}", commandName, FormatFocus());
        }

        void btnTest_Click(object sender, RoutedEventArgs e)
        {
            Log("btnTest_Click, {0}", FormatFocus());
            ICommand command = MyCommand;
            if (command.CanExecute(null))
                command.Execute(null);
        }

        void btnClearFocus_Click(object sender, RoutedEventArgs e)
        {
            FocusManager.SetFocusedElement(this, this);
            Keyboard.ClearFocus();
            Log("btnClearFocus_Click, {0}", FormatFocus());
        }

        void Log(string format, params object[] args)
        {
            textBoxOutput.AppendText(String.Format(format, args) + Environment.NewLine);
            textBoxOutput.CaretIndex = textBoxOutput.Text.Length;
            textBoxOutput.ScrollToEnd();
        }

        string FormatType(object obj)
        {
            return obj != null ? obj.GetType().Name : Null;
        }

        string FormatFocus()
        {
            return String.Format("focus: {0}, keyboard focus: {1}",
                FormatType(FocusManager.GetFocusedElement(this)),
                FormatType(Keyboard.FocusedElement));
        }
    }
}

[UPDATE] Let's change the code slightly:

void btnClearFocus_Click(object sender, RoutedEventArgs e)
{
    //FocusManager.SetFocusedElement(this, this);
    FocusManager.SetFocusedElement(this, null);
    Keyboard.ClearFocus();
    CommandManager.InvalidateRequerySuggested();
    Log("btnClearFocus_Click, {0}", FormatFocus());
}

Now we have another interesting case: no logical focus, no keyboard focus, but the command stil gets fired by the second button, reaches the top window's handler and gets executed (which I consider the correct behavior):

enter image description here

like image 647
noseratio Avatar asked Aug 24 '13 10:08

noseratio


2 Answers

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).

like image 116
Anatoliy Nikolaev Avatar answered Oct 12 '22 13:10

Anatoliy Nikolaev


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).

like image 40
user12034691 Avatar answered Oct 12 '22 13:10

user12034691