痞子衡嵌入式:痞子衡嵌入式半月刊 第 1 期
前面章节已经对命令进行了深入分析,分析了基类和接口以及WPF提供的命令库。但尚未例举任何使用这些命令的例子。
如前所述,RoutedUICommand类没有任何硬编码的功能,而是只表达命令,为触发命令,需要有命令源(也可使用代码)。为响应命令,需要有命令绑定,命令绑定将执行转发给普遍的事件处理程序。
一、命令源
命令库中的命令始终可用。触发他们的最简单的方法是将它们关联到实现了ICommandSource接口的控件,其中包括继承自ButtonBase类的控件(Button和CheckBox等)、单独的ListBoxItem对象、HyperLink以及MenuItem。
ICommandSource接口定义了三个属性,如下表所示。
表 ICommandSource接口的属性
例如,下面的按钮使用Command属性连接到ApplicationCommands.New命令:
<Button Command=”ApplicationCommands.New”>New</Button>
WPF的智能程度足够高,它能查找前面介绍的所有5个命令容器类,这意味着可使用下面的缩写的形式:
<Button Command="New">New</Button>
然而,由于没有指明包含命令的类,这种语法不够明确、不够清晰。
二、命令绑定
当将命令关联到命令源时,会看到一些有趣的现象。命令源将会被自动禁用。
例如,如果创建上一节提到的New按钮,该按钮的颜色就会变浅并且不能被单击,就像将IsEnabled属性设置为false那样(如下图所示)。这是因为按钮已经查询了命令的状态,而且由于命令还没有与其关联的绑定,所以按钮被认为是禁用的。
为改变这种状态,需要为命令创建绑定以明确以下三件事:
当命令被触发时执行什么操作。
如何确定命令是否能够被执行(这是可选的。如果未提供这一细节,只要提供了关联的事件处理程序,命令总是可用)。
命令在何处起作用。例如,命令可被限制在单个按钮中使用,或在整个窗口中使用(这种情况更常见)。
下面的代码片段为New命令创建绑定。可将这些代码添加到窗口的构造函数中:
//Create the binding CommandBinding binding=new CommandBinding(ApplicationCommands.New); //Attach the event handler binding.Executed+=NewCommand_Executed; //Register the binding this.CommandBinding.Add(binding);
注意,上面创建的CommandBinding对象呗添加到包含窗口的CommandBindings集合中,这通过事件冒泡进行工作。实际上,当单击按钮时,CommandBinding.Executed事件从按钮冒泡到包含元素。
尽管习惯上为窗口添加所有绑定,但CommandBindings属性实际是在UIElement基类中定义的。这意味着任何元素都支持该属性。例如,如果将命令绑定直接添加到使用它的按钮中,这个示例仍工作的很好(尽管不能在将该绑定重用与其他高级元素)。为得到最大的灵活性,命令绑定通常被添加到顶级窗口。如果希望在多个窗口中使用相同相同的命令,需要在这些窗口中分别创建命令绑定。
上面的代码假定在同一个类中已有名为NewCommand_Executed的事件处理程序,该处理程序已经准备好接收命令。下面是一个示例,该例包含一些显示命令源的简单代码:
private void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e) { MessageBox.Show("New command triggered by " + e.Source.ToString()); }
现在,如果允许应用成功需,按钮处于启用状态。如果单击按钮,就会触发Executed事件,该事件冒泡至窗口,并被上面给出的NewCommand_Executed()事件处理程序程序处理。这是,WPF会告知事件源(按钮)。通过ExecutedRoutedEventArgs对象还可获得被调用的命令的引用(ExecutedRoutedEventArgs.Command),以及所有同时传递的额外数据(ExecutedRoutedEventArgs.Parameter)。在该例中,因为没有传递任何额外的数据,所以参数为null(如果希望传递附加数据,赢设置命令源的CommandParameter属性;并且如果希望传递一些来自另一个控件的信息,还需要使用数据绑定表达式设置CommandParameter属性)。
在上面的示例中,使用代码生成了命令绑定。然而,如果希望精简代码隐藏文件,使用XAML以生命方式关联命令同样容易。下面是所需的标记:
<Window x:Class="Commands.TestNewCommand" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="TestNewCommand" Height="300" Width="300"> <Window.CommandBindings> <CommandBinding Command="ApplicationCommands.New" Executed="NewCommand"></CommandBinding> </Window.CommandBindings> <StackPanel> <Button Margin="5" Padding="5" Command="ApplicationCommands.New">New</Button> </StackPanel> </Window>
使用Visual Studio没有为定义命令绑定提供任何设计时支持。对连接控件和命令的支持也较弱。可使用Properties窗口设置控件的Command属性,但需要输入正确的命令名称——由于并未提供包含命令的下拉列表,因此不能方便地从列表中选择命令。
三、使用多命令源
上面示例中触发普通事件的方式看起来不那么直接。然而,当添加使用相同命令的更多控件时,额外命令层的意义就提现出出来了。例如,可添加如下也使用New命令的菜单项:
<Menu > <MenuItem Header="File"> <MenuItem Command="New"></MenuItem> </MenuItem> </Menu>
注意,New命令的这个MenuItem对象没有设置Header属性。这是因为MenuItem类足够智能,如果没有设置Header属性,它将从命令中提取文本(Button控件不具有这一特性)。虽然该特性带带来的便利看起来不大,但如果计划使用不同的语言本地化应用程序,这一特性就很重要了。在这种情况下,只需要在一个地方修改文本即可(通过设置命令的Text属性)。这比在整个窗口中进行跟踪更容易。
MenuItem类还有一项功能:能自动提取Command.InputBinding集合中的第一个快捷键(如果存在的话)。对于ApplicationCommands.New命令对象,这意味着在菜单文本的旁边会显示Ctrl+N快捷键(如下图所示)。
四、微调命令文本
既然菜单具有自动提取命令项文本的功能,肯恩改回好奇其他ICommandSource类是否也具有类似功能,如Button控件。
可以使用两种技术重用命令文本。一种选择是直接从静态命令对象中提取文本。XAML可使用Static标记扩展完成这一任务。下面的示例获取命令名New,并将它作为按钮的文本:
<Button Margin="5" Padding="5" Command="New" Content="{x:Static ApplicationCommands.New}"></Button>
该方法的问题在于,它指示调用命令对象命令对象的ToString()方法。因此,得到的是命令名,而不是命令的文本(对于哪些名称中包含多个单词的命令,使用命令文本更好些,因为命令文本包含空格)。虽然解决这一问题,但需要完成更多工作。这种方法还存在一个问题,一个按钮将同一个命令使用两次,可能会无意间从错误的命令获取文本)。
TS 原理详细解读(5)语法2-语法解析
更好的解决方案是使用数据绑定表达式。在此使用的数据绑定有些不寻常,因为他绑定到当前元素吗,获取正在使用的Command对象,并提取Text属性。下面是非常复杂的语法:
<Button Margin="5" Padding="5" Command="New" Content="{Binding RelativeSource={RelativeSource Self},Path=Command.Text}"></Button>
可通过另一种更具想象力的方式使用该技术。例如,可使用一幅小图像设置按钮的内容,而在按钮的工具提示中使用数据绑定表达式显示命令名:
<Button Margin="5" Padding="5" Command="ApplicationCommands.New" ToolTip="{Binding RelativeSource={RelativeSource Self},Path=Command.Text}"> <Image Source="redx.jpg" Stretch="None"></Image> </Button>
按钮的内容可以是形状,也可以是显示为缩略图的位图。
显然,这种方法比直接在标记中放置命令文本更麻烦些。然而,如果准备使用不同的语言本地化应用程序,使用这个方法是值得的。当应用程序启动时,只需要为所有命令设置命令文本即可(如果在创建了命令绑定后改变命令文本,不会产生任何效果。因为Text属性不是依赖项属性,所以没有自动的更改通知来更新用户界面)。
五、直接调用命令
并非只能使用实现了ICommandSource接口的类来触发希望执行的命令。也可以用Execute()方法直接调用来自任何事件处理程序的方法。这时需要传递参数值(或null引用)和对目标元素的引用:
ApplicationCommands.New.Execute(null,targetElement);
目标元素是WPF开始查找命令绑定的地方。可使用包含窗口(具有命令绑定)或嵌套的元素(例如,实际引发事件的元素)。
也可在关联的CommandBinding对象中调用Execute()方法。在这种情况下,不需要提供目标元素,因为会自动将公开正在使用的CommandBindings集合的元素设置为目标元素。
this.CommandBindings[0].Command.Execute(null);
这种方法只使用了半个命令模型。虽然也触发命令,但不能响应命令的状态变化。如果希望实现该特性,当命令变为启用或禁用时,也可能希望处理RoutedCommand.CanExecuteChanged事件进行响应。当引发CanExecuteChanged事件时,需要调用RoutedCommand.CanExecute()方法检查命令是否处于可用状态。如果命令不可用。可禁用或改变用户界面中的部分内容。
六、禁用命令
如果想要创建状态在启用和禁用之间变化的命令,你将体会到命令模型的真正优势。例如,分析下图中显示的单窗口应用程序,它是有菜单、工具栏以及大文本框构成的简单文本编辑器。该应用程序可以打开文件,创建新的(空白)文档,以及保存所执行的操作。
在该应用程序中,保持New、Open、Save、SaveAs以及Close命令一直可用是非常合理的。但还有一种设计,只有当某些操作使文本相对于原来的文件发生了变化时才启用Save命令。根据约定,可在代码中使用简单的Boolean值来跟踪这一细节:
private bool isDirty = false;
然后当文本发生变化时设置该标志:
private void txt_TextChanged(object sender, RoutedEventArgs e) { isDirty = true; }
现在需要从窗口命令绑定传递信息,使链接的控件可根据需要进行更新。技巧是处理命令绑定的CanExecute事件。可通过下面的代码为该事件关联事件处理程序:
CommandBinding binding = new CommandBinding(ApplicationCommands.Save); binding.Executed += SaveCommand_Executed; binding.CanExecute += SaveCommand_CanExecute; this.CommandBindings.Add(binding);
或使用声明方式:
<Window.CommandBindings> <CommandBinding Command="ApplicationCommands.Save" Executed="SaveCommand_Executed" CanExecute="SaveCommand_CanExecute"></CommandBinding> </Window.CommandBindings>
在事件处理程序中,只需要检查isDirty变量,并相应地设置CanExecuteRoutedEventArgs.CanExecute属性:
private void SaveCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = isDirty;
}
如果isDirty的值时false,就禁用命令。如果isDirty的值为true,就启用命令(如果没有设置CanExecute标志,就会保持最近的值)。
当使用CanExecute事件时,还需要理解一个问题,由WPF负责调用RoutedCommand.CanExecute()方法来触发事件处理程序,并确定命令的状态。当WPF命令管理器探测到某个确信十分重要的变化——例如,当焦点从一个控件移到另一个控件上时,或执行了某个命令后,WPF命令管理器就会完成该工作。控件还能引发CanExecuteChanged事件以通知WPF重新评估命令——例如,当用户在文本框中按下一个键时会发生该事件。总之,CanExecute事件会被频繁地触发,并且不应在该事件的处理程序中使用耗时的代码。
然而,其他因素可能影响命令状态。在当前示例中,为响应其他操作,可能会修改isDirty标志。如果发现命令状态未在正确的时间被更新,可强制WPF为所有正在使用的命令调用CanExecute()方法。通过调用静态方法CommandManager.InvalidateRequerySuggested()完成该工作。然后命令管理器触发RequerySuggested事件,通知窗口中的命令源(按钮、菜单项等)。此后命令源会重新查询它们链接的命令并相应地更新它们的状态。
七、具有内置命令的控件
一个输入控件可自行处理命令事件。例如,TextBox类处理Cut、Copy以及Paste命令(还有Undo、Redo命令,以及一些来自EditingCommd类的用于选择文本以及将光标移到不同位置的命令)。
当控件具有自己的硬编码命令逻辑时,为使命令工作不需要做其他任何事情。例如,对于上节示例的简单编辑器,添加如下工具栏按钮,就会自动获取对剪切、复制和粘贴文本的支持:
<ToolBar> <Button Command="Cut">Cut</Button> <Button Command="Copy">Copy</Button> <Button Command="Paste">Paste</Button> </ToolBar>
现在淡季这些按钮中的任意一个(当文本框具有焦点时),就可以复制、剪切或从剪贴板粘贴文本。有趣的是,文本框还处理CanExecute事件。如果当前未在文本框中选中任何内容,就会禁用剪切和复制命令。当焦点移到其他不支持这些命令的控件时,会自动禁用所有这三个命令(除非关联自己的CanExecute事件处理程序以启动这些命令)。
该例有一个有趣的细节。Cut、Copy和Paste命令被具有焦点的文本框处理。然而,由工具栏上的按钮触发的命令时完全独立的元素。在该例中,这个过程之所以能够无缝工作,是因为按钮被放到工具栏上,ToolBar类提供了一些内置逻辑,可将其子元素的CommandTarget属性动态设置为当前具有焦点的控件(从技术角度看,ToolBar控件一直在关注着其父元素,即窗口,并在上下文中查找最近具有焦点的控件,即文本框。ToolBar控件有单独的焦点范围(focus scope),并且在其上下文中按钮是具有焦点的)。
如果在不同容器(不是ToolBar或Menu控件)中放置按钮,就不会获得这些优势。这意味着除非手动设置CommanTarget属性,否则按钮不能工作。为此,必须使用命令目标元素的绑定的表达式。例如,如果文本框被命名为txtDocument,就应该像下面这样定义按钮:
<Button Command="Cut" CommandTarget="{Binding ElementName=txtDocument}">Cut</Button> <Button Command="Copy" CommandTarget="{Binding ElementName=txtDocument}">Copy</Button> <Button Command="Paste" CommandTarget="{Binding ElementName=txtDocument}">Paste</Button>
另一个较简单的选择是使用附加属性FocusManager.IsFocusScope创建新的焦点范围。当触发命令时,该焦点范围会通知WPF在父元素的焦点范围内查找元素:
<StackPanel FocusManager.IsFocusScope="True"> <Button Command="Cut">Cut</Button> <Button Command="Copy">Copy</Button> <Button Command="Paste">Paste</Button> </StackPanel>
该方法还有一个附加优点,即相同的命令可应用于多个控件,不像上个示例那样对CommandTarget进行硬编码。此外,Menu和ToolBar控件默认将FocusManager.IsFocusScope属性设置为true,但如果希望简化命令路由行为,不在父元素上下文中查找具有焦点的元素,也可将该属性设为false。
在极少数情况下,你可能发现控件支持内置命令,而你并不想启用它。在这种情况下,可以采用三种方法禁用命令。
理想情况下,控件提供用于关闭命令支持的属性,从而确保控件移除这些特性并连贯地调整自身。例如,TextBox控件提供了IsUndoEnabled属性,为阻止Undo特性,可将该属性设置为false(如果IsUndoEnabled属性为true,Ctrl+Z组合键将触发Undo命令)。
如果这种做法行不通,可为希望禁用的命令添加新的命令绑定。然后该命令绑定可提供新的CanExecute事件处理程序,并总是响应false。下面举一个使用该技术删除文本框Cut特性支持的示例:
CommandBinding commandBinding=new CommandBinding(ApplicationCommands.Cut,null,SuppressCommand); txt.CommandBindings.Add(commandBinding);
而且该事件处理程序设置CanExecute状态:
private void SupressCommand(object sender,CanExecuteRoutedEventArgs e) { e.CanExecute=false; e.Handled=false; }
注意,上面的代码设置了Handled标志以阻止文本框自我执行计算,而文本框可能将CanExecute属性设置为true。
该方法并不完美。它可成功地为文本框禁用Cut快捷键(Ctrl+X)和上下文菜单中的Cut命令。然而,仍会在上下文菜单中显示处理禁用状态的该选项。
最后一种选择是,使用InputBinding集合删除触发命令的输入。例如,可使用带阿妈禁用触发TextBox控件中的Copy命令的Ctrl+C组合键,如下所示:
KeyBinding keyBinding=new KeyBinding(ApplicationCommands.NotACommand,Key.C,ModifierKeys.Control); txt.InputBinding.Add(keyBinding);
技巧是使用特定的ApplicationCommands.NotACommand值,该命令什么都不做,它专门用于禁用输入绑定。
当使用这种方法时,仍启用Copy命令。可通过自己创建的按钮触发该命令(或使用文本框的上下文菜单触发命令,除非也通过将ContextMenu属性设置为null删除了上下文菜单)。
我也写了个疫情传播仿真程序