nos*_*tio 17 .net c# wpf xaml focus
我有一个RoutedUICommand命令,可以通过两种不同的方式触发:
ICommand.Execute按钮点击事件;<button Command="local:MainWindow.MyCommand" .../>.该命令仅由顶部窗口处理:
<Window.CommandBindings>
<CommandBinding Command="local:MainWindow.MyCommand" CanExecute="CanExecuteCommmand" Executed="CommandExecuted"/>
</Window.CommandBindings>
Run Code Online (Sandbox Code Playgroud)
第一种方法仅在窗口中存在聚焦元素时才有效.无论焦点如何,第二个总是这样.
我看着BCL的ICommand.Execute实施,发现该命令不被解雇,如果Keyboard.FocusedElement是null,那么这是由设计.我仍然质疑,因为顶层可能有一个处理程序(就像在我的情况下)仍然希望接收命令,即使应用程序没有UI焦点(例如,我可能想要ICommand.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>
Run Code Online (Sandbox Code Playgroud)
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));
}
}
}
Run Code Online (Sandbox Code Playgroud)
[更新]让我们稍微改变一下代码:
void btnClearFocus_Click(object sender, RoutedEventArgs e)
{
//FocusManager.SetFocusedElement(this, this);
FocusManager.SetFocusedElement(this, null);
Keyboard.ClearFocus();
CommandManager.InvalidateRequerySuggested();
Log("btnClearFocus_Click, {0}", FormatFocus());
}
Run Code Online (Sandbox Code Playgroud)
现在我们有另一个有趣的案例:没有逻辑焦点,没有键盘焦点,但命令stil被第二个按钮触发,到达顶部窗口的处理程序并被执行(我认为是正确的行为):

Ana*_*aev 13
好的,我会尝试描述这个问题,正如我所理解的那样.让我们从MSDNFAQ(Why are WPF commands not used?)部分的引用开始:
此外,路由事件传递到的命令处理程序由UI中的当前焦点确定.如果命令处理程序在窗口级别,这可以正常工作,因为窗口始终位于当前聚焦元素的焦点树中,因此它将被调用命令消息.但是,它对于拥有自己的命令处理程序的子视图不起作用,除非它们当时具有焦点.最后,只有一个命令处理程序可以查询路由命令.
请注意一下:
谁拥有自己的命令处理程序,除非他们当时关注.
很明显,当焦点不是时,命令将不会被执行.现在的问题是:文档的重点是什么?这是指焦点的类型?我提醒有两种类型的焦点:逻辑和键盘焦点.
现在让我们从这里引用:
Windows焦点范围内具有逻辑焦点的元素将用作命令目标.
Note这是Windows焦点范围而非活动焦点范围.这是合乎逻辑的焦点而非键盘焦点.在命令路由方面,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
"/>
<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>
Run Code Online (Sandbox Code Playgroud)
我添加了命令StackPanel并添加了Menu控件.现在,如果单击以清除焦点,则与命令关联的控件将不可用:

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

键盘焦点设置在Window,但命令仍然不运行.再一次,记住上面的注释:
请注意,它是Windows焦点范围而不是活动焦点范围.
他没有主动焦点,所以命令不起作用.它仅在焦点处于活动状态时有效,设置为TextBox:

让我们回到你原来的例子.
显然,Button没有主动聚焦,第一个不会导致命令.唯一的区别是,在这种情况下,第二个按钮没有被禁用,因为没有活动焦点,所以点击它,我们直接调用命令.也许,这可以用一串MSDN引号来解释:
如果命令处理程序在窗口级别,这可以正常工作,因为窗口始终位于当前聚焦元素的焦点树中,因此它将被调用命令消息.
我想,我找到了另一个可以解释这种奇怪行为的来源.从这里引用:
菜单项或工具栏按钮默认放在单独的FocusScope中(分别用于菜单或工具栏).如果任何此类项目触发路由命令,并且它们没有已设置的命令目标,则WPF始终通过在包含窗口(即下一个更高的焦点范围)内搜索具有键盘焦点的元素来查找命令目标.
因此,WPF并不是简单地查找包含窗口的命令绑定,正如您直觉所期望的那样,而是始终寻找一个以键盘为中心的元素来设置为当前命令目标!显然,WPF团队采取了最快的路线来制作内置命令,例如复制/剪切/粘贴工作,窗口包含多个文本框等; 不幸的是,他们在途中打破了所有其他命令.
这就是原因:如果包含窗口中的聚焦元素无法接收键盘焦点(例如,它是非交互式图像),则禁用所有菜单项和工具栏按钮 - 即使它们不需要执行任何命令目标!简单地忽略这些命令的CanExecute处理程序.
显然,问题#2的唯一解决方法是将任何此类菜单项或工具栏按钮的CommandTarget显式设置为包含窗口(或某些其他控件).
小智 5
为了详细说明 Noseratio 的答案,显式RoutedCommand实现ICommand,但也有自己的方法Execute和CanExcute带有附加target参数的方法。当您调用andRoutedCommand的显式实现时,它将调用这些函数的自己版本,并传递 null 作为。如果为 null,则默认使用. 如果之后仍然为 null(即没有焦点),则跳过该函数的主体并仅返回 false。请参阅RoutedCommand 源代码第 146 行和第 445 行。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);
}
Run Code Online (Sandbox Code Playgroud)
对于自定义控件,我通常将其称为Command.TryExecute(parameter, this).
我的同事JoeGaggler显然已经找到了这种行为的原因:
我想我发现它使用反射器:如果命令目标为空(即键盘焦点为空),则 ICommandSource使用自身(而不是窗口)作为命令目标,最终命中窗口的 CommandBinding (这就是为什么声明性绑定有效)。
我正在将这个答案作为社区维基,所以我不会因为他的研究而获得学分。
| 归档时间: |
|
| 查看次数: |
3978 次 |
| 最近记录: |