1 命令的用途
命令这个概念,最早是在AutoCAD中认识的,它可以由多种途径引发,进而执行一系列动作。在WPF中命令被附加了其他的特征,但其存在主要目的在于如下几点。
1. 重用逻辑:友好的界面为用户提供多种方式执行同一操作以迎合不同用户的习惯,如菜单条目、工具栏按钮、窗体快捷键、鼠标手势、显式命令行等。虽然也可以重用事件处理函数,但命令是更高级的抽象,也更加容易被理解。
2. 统一管理:某些动作只有在满足一定条件时才能被触发,而条件不满足时相应控件应为不可用的状态。命令可以(直接或间接)管理关联到自身的触发者,使其可用状态一同变化,这省去了编码时的很多麻烦。
3. 脚本控制:命令从事件处理函数中独立出来,使其单独调用更加方便,从而简化命令行或脚本方式的控制。
4. 历史记录:命令的抽象化在某种程度上是操作历史的前提,这使得实现Command模式的撤销、重做功能成为可能(本文不讨论Command模式的实现)。
2 WPF中的命令
在学习WPF的命令模型时,首先接触到的是ICommand接口(System.Windows.Input名空间)。
public interface ICommand
{
void Execute(object parameter);
bool CanExecute(object parameter);
event EventHandler CanExecuteChanged;
}
观察其成员,发现实现该接口的类实例应当包含一个执行逻辑、一个可用性判断逻辑及其变更事件,但是实际上在WPF中使用的RoutedCommand(System.Windows.Input名空间)并没有简单的实现这个接口,而是拓展了该接口并与其他几个类构成了较为复杂的命令模型(主要是为了应用路由事件)。
从图中可见,参与一次完整的命令调用需要(至少)三个主体实例:作为指令调用发出方的Command Trigger、作为命令事件激发点的Command Target、以及接收命令事件并进行处理的Parent UIElement。
其中CommandTrigger可以是菜单项、按钮等实现ICommandSource接口的类的实例,它会包含一个名为Command的成员,这个Command是RoutedUICommand的实例(继承自RoutedCommand,后者实现ICommand接口),RoutedCommand类重写了Execute和CanExecute方法,在执行命令时Execute方法会从CommandTarget引发CommandBinding.Executed事件。CommandTarget是界面上的元素,命令可以定位到它可能是出于两种情况,一是通过指定ICommandSource的CommandTarget属性,二是(当没有指定ICommandSource.CommandTarget时)元素持有焦点。
注意:Target单词意味“目标”,是指Command对象激发路由事件的目标;CommandBinding.Executed是路由事件,因此可以在ICommand.Execute方法中调用RaiseEvent触发。
路由事件沿可视化树向上冒泡,沿途的UIElement在其CommandBindings集合中检测事件对应的CommandBinding对象,若检测到则执行CommandBinding.Execute方法,进行真正的逻辑处理。
注意:CommandTrigger并不一定处于CommandTarget和ParentUIElement相同的可视化树中。
3 实现WinForm的命令
在传统的WinForm中不存在路由事件,因此可以相对简单的实现Command类,下面直接给出源码。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.ComponentModel;
namespace IEPI.App.Utility
{
/// <summary>
/// 一条命令可以包含多个触发器,并同步触发器可用状态
/// </summary>
public class Command
{
/// <summary>
/// 实例化命令
/// </summary>
/// <param name=”Description”>对命令的描述</param>
/// <param name=”Excution”>命令触发事件的执行体</param>
public Command(string Description, EventHandler Excution)
{
this.TriggerControls = new List<Control>();
this.TriggerItems = new List<ToolStripItem>();
this.Description = Description;
this.ExcutionBody = Excution;
this._Enabled = true;
}
//当前命令的可用状态,不应直接使用
bool _Enabled;
/// <summary>
/// 获取或设置当前命令的可用状态,这将影响所有触发器的Enabled属性
/// </summary>
public bool Enabled
{
get { return _Enabled; }
set
{
_Enabled = value;
foreach (Control TriggerControl in TriggerControls)
{ TriggerControl.Enabled = value; }
foreach (ToolStripItem TriggerItem in TriggerItems)
{ TriggerItem.Enabled = value; }
}
}
//Control类型触发器
List<Control> TriggerControls;
//ToolStripItem类型触发器
List<ToolStripItem> TriggerItems;
/// <summary>
/// 注册触发器
/// </summary>
/// <param name=”TriggerControl”>触发器</param>
public void RegistTrigger(Control TriggerControl)
{
this.TriggerControls.Add(TriggerControl);
TriggerControl.Click += new EventHandler(this.ExcutionShell);
}
/// <summary>
/// 注册触发器
/// </summary>
/// <param name=”TriggerToolStripItem”>触发器</param>
public void RegistTrigger(ToolStripItem TriggerToolStripItem)
{
this.TriggerItems.Add(TriggerToolStripItem);
TriggerToolStripItem.Click += new EventHandler(this.ExcutionShell);
}
//命令执行外壳,用于检验可用状态
void ExcutionShell(object sender, EventArgs e)
{
if (_Enabled) ExcutionBody(sender, e);
}
//命令执行主体
EventHandler ExcutionBody;
/// <summary>
/// 显式强制执行命令处理过程,无论Enabled状态如何
/// </summary>
public void Excute()
{
ExcutionBody(this, new EventArgs());
}
/// <summary>
/// 显式强制执行命令处理过程,无论Enabled状态如何
/// </summary>
/// <param name=”sender”>指定事件的发出者</param>
/// <param name=”e”>事件参数</param>
public void Excute(object sender, EventArgs e)
{
ExcutionBody(sender, e);
}
/// <summary>
/// 获取命令描述
/// </summary>
public string Description { get; private set; }
}
}
在上述代码中,RegistTrigger包含分别接受Control和ToolStripItem实例的两个重载,这是考虑到此两类独立继承自Component、并分别实现了各自的Enabled等属性。
以下链接指向本文原址,其中最末位置包含示例,内容为.csproj项目(以Visual Studio 2010、.Net FrameWork 4.0为平台)。