21

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

  • 直接通过ICommand.Execute按钮点击事件;
  • 使用声明性语法:<button Command="local:MainWindow.MyCommand" .../>.

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

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

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

我查看了 BCL 的实现,发现如果isICommand.Execute不会触发该命令,所以这是设计使然。我仍然会对此提出疑问,因为即使应用程序没有 UI 焦点(例如,我可能想从收到套接字消息时的异步任务)。就这样吧,我仍然不清楚为什么第二种(声明式)方法总是有效,而不管焦点状态如何。Keyboard.FocusedElementnullICommand.Execute

我对 WPF 命令路由的理解缺少什么?我确信这“不是错误,而是功能”。

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

这是一个人为的例子,但它具有现实生活的影响。我将发布一个有关托管 WinForms 控件的相关问题WindowsFormsHost[EDITED] 在这里询问),在这种情况下Keyboard.FocusedElement,总是null当焦点在内部时WindowsFormsHost(通过有效地杀死命令执行ICommand.Execute)。

XAML 代码:

<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# 代码,大部分与焦点状态记录有关:

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

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

在此处输入图像描述

4

3 回答 3

13

好的,我将尝试描述这个问题,据我了解。MSDN让我们从FAQ ( ) 部分的引用开始Why are WPF commands not used?

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

请注意以下线路:

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

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

现在从这里引用:

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

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

为了进一步调查,我稍微更改了您的示例(XAML 部分):

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

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

在此处输入图像描述

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

在此处输入图像描述

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

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

他没有活跃的焦点,所以命令不起作用。仅当焦点处于活动状态时才有效,设置为TextBox

在此处输入图像描述

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

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

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

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

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

因此,WPF 不会像您直观地期望的那样简单地查找包含窗口的命令绑定,而是始终查找以键盘为中心的元素以设置为当前命令目标!显然,WPF 团队在这里采取了最快的方法来使复制/剪切/粘贴等内置命令与包含多个文本框等的窗口一起工作;不幸的是,他们一路上破坏了所有其他命令。

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

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

于 2013-08-26T12:14:52.757 回答
4

为了详细说明 Noseratio 的答案,明确地RoutedCommand实现,ICommand但也有自己的Execute方法和CanExcute带有附加target参数的方法。当您调用andRoutedCommand的显式实现时,它将调用这些函数的自己的版本,并将 null 作为. 如果为 null,则默认使用. 如果在那之后仍然为null(即没有焦点),则跳过函数的主体并返回false。请参阅第 146 和 445 行的RoutedCommand 源代码ICommand.ExecuteICommand.CanExcutetargettargetKeyboard.FocusedElementtarget

如果您知道该命令是 RoutedCommand,则可以通过调用RoutedCommand.Execute(object, IInputElement)并提供目标来解决焦点问题。这是我写的一个相关的扩展方法:

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

对于自定义控件,我通常将其称为Command.TryExecute(parameter, this).

于 2019-09-07T13:51:18.470 回答
3

我的同事JoeGaggler显然找到了这种行为的原因:

我想我使用反射器找到了它:如果命令目标为空(即键盘焦点为空),则ICommandSource使用自身(而不是窗口)作为命令目标,最终命中窗口的 CommandBinding(这就是为什么声明式绑定有效)。

我正在将此答案设为社区 wiki,因此我没有因他的研究而获得学分。

于 2013-08-27T04:16:16.643 回答