C# WPF命令路由行为中的不一致性取决于UI焦点状态
我有一个C# WPF命令路由行为中的不一致性取决于UI焦点状态,c#,.net,wpf,xaml,focus,C#,.net,Wpf,Xaml,Focus,我有一个RoutedUICommand命令,它可以通过两种不同的方式启动: 直接通过ICommand。在按钮单击事件中执行 使用声明性语法: 该命令仅由顶部窗口处理: <Window.CommandBindings> <CommandBinding Command="local:MainWindow.MyCommand" CanExecute="CanExecuteCommmand" Executed="CommandExecuted"/> </Wind
RoutedUICommand
命令,它可以通过两种不同的方式启动:
- 直接通过
ICommand。在按钮单击事件中执行
李>
- 使用声明性语法:
- 使用声明性语法:
<Window.CommandBindings>
<CommandBinding Command="local:MainWindow.MyCommand" CanExecute="CanExecuteCommmand" Executed="CommandExecuted"/>
</Window.CommandBindings>
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部分的一句话开始(
为什么不使用WPF命令?
):
此外,路由事件传递到的命令处理程序由UI中的当前焦点确定。如果命令处理程序位于窗口级别,则这种方法可以正常工作,因为窗口始终位于当前焦点元素的焦点树中,因此它会被调用以获取命令消息。但是,它不适用于具有自己的命令处理程序的子视图,除非它们当时具有焦点。最后,只有一个命令处理程序会被路由命令查询
请注意线路:
他们有自己的命令处理程序,除非他们当时有焦点
很明显,当焦点不在时,该命令将不会执行。现在的问题是:文档是什么意思?这是指焦点的类型?我提醒大家有两种焦点:逻辑焦点和键盘焦点
现在让我们引用一下:
Windows焦点范围内具有逻辑焦点的元素将用作命令目标。
注意
这是windows焦点作用域,而不是活动焦点作用域。这是逻辑焦点,而不是键盘焦点。当涉及到命令路由聚焦镜时,请从命令路由路径中删除放置它们的任何项及其子元素。因此,如果您在应用程序中创建一个焦点作用域,并希望命令路由到该作用域,则必须手动设置命令目标。或者,除了工具栏、菜单等,您不能使用聚焦镜,也不能手动处理容器聚焦问题
根据这些来源,可以假设焦点必须处于活动状态,即可以与键盘焦点一起使用的元素,例如:TextBox
为了进一步研究,我对您的示例(XAML部分)做了一些更改:
我在StackPanel
中添加了命令,并添加了菜单
控件。现在,如果单击以清除焦点,则与该命令关联的控件将不可用:
现在,如果我们单击按钮Test(ICommand.Execute)
我们会看到以下内容:
键盘焦点设置在窗口上,但命令仍不运行。再一次,记住上面的注释:
请注意,它是windows焦点作用域,而不是活动焦点作用域
他没有活动焦点,因此命令不起作用。只有当焦点处于活动状态,设置为文本框时,它才会工作:
让我们回到您最初的示例
显然,第一个按钮
在没有活动焦点的情况下不会导致命令。唯一的区别是,在这种情况下,第二个按钮没有被禁用,因为没有活动焦点,所以单击它,我们直接调用命令。也许,这可以用一系列MSDN
引号来解释:
如果命令处理程序位于窗口级别,则这种方法可以正常工作,因为窗口始终位于当前焦点元素的焦点树中,因此它会被调用以获取命令消息
我想,我找到了另一个可以解释这种奇怪行为的来源。引自:
默认情况下,菜单项或工具栏按钮放置在单独的聚焦范围内(分别用于菜单或工具栏)。如果任何此类项触发路由命令,并且它们没有设置命令目标,则WPF始终通过搜索包含窗口中具有键盘焦点的元素(即下一个更高的焦点范围)来查找命令目标
因此,WPF并不像您直观地期望的那样,只是查找包含窗口的命令绑定,而是始终查找以键盘为中心的元素以设置为当前命令目标!显然,WPF团队在这里采取了最快的方法,使诸如复制/剪切/粘贴之类的内置命令与包含多个文本框等的窗口一起工作;不幸的是,他们破坏了沿途的所有其他命令
原因如下:如果包含窗口中的聚焦元素无法接收键盘焦点(例如,它是非交互式图像),那么所有菜单项和工具栏按钮都将被禁用——即使它们不需要任何命令目标来执行!这类命令的CanExecute处理程序将被忽略
显然,解决问题#2的唯一方法是将任何此类菜单项或工具栏按钮的CommandTarget显式设置为包含窗口(或其他控件)
,我的一位同事,显然已经找到了这种行为的原因:
我想我是通过reflector找到它的:如果命令目标为null(即键盘焦点为null),则使用自身(而不是窗口)作为命令目标,最终命中窗口的CommandBinding(这就是声明性绑定工作的原因)
我把这个答案做成了一个社区维基,所以我没有因为他的研究而获得学分。为了详细说明Nosertio的答案,RoutedCommand
明确地实现了ICommand
,但也有自己的Ex
void btnClearFocus_Click(object sender, RoutedEventArgs e)
{
//FocusManager.SetFocusedElement(this, this);
FocusManager.SetFocusedElement(this, null);
Keyboard.ClearFocus();
CommandManager.InvalidateRequerySuggested();
Log("btnClearFocus_Click, {0}", FormatFocus());
}
<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
"/>
<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>
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);
}