不一致的WPF命令路由行为取决于用户界面焦点状态路由、用户界面、命令、状态

2023-09-03 02:28:08 作者:只偏袒你

我有一个 RoutedUICommand 命令,它可以以两种不同的方式被解雇:

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

直接通过 ICommand.Execute 在一个按钮单击事件; 使用声明的语法:&LT;按钮命令=本地:MainWindow.MyCommand。... /&GT; 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>

第一种方法只有当出现在窗口中的焦点元素。第二个总是做的重点,不管。

我看着BCL的 ICommand.Execute 的实施,发现该命令没有得到,如果 Keyboard.FocusedElement 是,所以这是由设计。我仍然质疑,因为有可能会在顶层的处理程序(像我的情况),它仍然要接受命令,即使该应用程序不具有一个用户界面的焦点(例如,我可能需要调用 ICommand.Execute 从异步任务时,已收到一个套接字消息)。让它如此,它仍然是我不清楚为什么第二(声明)的做法始终工作的重点国家,不管。

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.

什么是我在我的WPF的命令路由的理解缺失?我敢肯定,这是'不是一个错误,但是功能。

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

下面是code。如果你喜欢用它玩,这里的整个项目。点击第一个按钮 - 该命令将被执行,因为重点是文本框里面。单击第二个按钮 - 一切都很好。点击重点明确按钮。现在,第一个按钮( ICommand.Execute )不执行命令,而第二个仍然如此。你需要点击进入文本框后,第一个按钮的工作了,所以有一个集中的元素。

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.

这是一个人为的例子,但它有真实生活imlications。我要发布一个相关的问题对主办WinForms控件与的WindowsFormsHost ( asked这里),在这种情况下 Keyboard.FocusedElement 总是时,重点是内部的WindowsFormsHost (通过有效查杀命令执行 ICommand.Execute )。

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 ( 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 ,其中大部分是关系到国家重点记录:

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));
        }
    }
}

[更新] 让我们改变了code稍微:

[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());
}

现在我们有另一个有趣的案例:没有逻辑焦点,没有键盘焦点,但STIL得到的第二个按钮触发的命令,到达顶部窗口的处理程序,并得到执行(这是我认为正确的行为):

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

推荐答案

JoeGaggler ,我的一个同事,显然已经找到了原因,此行为:

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

我想我找到了使用反射:如果该命令的目标为空(即键盘焦点为空),那么的 ICommandSource 的使用本身(而不是窗)作为命令目标,这最终击中的CommandBinding为窗口(这就是为什么声明性结合作品)。的

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.