Command探究
Command探究
Prism中的Command是基于AttachedBehavior的。本章不讨论AttachedBehavior的原理,只涉及在项目中如何使用Command,而且只讨论Button上的Click事件。
对于Command,WPF和Silverlight不太一样,因为后者不支持静态类和静态成员,所以二者在实现上有所不同。本章默认介绍WPF的语法,捎带提及Silverlight的实现方式。
早在WPF设计的最初,就为Command编程模型打好了基础,所有按钮(包括Button、RadioButton、CheckBox等等)的基类ButtonBase都实现了ICommandSource接口,如下所示:
// Summary:
// Defines an object that knows how to invoke a command.
public interface ICommandSource
{
// Summary:
// Gets the command that will be executed when the command source is invoked.
ICommand Command { get; }
//
// Summary:
// Represents a user defined data value that can be passed to the command when
// it is executed.
//
// Returns:
// The command specific data.
object CommandParameter { get; }
//
// Summary:
// The object that the command is being executed on.
IInputElement CommandTarget { get; }
}
这样,这些按钮就都具有3个属性,其中以Command这个只读属性使用频率最高,它是ICommand接口类型的,定义如下:
public interface ICommand
{
event EventHandler CanExecuteChanged;
bool CanExecute(object parameter);
void Execute(object parameter);
}
其中,我们经常使用的是后两个方法:CanExecute和Execute。而且从Execute方法的返回类型可以看到,这里的Command是只支持void返回类型的方法。
话说,Command有2种玩法,分别是DelegateCommand和CompositeCommand,它们都派生于ICommand接口。
1) 一次操作执行一个Command
这是由DelegateCommand来实现的。
如图所示,我们先声明一个Command并绑上指定Execute和CanExecute方法:
public DelegateCommand<object> ClickCommand = new DelegateCommand<object>(OnClick, CanExecute);
void OnClick(object e)
{
//do something
}
bool CanExecute(object e)
{
//do something
}
然后在View(也就是XAML)中绑定这个Command。
<Button Height="23" Command="{Binding}">Button1</Button>
最后:在二者之间进行绑定:
this.button.DataContext = ClickCommand;
看到没,Command就这么简单,但是很可惜,只能用于Button的Click事件,如何将任意控件的事件转换为Command,请参见下一章《从Event折腾到Command》。
下面介绍Command的几个扩展。
1. Command上不是有2个方法吗?Execute和CanExecute。首先执行CanExecute,根据返回值决定是否要执行Execute。但我们通常不进行CanExecute判断,而直接执行Execute,也就是说,使用这个DelegateCommand<T>泛型类的第一个构造函数:
public DelegateCommand<object> ClickCommand = new DelegateCommand<object>(OnClick);
另外,根据C# 3.0中的lambda表达式语法,也可以改写为如下形式:
public DelegateCommand<object> ClickCommand = new DelegateCommand<object>(OnClick, arg => true);
2. 注意,Button实现了ICommandSource接口,其中Command属性是只读的,既然用不到它的set方法,那么就让我们把它设置为private set,如下所示:
public DelegateCommand<object> ClickCommand { get; private set; }
这也从侧面说明了CanExecute和Execute两个方法只能在Command的构造函数中初始化。
3. 出于惰性声明的思想,我们将Command声明为ICommand类型,而在构造函数中将其实例化为具体的类型,于是大家常常会看到这样的语句:
public DelegateCommand<object> ClickCommand { get; private set; }
//以下实例化语句出现在其它方法中,也就是需要实例化的时候
ClickCommand = new DelegateCommand<object>(OnClick, e => true);
4. 大家可以看到,我在SaveCommand的声明中使用的是object作为DelegateCommand<T>这个泛型对象的参数。这是因为,一般而言,Button本身不带有任何有意义的数据,所以使用object来来填充T这个位置。当然我们也可以使用其它任何类型,甚至是自定义类型,从而在点击Button的同时收集到这些有用的数据。
基于这个思路,我们来修改上面的代码:
首先是XAML:
<Button Height="23" Name="button2" Command="{Binding}" CommandParameter="BaoBao">Button2</Button>
这里把CommandParameter的值作为字符串参数传递到Command的OnSave中。
其次是Command的声明:
public ICommand ClickCommand { get; private set; }
this.ClickCommand = new DelegateCommand<string>(OnClick, arg => true);
this.button.DataContext = ClickCommand;
这里使用了string,来作为Command的参数。
5. 上述的代码是基于WPF的,对于Silverlight,由于后者不支持静态类和静态成员,所以要把,而其他部分保持不变:
<Button x:Name="button1" Height="20" cmd:Click.Command="{Binding}" Content="Save"/>
分别为WPF和Silverlight准备了一个Demo,来验证以上若干文字:
C:\Users\baoj\Documents\Visual Studio 2008\Projects\ WpfPrismApplication1.zip
C:\Users\baoj\Documents\Visual Studio 2008\Projects\SilverlightPrismApplication1
补充:在具体的项目中,我们可以把”Baobao”替换为数据绑定。不过这就麻烦了,因为button1的Command和CommandParameter都要进行数据绑定,所以要把这两个参数所要绑定的数据抽象为一个实体类Model:
public class Model
{
public ICommand ClickCommand2 { get; set; }
public string UserName { get; set; }
}
然后将原先的Command声明和数据绑定进行如下修改:
public Model Model { get; set; }
this.button2.DataContext = new Model()
{
ClickCommand2 = new DelegateCommand<string>(OnClick2, arg => true),
UserName = "BaoBao"
};
最后在XAML中的修改就简单了:
<Button Height="23" Name="button2" Command="{Binding ClickCommand2}" CommandParameter="{Binding UserName}">Button2</Button>
修改后的代码下载:
C:\Users\baoj \Documents\Visual Studio 2008\Projects\WpfPrismApplication1_new version.zip
2) 一次操作执行多个Command
扯了半天,我们所遇到的场景只局限于点击一次按钮然后执行一个Command。我们还有一种需求,就是点击一次按钮,执行一连串的Command。为此,Prism为我们提供了CompositeCommand类来解决这一需求。
CompositeCommand类,从字面上就能看出,它由若干Command组成的。它实现了ICommand接口,就是说,它也具有CanExecute和Execute这两个接口方法。
设想一个场景,点击Button的同时,一次触发两个Command,分别修改TextBlock和TextBox的值:
这是我们要定义一个CompositeCommand,它包括这两个Command。但是,为了能够对其进行单元测试,我们创建了一个静态的代理类,将这个CompositeCommand封装成一个静态属性:
public static class GlobalCommands
{
public static CompositeCommand MyCompositeCommand = new CompositeCommand();
}
然后在后台代码中进行声明:
public partial class Window1 : Window
{
public ICommand ClickCommand1 { get; private set; }
public ICommand ClickCommand2 { get; private set; }
public Window1()
{
InitializeComponent();
ClickCommand1 = new DelegateCommand<object>(OnClick1, args => true);
ClickCommand2 = new DelegateCommand<object>(OnClick2, args => true);
GlobalCommands.MyCompositeCommand.RegisterCommand(ClickCommand1);
GlobalCommands.MyCompositeCommand.RegisterCommand(ClickCommand2);
}
public void OnClick1(object obj)
{
textBox1.Text = "BaoBao";
}
public void OnClick2(object obj)
{
textBlock1.Text = "Jax.Bao";
}
}
其中OnClick1是操作TextBox的,OnClick2是操作TextBlock的。
在XAML中,绑定到Button的语法:
<Button Command="{x:Static local:GlobalCommands.MyCompositeCommand}">Button1</Button>
这里的local定义如下,是对当前项目namespace的一个引用声明:
xmlns:local="clr-namespace:WpfPrismApplication2"
这里,我们连this.button.DataContext = ClickCommand; 这样的语法也不需要了,因为在xaml的绑定语法中,静态类GlobalCommands事先已经帮我们打理好一切了。
但是,对于Silverlight而言,它是不支持x:static的,所以为了演示上面相同的功能,我们要对刚才的代码进行改造:
首先要把原先的静态代理类作为View的一个属性:
public ICommand MyCompositeCommand
{
get
{
return GlobalCommands.MyCompositeCommand;
}
}
然后在xaml的button中,直接绑定这个属性:
<Button x:Name="button1" cmd:Click.Command="{Binding}" Content="Button"/>
最后,手动设置xaml中button和CompositeCommand之间的绑定关系:
this.button1.DataContext = this.MyCompositeCommand;
效果图如下所示(点击Button1后的效果):
分别为WPF和Silverlight准备了一个Demo,来验证以上若干文字:
C:\Users\baoj\Documents\Visual Studio 2008\Projects\WpfPrismApplication2
C:\Users\baoj\Documents\Visual Studio 2008\Projects\SilverlightPrismApplication2
3) 一次操作执行不同View中的多个Command
如果CompositeCommand只是这样,那它就没什么实用价值了。我们看下面这个图:
怎么理解上面这个图呢?让我们把CompositeCommand提升到单独一个项目中,这样就可以让不同项目中的不同View都共享同一套CompositeCommand,注册它们自己的Command。当在其中一个View中点击Button时,所有注册过的Command,即使不在同一个项目中,都会被执行。
举一个最简单的例子,就是注册新用户。要在好几个Tab页面中填写不同的信息,最后点击SaveAll按钮,所有数据一次性全部提交。
这才是CompositeCommand的真正用武之地。
我信手写了一个WPF版本的实现,大家可以参考,从中领悟这其中的深刻思想。代码下载:C:\Users\baoj\Documents\Visual Studio 2008\Projects\WpfPrismApplication3.zip
写完这个Demo,我忽然发现,点击SaveAll按钮后,两个子View都会执行各自的OnClick方法,比如说弹出对话框。但我现在的需求是把这些填入的信息都汇总到Button所在的主View中,从而一次性提交所有数据到数据库。
如何在主View中搜集这些信息呢?这就到了Prism中Event出场的时候了。
关于Prism中的Event机制,读者可以参加我的另一篇文章《Prism研究 之Event》。我们知道,在Prism中,Event专门用于在不同View之间传递消息。
于是上面的Demo可以修改为:在主View中subscribe,而在各个子View的OnClick方法中publish。
效果图如下所示,改良过的Demo下载。
C:\Users\baoj\Documents\Visual Studio 2008\Projects\PrismEvent\PrismEvent
注意,这个例子有很多问题没有细究,比如说如果没填写完整个人信息并没有检查,又比如说多个Event向上冒时的多线程处理。毕竟我这个小程序只是为了展示Command,我会在稍后章节进行介绍其它的细节。
4) 一次操作可选择地执行执行多个Command
继续扩展我们的需求。在上一部分,我们点击了一次按钮,执行了多个Command,但是如何有选择地只执行其中一部分Command呢?
答案是IActiveAware接口,这个接口是由Prism为我们提供的,定义如下:
/// <summary>
/// Interface that defines if the object instance is active
/// and notifies when the activity changes.
/// </summary>
public interface IActiveAware
{
/// <summary>
/// Gets or sets a value indicating whether the object is active.
/// </summary>
/// <value><see langword="true" /> if the object is active; otherwise <see langword="false" />.</value>
bool IsActive { get; set; }
/// <summary>
/// Notifies that the value for <see cref="IsActive"/> property has changed.
/// </summary>
event EventHandler IsActiveChanged;
}
由于DelegateCommand<T>这个泛型类在实现了ICommand接口的同时,也实现了IActiveAware接口,所以我们可以根据IsActive这个bool值来判断当前Command是否是要执行的。
这里势必有人会问,我们在哪里可以设置Command的IsActive属性呢?CompositeCommand有2个构造函数:
public partial class CompositeCommand : ICommand
{
private readonly bool monitorCommandActivity;
public CompositeCommand()
{
}
public CompositeCommand(bool monitorCommandActivity)
: this()
{
this.monitorCommandActivity = monitorCommandActivity;
}
//以下省略其它成员
}
第二个构造函数中bool类型的monitorCommandActivity参数,如果设为true,则表示CompositeCommand会监视其中所有Command的IsActive属性,否则,就不会监视。我们通常使用无参构造函数来实例化CompositeCommand,那就是说,使用monitorCommandActivity的默认值false(bool类型的默认值为false),所以不会监视,而相应的ShouldExecute方法,也会直接返回true。
protected virtual bool ShouldExecute(ICommand command)
{
var activeAwareCommand = command as IActiveAware;
if (this.monitorCommandActivity && activeAwareCommand != null)
{
return activeAwareCommand.IsActive;
}
return true;
}
ShouldExecute方法只是对IsActive属性的包装。
对于某个Command是否需要执行,具体的判断是在CompositeCommand的CanExecute方法中做的,该方法会遍历注册到当前CompositeCommand中的所有Command,检查该Command的IsActive属性是否为true(通过ShouldExecute方法来判断),检查CanExecute方法是否返回true,只要有一个不为true,就不会执行。
public virtual bool CanExecute(object parameter)
{
bool hasEnabledCommandsThatShouldBeExecuted = false;
ICommand[] commandList;
lock (this.registeredCommands)
{
commandList = this.registeredCommands.ToArray();
}
foreach (ICommand command in commandList)
{
if (this.ShouldExecute(command))
{
if (!command.CanExecute(parameter))
{
return false;
}
hasEnabledCommandsThatShouldBeExecuted = true;
}
}
return hasEnabledCommandsThatShouldBeExecuted;
}
Execute比较有趣,它是把CompositeCommand中存放的Command集合registeredCommands,找出其IsActive为true的Command子集,放到一个队列中,然后逐个弹出并执行。
注意,CompositeCommand还有RegisterCommand和UnregisterCommand这对方法,这就导致了CompositeCommand中存放的Command集合registeredCommands会随着这两个方法的执行而变化。
是不是很乱?可以操作的元素多了,灵活性大了,但是没搞清楚这之间的关系,出错的可能性也大了。让我们理清一下思路,想要执行CompositeCommand中的一部分Command,需要考虑的事情还是蛮多的。
我想,可以根据CompositeCommand构造函数的不同划分为两种情况:
对于CompositeCommand的无参构造函数,由于它的ShouldExecute永远返回true(具体分析见上文),所以我们不要考虑每个Command的IsActive属性。对于不需要的Command,我们只需调用UnregisterCommand方法将它从Command集合中移除,就可以保证CompositeCommand不会执行该Command。如果以后还想执行这个Command,就要使用RegisterCommand方法把它加进来。貌似是废话哦,人家都离职了,还要发给人家薪水~~当然还可以再次加入这家公司,但主动权在公司那边(要CompositeCommand调用RegisterCommand方法才可以)。
对于CompositeCommand的带参构造函数,我们一般将其monitorCommandActivity参数设为true(设为false?汗,那还不如用无参的构造函数呢)。这就会检查每个Command的IsActive属性啦。我们可以把不需要执行的Command的IsActive属性设为false,从而“躲”过这一轮的执行,下次如果又想执行这个Command了,把它的IsActive属性设为true就好了。人家是停职留薪,而不是离职,什么时候想上班,照样可以领薪水的,主动权在自己手里(修改自身的IsActive属性就可以了)。
对于第一种情况,无参构造函数+UnregisterCommand组合,Prism自带的示例Commanding(位于PRISM\Quickstarts\Commanding)就是基于此而实现的。这个例子中,有3个Order需要填写,每个Order都有一个Save按钮,可以点击提交;而在所有Order的外面,又有一个SaveAll的按钮,可以一次性提交所有的Order(从技术角度讲,就是执行所有的Command,这里是3个)。
注意到,每次点击Save按钮后,就会关闭当前所在的Order,在后台程序中会把这个Command从Command集合中注销,这样再点击SaveAll按钮的时候,就只会执行2个Command了。
我们考察OrdersEditorPresentationModel这个类,其中OrderSaved方法中的UnregisterCommand语句,就是在做注销的事情。
private void OrderSaved(object sender, DataEventArgs<OrderPresentationModel> e)
{
if (e != null && e.Value != null)
{
OrderPresentationModel order = e.Value;
if (this.Orders.Contains(order))
{
order.Saved -= this.OrderSaved;
this.commandProxy.SaveAllOrdersCommand.UnregisterCommand(order.SaveOrderCommand);
this.Orders.Remove(order);
if (this.Orders.Count > 0)
{
this.SelectedOrder = this.Orders[0];
}
}
}
}
于是我们看到,每点击一次Save,左边的Order就少了一个,如下图所示:
A.初始化界面,左边3个Order列表。
B.填写右边的Order信息,然后点击右下角的Save按钮。
C.点击Save后,左边的Order列表还剩2个。这时候点击左上角的Save All Orders按钮,只会Save剩下的2个Order了。
Prism自带的这个Command示例,除了麻烦一点,本身是没什么难度的。大家花一点耐心,就可以看明白它的思想。
对于第2种情况,也就是有参构造函数+IsActive组合,我想了很久,试图找一个很有说服力的Demo,终于在凌晨2点有了灵感,这不就是我们赖以生存的Visual Studio吗?看一个截图效果:
对,就是dirty save啦。我用VS打开了3个文件,并修改了其中2个文件,尚未保存(看见文件OrderModule.cs右上角的那个星号了没,不要说你不知道那是啥意思噢)。当我按下File菜单中的Save All的时候,带星号的两个文件都会被保存,而第一个文件,也就是没修改过的OrdersToolBar.xaml.cs不会执行任何操作。
我信手写了一个实现,以飨读者。代码下载:
其实这两种情况(暂时称之为无参式和有参式)的区别就在于,在有参式中,CompositeCommand中Command的数量是不变的,变化的只是其中IsActive为true的数量(也就是可以执行的Command的数量);而在无参式中,CompositeCommand中的Command数量则是变化的。所以有参式更适合做Visual Studio这样的Save All操作,而无参式则适合于一次性操作(做完一次就关闭窗口的那种)。当然,在无参式中,手动调用RegisterCommand方法把之前移除的Command重新添加到CompositeCommand中,也能做出和有参式相同的效果,只不过有点不合时宜了。
为了把Prism中的Command讲明白,本章没有使用MVP或MVVM模式来重构。注意,在使用Command的时候,往往要结合这些模式来剥离View和逻辑,代码稍有变化,但大同小异,请参见我的另一篇文章《MVP之今生前世》。
此外,对于CompositeCommand的实现,Prism文档还提供了另一种方法,就是把Command存储在App.xaml的Application.Resources节点中,然后在xaml中绑定该资源。我个人觉得这是一个馊主意,因为它把一部分业务逻辑放到了资源中而这个地方一般只是用来放置Style之类的东西,所以在这里就不介绍了。