锦囊妙计的本质就是命令。
有了路由事件为什么还需要命令系统呢?事件的作用是发布、传播一些消息,消息送达接收者,事件的使命也就完成了,至于如何响应事件送来的消息事件并不做规定,每个接收者可以使用自己的行为来响应事件。也就是说,事件不具有约束力。命令与事件的区别就在于命令是具有约束力的。
实际编程中就算只使用事件、不使用命令,程序的逻辑也一样可以被驱动的很好,但我们不能阻止程序员按自己的习惯去编写代码。比如保存事件的处理器,程序员可以写成Save()、SaveHandler()、SaveDocument()这些都符合代码规范,但迟早有一天整个项目会变得无法被读懂,新来的程序员或修改bug的程序员会很抓狂。如果使用命令,情况会好很多——当Save命令到达某个组件的时候,命令会主动调用组件的Save()方法,而这个方法可能被定义在基类或者接口里,这就在代码结构和命名上做了约束。不但如此,命令还可控制接收者”先做校验、再保存、然后关闭“,也就是说,命令除了可以约束代码,还可以约束步骤逻辑。
WP的命令系统由一下几个基本要素构成:
1) 命令(Command):WPF的命令实际上就是实现了ICommand接口的类,平时使用最多的是RoutedCommand类。
2) 命令源(Command Source):即命令的发送者,是实现了ICommandSource接口的类。很多界面元素都实现了这个接口,包括Button、MenuItem、ListBoxItem等。
3) 命令目标(Command Target):即命令将发送给谁,或者说命令将作用在谁身上。命令目标必须是实现了IInputElement接口的类。
4) 命令关联(Command Binding):负责把一些外围逻辑与命令关联起来,比如执行之前对命令是否可以执行进行判断、命令执行之后还有哪些后续工作等。
这些基本元素之间体现在使用命令的过程中。命令的使用大概分为以下几步:
1) 创建命令类:即获得一个实现ICommand接口的类,如果命令与具体业务逻辑无关则使用WPF类库中的RoutedCommand类即可。如果想要得到与业务逻辑相关的专有命令,则需要创建RoutedCommand(或者ICommand接口)的派生类。
2) 声明命令实例:使用命令时需要创建命令类的实例。这里有个技巧,一般情况下程序中某种操作只需要一个命令实例与之对应即可。比如对应”保存“这个操作,你可以拿同一个实例去命令每个组件执行保存功能,因此程序中的命令多使用单件模式(Singletone Pattern)以减少代码的复杂度。
3) 指定命令的源:即指定由谁来发送这个命令。如果把命令当做炮弹,那么命令源就相当于火炮。同一个命令可以有多个源。比如保存命令,既可以由菜单中的保存项来发送,也可以由工具栏中的保存图标来发送。需要注意的是,一旦把命令指派给命令源,那么命令源就会受命令的影像,当命令不能被执行的时候作为命令源的控件将处在不可用状态。看来命令这种炮弹很智能,当不满足发射条件时还会给用来发射它的火炮上一道保险、避免走火。还需要注意,各种控制发送命令的方法不尽相同,比如Button和MenuItem是在单击时发送命令,而ListBoxItem单击时表示被选中所以双击才会发送命令。
4) 指定命令目标:命令目标不是命令的属性而是命令源的属性,指定命令目标是告诉命令源向哪个组件发送命令,无论这个组件是否拥有焦点它都会受到这个命令。如果没有命令源指定命令目标,则WPF系统认为当前拥有焦点的对象就是命令目标。这个步骤有点向为火炮指定目标。
5) 设置命令关联:炮兵是不能单独作战的,就像炮兵需要侦查兵在射击前观察敌情、判断发射时机,在射击后观测射击效果、帮助修正一样,WPF命令需要CommandBinding在执行前来帮助是不是可以执行、在执行后做一些事情来“打扫战场”。
在命令目标和命令关联之后还有一个微妙的关系。无论命令目标是由程序员指定还是WPF系统根据焦点所在地判断出来,一旦某个UI组件被命令源“瞄上”,命令源就会不停地向命令目标“投石问路”,命令目标就会不停地发送出可路由的PreviewCanExecute和CanExecute附加事件,事件会沿着UI元素树向上传递并被命令关联所捕获,命令关联捕获到这些事件后会把命令能不能发送实时报告给命令。类似的,如果命令被发送出来并达到命令目标,命令目标就会发送PreviewExecuted和Executed两个附加事件,这两个事件也会沿着UI元素树向上传递并被命令关联所捕获,命令关联会完成一些后续的任务。别小看后续任务,对于那些与业务逻辑无关的通用命令,这些后续任务才是最重要的。
命令目标怎么会发出PreviewCanExecute、CanExecute、PreviewExecuted和Execute事件呢?其实这四个事件都是附加事件,是被CommandManager类“附加”给命令目标的。另外,PreviewCanExecute和CanExecute的执行时机不由程序员控制,而且执行频率比较高,这不但给系统性能带来些降低,偶尔还会引入几个意想不到的bug并且比较难调试,请多加小心。
WPF命令系统基本元素的关系图:
DEMO
定义一个命令,使用Button来发送这个命令,当命令送达TextBox时TextBox会被清空(如果TextBox中没有文字则命令不可被发送)。
XAML:
<Window x:Class="Chapter9.Page178.CommandDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<StackPanel x:Name="stackPanel">
<Button x:Name="button1" Content="Send Command" Margin="5"/>
<TextBox x:Name="textBoxA" Margin="5" Height="100"/>
</StackPanel>
</Window>
后台代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Chapter9.Page178.CommandDemo
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
InitializeCommand();
}
//声明并定义命令
private RoutedCommand clearCmd = new RoutedCommand("Clear", typeof(MainWindow));
private void InitializeCommand()
{
//把命令赋值给命令源(发送者)并指定快捷键ALt+B
this.button1.Command = this.clearCmd;
this.clearCmd.InputGestures.Add(new KeyGesture(Key.B, ModifierKeys.Alt));
//指定命令目标
this.button1.CommandTarget = this.textBoxA;
//创建命令关联
CommandBinding cb = new CommandBinding();
cb.Command = this.clearCmd; //只关注与clearCmd相关的事件
cb.CanExecute += new CanExecuteRoutedEventHandler(cb_CanExecute);
cb.Executed += new ExecutedRoutedEventHandler(cb_Executed);
//把命令关联安置在外围控件上
this.stackPanel.CommandBindings.Add(cb);
}
//当命令送达目标后,此方法被调用
void cb_Executed(object sender, ExecutedRoutedEventArgs e)
{
this.textBoxA.Clear();
//避免继续上传而降低程序性能
e.Handled = true;
}
//当探测命令是否可以执行时,此方法被调用
void cb_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
if (string.IsNullOrEmpty(this.textBoxA.Text))
{
e.CanExecute = false;
}
else
{
e.CanExecute = true;
}
//避免继续向上传递而降低程序性能
e.Handled = true;
}
}
}
运行程序,在TextBox中输入文字后Button在命令可执行状态的影响下变为可用,此时单击Button或者Alt+B键,TextBox都会被清空。
对于上面的代码需要注意:
1) 使用命令可以避免自己写代码判断Button是否可用以及添加快捷键。
2) RoutedCommand是一个与业务逻辑无关的类,只负责在程序中“跑腿”而并不对命令目标做任何操作,TextBox并不是由它清空的。清空操作是由CommandBinding实现的。因为无论是探测命令是否执行还是命令送达目标,都会激发命令目标发送路由事件,这些路由事件会沿着UI元素树向上传递并最终被CommandBinding所捕获。本例中CommandBinding被安装在外围的StackPanel上,CommandBinding“站在高处”起一个侦听器的作用,而且专门针对clearCmd命令捕捉与其相关的路由事件。本例中,当CommandBinding捕获到CanExecute事件就会调用cb_CanExecute方法(判断命令执行的条件是否满足,并反馈给命令供其影响命令源的状态);当捕获到的是Executed事件(表示命令在Execute方法已经执行,或说明命令已经作用在了命令目标上,RoutedCommand的Execute方法不包含业务逻辑,只负责让命令目标激发Executed),则调用cb_Executed方法。
3) 因为CanExecute事件的激发频率比较高,为了避免降低性能,在处理完后建议把e.Handled设为true。
4) CommandBinding一定要设置在命令目标的外围控件上,不然无法捕获到CanExecute和Executed等路由事件。\
WPF的命令库
上面的这个例子中我们自己生命定义了一个命令:
//声明并定义命令
private RoutedCommand clearCmd = new RoutedCommand("Clear", typeof(MainWindow));
命令具有“一处声明,处处使用”的特点,比如Save命令,在程序的任何地方它都表示要求命令目标保存数据。因此,微软在WPF类库里准备了一些便捷的命令库,这些命令库包括:
1) ApplicationCommands
2) ComponentCommands
3) NavigationCommands
4) MediaCommands
5) EditingCommands
他们都是静态类,而命令就是用这些类的静态类只读属性以单件模式暴露出来的。
命令参数
前面提到命令库里有很多WPF预制的命令,这些命令都是ApplicationCommands类的静态属性,所以他们的实例永远只有一个。这就有一个问题:如果界面上有两个按钮,一个用来新建Teacher档案,另一个用来新建Students档案,都使用New命令的话,程序应该如何区别新建的是什么档案呢?
答案是使用CommandParameter。命令源一定是实现了ICommandSource接口的对象,而ICommandSource就有一个属性就是CommandParameter,如果把命令当做飞向目标的炮弹,那么CommandParameter就相当于装载在炮弹肚子里的“消息”。下面是程序的实现代码:
<Window x:Class="Chapter9.Page181.CommandParameterDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<!--为窗体添加CommandBinding-->
<Window.CommandBindings>
<CommandBinding Command="New" CanExecute="CommandBinding_CanExecute" Executed="CommandBinding_Executed"/>
</Window.CommandBindings>
<Grid Margin="6">
<Grid.RowDefinitions>
<RowDefinition Height="24"/>
<RowDefinition Height="4"/>
<RowDefinition Height="24"/>
<RowDefinition Height="4"/>
<RowDefinition Height="24"/>
<RowDefinition Height="4"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!--命令和命令参数-->
<TextBlock Text="Name:" VerticalAlignment="Center" HorizontalAlignment="Left" Grid.Row="0"/>
<TextBox x:Name="nameTextBox" Margin="60,0,0,0" Grid.Row="0"/>
<Button Content="New Teacher" Command="New" CommandParameter="Teacher" Grid.Row="2"/>
<Button Content="New Student" Command="New" CommandParameter="Student" Grid.Row="4"/>
<ListBox x:Name="listBoxNewItems" Grid.Row="6"/>
</Grid>
</Window>
代码有两个值得注意的地方:
1) 两个按钮都使用New命令,但分别使用字符串Teacher和Student作为参数。
2) 这次是使用XAML代码为窗体添加CommandBinding、CommandBinding的CanExecute和Executed事件处理器写在后台C#代码里。
后台代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Chapter9.Page181.CommandParameterDemo
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
if (string.IsNullOrEmpty(this.nameTextBox.Text))
{
e.CanExecute = false;
}
else
{
e.CanExecute = true;
}
e.Handled = true;
}
private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
{
string name = this.nameTextBox.Text;
if (e.Parameter.ToString() == "Teacher")
{
this.listBoxNewItems.Items.Add(string.Format("New Teacher:{0}", name));
}
if (e.Parameter.ToString() == "Student")
{
this.listBoxNewItems.Items.Add(string.Format("New Student:{0}", name));
}
e.Handled = true;
}
}
}
运行程序,当TextBox中没有内容时两个按钮均不可用;当输入文字后按钮变成可用,单击按钮,ListBox会加入不同条目。
命令与Binding的结合
有这么一个问题:控件有很多事件,可以让我们进行各种各样不同的操作,可控件只有一个Command属性,而命令库中却有数十种命令,这样怎么可能使用这个唯一的Command属性来调用那么多种命令呢?答案是:使用Binding。前面已经说过,Binding作为一种间接地、不固定的赋值手段,可以让你有机会选择在某个条件下为目标赋特定的值(有时候需要借助Converter)。
例如,如果一个Button所关联命令有可能根据某些条件而改变,我们可以把代码写成这样:
<Button x:Name="dynamicCmdBtn" Command="{Binding Path=ppp,Source=sss}" Content="Command"/>
日常工作中一个控件的命令一经确定就很少改变。
ICommand接口与RoutedCommand
一般情况下,程序使用与逻辑无关的RoutedCommand来泡泡龙套就足够了,但为了使程序的结构更加简洁,我们通常需要定义自己的命令。
WPF的命令是实现了ICommand接口的类。ICommand接口非常简单,只包含两个方法和一个事件:
1) Execute方法:命令执行,或者说命令作用于命令目标之上。需要注意的是,现实世界中的命令是不会自己“执行”的,它只能“被执行”,而在这时,执行变成了命令的方法。
2) CanExecute方法:在执行之前用来探知命令是否可被执行。
3) CanExecuteChanged事件:当命令可执行状态发生改变时,可激发此事件来通知其他对象。
RoutedCommand就是这样一个实现了ICommand接口的类。RoutedCommand在实现ICommand接口时,并未向Execute和CanExecute方法中添加任何逻辑。
自定义Command
自定义命令可以分为两个层次来理解:
第一层次是比较浅,指的是当WPF命令库中没有包含想要的命令时,我们就得声明定义自己的RoutedCommand实例。比如想让命令目标在命令到达时发出笑声,WPF命令库里没有这个命令,那就可以定义一个名为Laugh的RoutedCommand实例。很难说着是真正意义上的“自定义命令”,这只是对RoutedCommand的使用。
第二个层次是指从实现ICommand接口开始,定义自己的命令并且把某些业务逻辑也包含在命令之中,这才称得上是真正意义上的自定义命令。