说到用户输入,可能我们更多地会联想到键盘、鼠标、手写笔,其实还用一种高级别的输入 —— 命令( Commands ),从 WPF 类库角度讲他们分别对于 Keyboard , Mouse , Ink 与 ICommand 。命令是一种语义级别的输入而不是设备级别的,比如 “ 复制 ” 与 “ 粘贴 ” ,但实现一个命令可以有很多中方式,比如 “ 粘 贴 ” ,我们可以使用 CTRL-V ,也可以使用主菜单或右键菜单(上下文菜单)等等。在以往的 .net 版本中,要在软件界面上添加一个 “ 粘贴 ” 按钮,是非常 麻烦的事情,你得监视剪切板中是否有可用的文本以及对应的文本框是否获得了焦点以便启用或禁用该按钮,当粘贴时你还得从剪切板中取得相应的文本并插入到文 本框的合理位置,等等。
在 WPF 中提供的命令机制能非常简单地实现这些任务,下面的 Demo 演示了如何简单到不用手动编写一行后台逻辑代码便解决上面的难题的,你可以粘贴下面的代码到 XamlPad:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="Window" Title="Window1" Width="640" Height="480"> <DockPanel LastChildFill="True"> <Menu Width="Auto" Height="20" DockPanel.Dock="Top"> <MenuItem Command="ApplicationCommands.Copy" Header="{Binding Path=Command.Text, RelativeSource={RelativeSource Self}}"/> <MenuItem Command="ApplicationCommands.Paste" Header="{Binding Path=Command.Text, RelativeSource={RelativeSource Self}}"/> <MenuItem Command="ApplicationCommands.Cut" Header="{Binding Path=Command.Text, RelativeSource={RelativeSource Self}}"/> <MenuItem Command="ApplicationCommands.Redo" Header="{Binding Path=Command.Text, RelativeSource={RelativeSource Self}}"/> <MenuItem Command="ApplicationCommands.Undo" Header="{Binding Path=Command.Text, RelativeSource={RelativeSource Self}}"/> </Menu> <RichTextBox> <FlowDocument> <Paragraph/> </FlowDocument> </RichTextBox> </DockPanel> </Window>
Demo 中菜单栏的菜单项不仅仅能完美地完成任务而且能根据文本框的状态和剪切板自动的启用与禁用,而我们却没有为这些复杂的逻辑编写任何的后台代码。这就是 WPF 中的命令机制为我们提供了方便。
<MenuItem Command="ApplicationCommands.Copy" Header="{Binding Path=Command.Text, RelativeSource={RelativeSource Self}}"/>
我们将 “ 复制 ” 命令( ApplicationCommands.Copy )赋值给了菜单项的 Command 属性,实现了 ICommandSource 接口 的元素都拥有该属性,这表示该元素可以作为一个 “ 命令源 ” 来引发某个命令,其 Command 属性就指示了其将引发的命令。
其实一个命令系统是被分为四个部分的:
Command( 命令):一个语义级别的输入,比如 “ 复制 ” , “ 左对齐 ” , “ 播放 ” 等
CommandSource (命令源):引发某命令的元素,比如按钮,菜单项,键盘( Ctrl-C , F1 等),鼠标等。
CommandTarget (命令目标):命令被作用的目标,比如文本框,播放器等。
CommandBinding (命令绑定):用于将命令和命令的处理逻辑链接起来,比如同样的 " 粘贴 " ,但粘贴文本和粘贴图片的处理逻辑是不一样的,命令绑定负责将 “ 粘贴 ” 命令与合理的处理逻辑连接起来。
关于命令系统将在本文章的后续部分中讲解,不过值得一提的是,在上面的 Demo 中我们只指定了命令和命令源,并未指定命令目标,但它会以获取键盘焦点的元 素(这里是我们的 RichTextBox )作为默认值,而命令绑定以及命令的后台执行逻辑被隐藏到了 RichTextBox 内部,那些编写 RichTextBox 控件的开发人员会为我们编写该部分代码。
另外,你可能已经发现,在 Demo 中我们并没有为菜单项标题直接设置 “ 复制 ”“ 粘贴 ” 这样的文本,而是使用了如下的一个绑定:
Header="{Binding Path=Command.Text, RelativeSource={RelativeSource Self}}"/>
我们将菜单文本绑定到了命令的 Text 属性,这是因为,如果一个命令为 RoutedUICommand 类型,那么该命令将有一个 Text 属性来说明该命令 对应到的文本名称,该 Text 属性会自动本地化的,也就是说如果你的计算机使用语言是简体中文的话该菜单项显示的是 “ 复制 ” ,如果你的计算机使用的语言是 英语的话该菜单项显示的将是 “Copy” 。
WPF 为我们提供了大量内置命令,包括 ApplicationCommands , NavigationCommands, , MediaCommands , EditingCommands 与 ComponentCommands ,以及控件开发人员为它们的控件也提供了很多特有的命令(比如 Slider.DecreaseLarge 与 Slider.DecreaseSmall ),这些足以应付平时的大多数应用,如果还不够的话,你可以为自己的应用自定义更多的命令。
在 WPF 中,命令( Commanding )被分割成了四个部分,分别是 ICommand , ICommandSource , CommandTarget 和 CommandBinding 。下面我们来分别探讨这四个部分。
1 , ICommand
Command 也就是我们的 “ 命令 ” 本身,比如 “ 复制 ”“ 粘贴 ” 。在 WPF 中,所有的命令都必须实现 ICommand 接口,它为所有的命令提供一个抽象, 这个抽象对于我们实现 Undo 、 Redo 操作非常重要,如果你学习一下设计模式中的 “ 命令 ” 模式,你会更加深刻的理解。
ICommand 接口中拥有 Execute ()方法,该方法用于命令的执行(不过,注意:命令的执行逻辑 —— 比如将剪切板中的文本去出来放到文本框的合适 位置 —— 并没有被编写到该方法中,稍后我们会讲到这其中的奥妙),另外的一个方法是 CanExecute ()用于指示当前命令在目标元素上是否可用,当这种可用性发生改变时其便会引发该接口的尾页一个事件 CanExecuteChanged 。
在目前的 WPF 类库中,你能看到唯一一个实现了 ICommand 接口的类型 RoutedCommand (其实还有一个名为 SecureUICommand 的类也实现了该接口,不过该类未被公开), “Routed” 是一个不太容易被翻译的修饰词(有人将它翻译为 “ 路由 ” ),但这意味着该类型的命令可以向 WPF 中的 RoutedEvent 一样在元素树中上下传递。
RoutedCommand 的子类 RoutedUICommand 是我们经常使用 的类型,它与 RoutedCommand 的不同之处仅仅在与它多了一个 Text 属性来描述该命令,不过大多数 WPF 内置命令的 Text 属性有一个很不错的 特点:其支持自动本地化。 这至少会为我们的软件的本地化减少工作量。
2 , ICommandSource 与 CommandTarget
命令源,用来触发我们的命令,比如用一个菜单项来触发 “ 复制 ” 命令,那么该菜单项就是命令源。要使一个元素成为命令源,其必须实现 ICommandSource 接口。命令源决定了它所要触发的命令、该命令所作用的对象以及命令参数(如果需要的话),这分别对应于它的三个属性: Command 、 CommandTarget 以及 CommandParameter 。其中需要注意的是 CommandTarget ,因为在 WPF 中如果你 不为命令源指定其命令对象,那么其将会把界面上获得键盘焦点的元素作为默认的命令对象,这为我们提供了方便, 比如界面上有两个文本框,我们不必担心主菜单项上的 “ 粘贴 ” 操作是针对哪个文本框的,谁获得焦点便针对谁,这符合大家的习惯。但引入的问题是,如果命令目标不具备获取键盘焦点的能力(比如 Label )或命令源会抢占焦点(比如用 Button 来代替菜单项,点击按钮时焦点由文本框转移到了按钮上),你的命令将会无效,这时你就必须为命令源指 定命令目标。
3 , CommandBinding
前面已经提到我们并没有将命令的执行逻辑编写到其 Excute ()方法中,这是有道理的,比如 " 粘贴 " 命令 ( ApplicationCommands.Paste ),粘贴一段文本到文本框和粘贴一个图片到绘图板的执行逻辑肯定是不一样的,负责开发该 “ 粘贴 ” 命令的开发人员不可能知道所有的粘贴操作的具体逻辑,使用 “ 粘贴 ” 命令的客户也不应该为该执行逻辑负责,编写该执行逻辑的任务应该被分发给那些支持 “ 粘贴 ” 操作的控件的开发人员以及那些希望为自己的控件添加 “ 粘贴 ” 操作的客户。也就是说我们需要将 “ 行为的请求者(命令) ” 和 “ 行为的执行者(命令的执行逻 辑) ” 分开而实现一种松耦合,而 CommandBinding (命令绑定)便是命令和命令执行逻辑的桥接器。
我们使用 CommandBinding 将命令与其合适的执行逻辑绑定在一起:
CommandBinding CloseCommandBinding = new CommandBinding( ApplicationCommands.Close, CloseCommandHandler, CanExecuteHandler);
CommandBinding 构造方法的最后两个参数分别是 ExecutedRoutedEventHandler 与 CanExecuteRoutedEventHandler 类型的委托,用于指示如何执行命令和如何判断命令能否被执行。
与 CommandBinding 一样扮演着中间角色的还有 CommandManager 类,它为命令绑定(以及输入绑定)提供了很多实用方法。
对于3.CommandBinding感觉有点浮云