WPF命令路由行为的不一致性取决于UI焦点状态

我有一个RoutedUICommand命令,可以通过两种不同的方式触发:

  • 直接通过ICommand.Execute点击按钮事件;
  • 使用声明性语法:

该命令仅由顶部窗口处理:

    

第一种方法仅在窗口中存在聚焦元素时才有效。 无论焦点如何,第二个总是这样。

我查看了BCL的ICommand.Execute实现,发现如果Keyboard.FocusedElementnull ,命令不会被触发,所以这是设计的。 我仍然质疑,因为顶层可能有一个处理程序(就像在我的情况下)仍然希望接收命令,即使应用程序没有UI焦点(例如,我可能想调用ICommand.Execute当收到套接字消息时,从异步任务中执行)。 让它成为现实,我仍然不清楚为什么第二种(声明性)方法总是有效,无论焦点状态如何。

我对WPF命令路由的理解中缺少什么? 我敢肯定这不是一个错误,而是一个function。

以下是代码。 如果你喜欢玩它,这是完整的项目 。 单击第一个按钮 – 命令将被执行,因为焦点位于TextBox内部。 点击第二个按钮 – 一切都很好。 单击“ Clear Focus按钮。 现在第一个按钮( ICommand.Execute )不执行命令,而第二个按钮仍然执行。 您需要单击TextBox以使第一个按钮再次工作,因此有一个聚焦元素。

这是一个人为的例子,但它具有现实生活中的意义。 我将发布一个关于使用WindowsFormsHost托管WinForms控件的相关问题( 此处询问 [EDITED] ),在这种情况下,当焦点位于WindowsFormsHost内时, Keyboard.FocusedElement始终为null (通过ICommand.Execute有效地终止命令执行)。

XAML代码:

       

C#代码 ,大部分与焦点状态记录有关:

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

[更新]让我们稍微改变一下代码:

 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被第二个按钮触发,到达顶部窗口的处理程序并被执行(我认为是正确的行为):

在此处输入图像描述

好的,我会尝试描述这个问题,正如我所理解的那样。 让我们从带有FAQ的MSDN部分的引用开始( Why are WPF commands not used? ):

此外,路由事件传递到的命令处理程序由UI中的当前焦点确定。 如果命令处理程序位于窗口级别,这可以正常工作,因为窗口始终位于当前聚焦元素的焦点树中,因此会调用命令消息。 但是,它对于拥有自己的命令处理程序的子视图不起作用,除非它们当时具有焦点。 最后,只有一个命令处理程序可以查询路由命令。

请注意行:

谁有自己的命令处理程序,除非他们当时有焦点。

很明显,当焦点不是时,命令将不会被执行。 现在的问题是:文档的重点是什么? 这指的是焦点类型? 我提醒有两种类型的焦点: 逻辑键盘焦点。

现在让我们从这里引用:

Windows焦点范围内具有逻辑焦点的元素将用作命令目标。 Note ,它是Windows焦点范围而不是活动焦点范围。 这是合乎逻辑的焦点而不是键盘焦点。 在命令路由方面,FocusScopes会删除放置它们的任何项目,并从命令路由路径中删除它们的子元素。 因此,如果您在应用程序中创建焦点范围并希望命令路由到它,则必须手动设置命令目标。 或者,您不能使用除工具栏,菜单等之外的FocusScopes并手动处理容器焦点问题。

根据这些来源,可以假设焦点必须是活动的,即可以与键盘焦点一起使用的元素,例如: TextBox

为了进一步调查,我稍微改变了你的例子(XAML部分):

               

我在StackPanel添加了命令并添加了Menu控件。 现在,如果单击以清除焦点,则与命令关联的控件将不可用:

在此处输入图像描述

现在,如果我们单击按钮Test (ICommand.Execute)我们会看到以下内容:

在此处输入图像描述

键盘焦点在Window上设置,但命令仍然无法运行。 再一次,记住上面的注释:

请注意,它是Windows焦点范围而不是活动焦点范围。

他没有主动焦点,所以命令不起作用。 它只有在焦点处于活动状态时才有效,设置为TextBox

在此处输入图像描述

让我们回到你原来的例子。

显然,没有活动焦点,第一个Button不会导致命令。 唯一的区别是,在这种情况下,第二个按钮没有被禁用,因为没有活动焦点,所以点击它,我们直接调用命令。 也许,这可以通过一串MSDN引用来解释:

如果命令处理程序位于窗口级别,这可以正常工作,因为窗口始终位于当前聚焦元素的焦点树中,因此会调用命令消息。

我想,我找到了另一个可以解释这种奇怪行为的来源。 从这里引用:

菜单项或工具栏按钮默认放在单独的FocusScope中(分别用于菜单或工具栏)。 如果任何此类项触发路由命令,并且它们没有已设置的命令目标,则WPF始终通过在包含窗口(即下一个更高的焦点范围)内搜索具有键盘焦点的元素来查找命令目标。

因此,WPF并不是简单地查找包含窗口的命令绑定,正如您所期望的那样,而是始终寻找一个以键盘为中心的元素来设置为当前命令目标! 显然,WPF团队采取了最快的路线来制作内置命令,例如复制/剪切/粘贴工作,窗口包含多个文本框等; 不幸的是,他们在途中打破了所有其他命令。

这就是原因:如果包含窗口中的聚焦元素无法接收键盘焦点(例如,它是非交互式图像),则禁用所有菜单项和工具栏按钮 – 即使它们不需要执行任何命令目标! 简单地忽略这些命令的CanExecute处理程序。

显然,问题#2的唯一解决方法是将任何此类菜单项或工具栏按钮的CommandTarget显式设置为包含窗口(或某些其他控件)。

我的同事JoeGaggler显然发现了这种行为的原因:

我想我发现它使用了reflection器:如果命令目标为null(即键盘焦点为空),则ICommandSource使用自身(而不是窗口)作为命令目标,最终命中窗口的CommandBinding(这就是为什么声明性绑定工作)。

我将这个答案作为一个社区维基,所以我没有得到他的研究成绩。