命令的定义:
命令与事件的区别:命令是具有约束性的。
命令还可以控制接收者"先做校验,再保存,再关闭"。
命令:WPF的命令,实际上就是实现了ICommand接口的类,平时使用最多的是RoutedCommand类,还可以自定义命令。
如何使用自定义命令:
1、命令源:
即命令的发送者,是实现了TCommandSource接口的类,很多界面元素都实现了这个接口。
2、命令目标:
CommandTarget,即命令发送给谁,将作用在谁的身上,命令目标必须是实现了IInputElement接口的类。
3、命令关联:
CommandBinding,负责把一些外围逻辑和命令关联起来,不如执行之前对命令是否可以执行进行判断,命令执行之后还有哪些后续工作等。
基本元素之间的关系:
1、创建命令类:即获得一个实现ICommand接口的类,如果命令与具体业务逻辑无关,则使用WPF类库中的RoutedCommand类即可,如果想得到与业务逻辑相关的专有命令,则需要创建RoutedCommand的派生类。
2、声明命令实例:使用命令时需要创建命令类的实例。这里有个技巧,一般情况下程序中某种操作只需要一个命令实例与之对应即可。比如对应"保存"这个操作,你可以拿同一个实例去命令每个组件执行其保存功能,因此程序中的命令会使用单件模式(Singleton Pattern)以减少代码的复杂度。
3、指定命令的源:即指定由谁来发送这个命令。如果把命令看作炮弹,那么命令源就相当于火炮。同一个命令可以有多个源。比如保存命令,既可以由菜单中的保存项来发送,也可以由工具栏中的保存图标来发送。需要注意的是,一旦把命令指派给命令源,那么命令源就会受命令的影响,当命令不能被执行的时候作为命令源的控件将处在不可用状态。看来命令这种炮弹很智能,当不满足发射条件时还会给用来发射它的火炮上一道保险。避免走火。还需要注意,各种控件发送命令的方法不尽相同,比如 Button 和 MenuItem 是在单击时发送命令,而 ListBoxItem 单击时表示被选中所以双击时才发送命令。
4、指定命令目标:命令目标并不是命令的属性而是命令源的属性,指定命令目标是告诉命令源向哪个组件发送命令,无论这个组件是否拥有焦点它都会收到这个命令。如果没有为命令源指定命令目标,则 WPF 系统认为当前拥有焦点的对象就是命令目标。这个步骤有点像为火炮指定目标。
5、设置命令关联:炮兵是不能单独战斗的,就像炮兵需要侦查兵在射击前观察敌情、判断发射时机,在射击后观测射击效果、帮助修正一样,WPF 命令需要 CommandBinding 在执行前来帮助判断是不是可以执行、在执行后做一些事件来"打扫战场"。

命令实战:
XML
<Grid>
<StackPanel x:Name="stackPanel">
<Button Content="Send Command" x:Name="btn_1" Margin="5"/>
<TextBox x:Name="txt_1" Margin="5" Height="100"/>
</StackPanel>
</Grid>
cs
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
InitialCommand();
}
//第一个参数 Clear 只是一个对命令的命名,用以区分别的命令
//第二个参数 是这个命令归属于哪个类
private RoutedCommand clearCmd = new RoutedCommand("Clear",typeof(MainWindow));
public void InitialCommand()
{
//把命令赋值给命令源(发送者)并指定快捷键
this.btn_1.Command = this.clearCmd;
//添加快捷键启动命令
this.clearCmd.InputGestures.Add(new KeyGesture(Key.C,ModifierKeys.Alt));
//指定命令目标
this.btn_1.CommandTarget = this.txt_1;
//创建命令关联 CommandBinding 是用来定义命令的执行逻辑(Executed 事件)和能否执行的逻辑(CanExecute 事件)
//当一个命令被触发时(例如通过按钮点击或快捷键),WPF 会沿着元素的逻辑树向上查找是否有与该命令相关的 CommandBinding。如果找到,则会触发对应的事件。
CommandBinding cb = new CommandBinding();
cb.Command = this.clearCmd;//只关注与clearCmd相关的事件
cb.CanExecute += new CanExecuteRoutedEventHandler(cb_CanExecute);//添加是否可以执行的事件
cb.Executed += new ExecutedRoutedEventHandler(cb_Executed);//添加执行的事件
//把命令关联安置在外围控件上?为什么呢?
//如果将 CommandBinding 放在按钮(btn_1)上,那么只有当按钮本身直接处理命令时才有效。但如果将命令绑定放在外围控件(如 stackPanel)上,那么整个 stackPanel 内的任何元素(包括按钮、快捷键等)都可以触发该命令,并且会找到这个 CommandBinding。这样可以确保命令的逻辑在整个布局范围内生效。
//代码中,命令可以通过按钮点击触发,也可以通过快捷键(Alt + C)触发。如果将 CommandBinding 放在按钮上,那么快捷键触发时可能无法找到对应的绑定逻辑。而将命令绑定放在外围控件上,无论是按钮点击还是快捷键,都可以正确找到并执行命令。
this.stackPanel.CommandBindings.Add(cb);
}
//当探测命令是否可以执行时,此方法被调用
void cb_CanExecute(object sender,CanExecuteRoutedEventArgs e
)
{
if (string.IsNullOrEmpty(this.txt_1.Text))
{
e.CanExecute = false;
}
else
{
e.CanExecute = true;
}
//避免继续向上传递降低程序性能
e.Handled = true;
}
void cb_Executed(object sender,ExecutedRoutedEventArgs e)
{
this.txt_1.Clear();
//避免继续向上传递降低程序性能
e.Handled = true;
}
}
对于上面代码有几点需要注意的地方:
第一,使用命令可以避免自己写代码判断 Button 是否可用以及添加快捷键。 第二,RoutedCommand 是一个与业务逻辑无关的类,只负责在程序中"跑腿"而并不对命令目标做任何操作,TextBox 并不是由它清空的。那么对 TextBox 的清空操作是谁做的呢?答案是 CommandBinding。因为无论是探测命令是否执行还是命令送达目标,都会激发命令目标发送路由事件,这些路由事件会沿着 UI 元素树向上传递并最终被 CommandBinding 所捕捉。本例中 CommandBinding 被安装在外围的 StackPanel 上,CommandBinding "站在高处"起一个侦听器的作用,而且专门针对 clearCmd 命令捕捉与其相关的路由事件。本例中,当 CommandBinding 捕捉到 CanExecute 事件就会调用 cb_CanExecute 方法(判断命令执行的条件是否满足,并反馈给命令供其影响命令源的状态);当捕捉到的是 Executed 事件(表示命令的 Execute 方法已经执行了,或说命令已经作用在了命令目标上,RoutedCommand 的 Execute 方法不包含业务逻辑,只负责让命令目标激发 Executed),则调用 cb_Executed 方法。 第三,因为 CanExecute 事件的激发频率比较高,为了避免降低性能,在处理完后建议把 e.Handled 设为 true。 第四,CommandBinding 一定要设置在命令目标的外围控件上,不然无法捕捉到 CanExecute 和 Executed 等路由事件。
WPF 的命令库
命令具有"一处声明、处处使用"的特点,比如 Save 命令,在程序的任何地方它都表示要求命令目标保存数据。因此,微软在 WPF 类库里准备了一些便捷的命令库,这些命令库包括:
-
ApplicationCommands
-
ComponentCommands
-
NavigationCommands
-
MediaCommands
-
EditingCommands
命令参数:
前面提到命令库里有很多 WPF 预制的命令,如 New、Open、Copy、Cut、Paste 等。这些命令都是 ApplicationCommands 类的静态属性,所以它们的实例永远只有一个,这就引出一个问题:如果界面上有两个按钮,一个用来新建 Teacher 的档案,另一个用来新建 Student 的档案,都使用 New 命令的话,程序应该如何区别新建的是什么档案呢? 答案是使用 CommandParameter。命令源一定是实现了 ICommandSource 接口的对象,而ICommandSource 有一个属性就是CommandParameter,如果把命令看作飞向目标的炮弹,那么 CommandParameter 就相当于装载在炮弹肚子里的"消息"。下面是程序的实现代码。
XML
<Window x:Class="CommandParameterSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Command Parameter"
Height="240" Width="360" Background="LightBlue" WindowStyle="ToolWindow">
<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>
<!--为窗体添加 CommandBinding-->
<Window.CommandBindings>
<CommandBinding Command="New" CanExecute="New_CanExecute" Executed="New_Executed" />
</Window.CommandBindings>
</Window>
注意:
代码有两个值得注意的地方: 两个按钮都使用 New 命令,但分别使用字符串 Teacher 和 Student 作为参数。 这次是使用 XAML 代码为窗体添加 CommandBinding,CommandBinding 的 CanExecute 和 Executed 事件处理器写在后台 C#代码里。
cs
private void New_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
if (string.IsNullOrEmpty(this.nameTextBox.Text))
{
e.CanExecute = false;
}
else
{
e.CanExecute = true;
}
}
private void New_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));
}
}
命令与 Binding 的结合
初试命令,你可能会想到这样一个问题:控件有很多事件,可以让我们进行各种各样不同的操
作,可控件只有一个 Command 属性,而命令库中却有数十种命令,这样怎么可能使用这个唯一的
Command 属性来调用那么多种命令呢?答案是:使用 Binding。前面已经说过,Binding 作为一种间接的、不固定的赋值手段,可以让你有机会选择在某个条件下为目标赋特定的值(有时候需要借助 Converter)。
例如,如果一个 Button 所关联命令有可能根据某些条件而改变,我们可以把代码写成这样:
cs
<Button x:Name="dynamicCmdBtn" Command="{Binding Path=ppp, Source=sss}" Content="Command" />
-
Command
:这是一个附加属性,它告诉WPF按钮应该执行一个命令。 -
{Binding ...}
:这是一个绑定表达式,用于将按钮的Command
属性与视图模型中的命令属性进行连接。 -
Path=ppp
:这里的ppp
是一个占位符,代表视图模型中命令属性的名称。例如,如果你的视图模型中有一个名为SaveCommand
的命令属性,那么这里的ppp
应该替换为SaveCommand
。 -
Source=sss
:这里的sss
也是一个占位符,代表数据上下文(DataContext)的名称。在WPF中,数据上下文是绑定的起点,它通常是包含数据和命令的对象。如果你的视图模型实例被设置为某个控件(如窗口或页面)的DataContext
,那么这里的sss
应该替换为该控件的XAML名称,或者是RelativeSource
绑定,指向包含数据上下文的父级控件。