Prism研究(for WPF & Silverlight)9.Command批判
Prism中的Command是基于AttachedBehavior的。本章不讨论AttachedBehavior的原理,只涉及在项目中如何使用Command,而且只讨论Button上的Click事件。
对于Command,WPF和Silverlight不太一样,因为后者不支持静态类和静态成员,所以二者在实现上有所不同。本章默认介绍WPF的语法,捎带提及Silverlight的实现方式。
早在WPF设计的最初,就为Command编程模型打好了基础,所有按钮(包括Button、RadioButton、CheckBox等等)的基类ButtonBase都实现了ICommandSource接口,如下所示:
// 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接口类型的,定义如下:
{
event EventHandler CanExecuteChanged;
bool CanExecute(object parameter);
void Execute(object parameter);
}
其中,我们经常使用的是后两个方法:CanExecute和Execute。而且从Execute方法的返回类型可以看到,这里的Command是只支持void返回类型的方法。
在Prism中,提供了两个Command,分别是DelegateCommand和CompositeCommand,它们都派生于ICommand接口,它们在Prism中的位置如下图所示:
分别讨论如下:
(一) 一次执行一个Command
这是由DelegateCommand来实现的。
如图所示,我们先声明一个Command并绑上指定Execute和CanExecute方法:
void OnClick(object e)
{
//do something
}
bool CanExecute(object e)
{
//do something
}
然后在View(也就是XAML)中绑定这个Command。
最后:在二者之间进行绑定:
看到没,Command就这么简单,但是很可惜,只能用于Button的Click事件,如何将任意控件的事件转换为Command,请参见下一章《从Event折腾到Command》。
下面介绍Command的几个扩展。
1. Command上不是有2个方法吗?Execute和CanExecute。首先执行CanExecute,根据返回值决定是否要执行Execute。但我们通常不进行CanExecute判断,而直接执行Execute,也就是说,使用这个DelegateCommand<T>泛型类的第一个构造函数:
另外,根据C# 3.0中的lambda表达式语法,也可以改写为如下形式:
2. 注意,Button实现了ICommandSource接口,其中Command属性是只读的,既然用不到它的set方法,那么就让我们把它设置为private set,如下所示:
这也从侧面说明了CanExecute和Execute两个方法只能在Command的构造函数中初始化。
3. 出于惰性声明的思想,我们将Command声明为ICommand类型,而在构造函数中将其实例化为具体的类型,于是大家常常会看到这样的语句:
//以下实例化语句出现在其它方法中,也就是需要实例化的时候
ClickCommand = new DelegateCommand<object>(OnClick, arg => true);
4. 大家可以看到,我在SaveCommand的声明中使用的是object作为DelegateCommand<T>这个泛型对象的参数。这是因为,一般而言,Button本身不带有任何有意义的数据,所以使用object来来填充T这个位置。当然我们也可以使用其它任何类型,甚至是自定义类型,从而在点击Button的同时收集到这些有用的数据。
基于这个思路,我们来修改上面的代码:
首先是XAML:
这里把CommandParameter的值作为字符串参数传递到Command的OnSave中。
其次是Command的声明:
this.ClickCommand = new DelegateCommand<string>(OnClick, arg => true);
this.button.DataContext = ClickCommand;
这里使用了string,来作为Command的参数。
5. 上述的代码是基于WPF的,对于Silverlight,由于后者不支持静态类和静态成员,所以要把,而其他部分保持不变:
分别为WPF和Silverlight准备了一个Demo,来验证以上若干文字:WPF版本 Silverlight版本
补充:在具体的项目中,我们可以把”Baobao”替换为数据绑定。不过这就麻烦了,因为button1的Command和CommandParameter都要进行数据绑定,所以要把这两个参数所要绑定的数据抽象为一个实体类Model:
{
public ICommand ClickCommand2 { get; set; }
public string UserName { get; set; }
}
然后将原先的Command声明和数据绑定进行如下修改:
this.button2.DataContext = new Model()
{
ClickCommand2 = new DelegateCommand<string>(OnClick2, arg => true),
UserName = "BaoBao"
};
最后在XAML中的修改就简单了:
修改后的代码下载:WpfPrismApplication1_newversion.zip
(二) 一次执行多个Command
扯了半天,我们所遇到的场景只局限于点击一次按钮然后执行一个Command。我们还有一种需求,就是点击一次按钮,执行一连串的Command。为此,Prism为我们提供了CompositeCommand类来解决这一需求。
CompositeCommand类,从字面上就能看出,它由若干Command组成的。它实现了ICommand接口,就是说,它也具有CanExecute和Execute这两个接口方法。
设想一个场景,点击Button的同时,一次触发两个Command,分别修改TextBlock和TextBox的值:
这是我们要定义一个CompositeCommand,它包括这两个Command。但是,为了能够对其进行单元测试,我们创建了一个静态的代理类,将这个CompositeCommand封装成一个静态属性:
{
public static CompositeCommand MyCompositeCommand = new CompositeCommand();
}
然后在后台代码中进行声明:
{
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的语法:
这里的local定义如下,是对当前项目namespace的一个引用声明:
这里,我们连this.button.DataContext = ClickCommand; 这样的语法也不需要了,因为在xaml的绑定语法中,静态类GlobalCommands事先已经帮我们打理好一切了。
但是,对于Silverlight而言,它是不支持x:static的,所以为了演示上面相同的功能,我们要对刚才的代码进行改造:
首先要把原先的静态代理类作为View的一个属性:
{
get
{
return GlobalCommands.MyCompositeCommand;
}
}
然后在xaml的button中,直接绑定这个属性:
最后,手动设置xaml中button和CompositeCommand之间的绑定关系:
效果图如下所示(点击Button1后的效果):
分别为WPF和Silverlight准备了一个Demo,来验证以上若干文字:WPF版本 Silverlight版本
(三) 一次执行不同View中的多个Command
如果CompositeCommand只是这样,那它就没什么实用价值了。我们看下面这个图:
怎么理解上面这个图呢?让我们把CompositeCommand提升到单独一个项目中,这样就可以让不同项目中的不同View都共享同一套CompositeCommand,注册它们自己的Command。当在其中一个View中点击Button时,所有注册过的Command,即使不在同一个项目中,都会被执行。
举一个最简单的例子,就是注册新用户。要在好几个Tab页面中填写不同的信息,最后点击SaveAll按钮,所有数据一次性全部提交。
这才是CompositeCommand的真正用武之地。
我信手写了一个WPF版本的实现,大家可以参考,从中领悟这其中的深刻思想。代码下载:WpfPrismApplication3.zip。
写完这个Demo,我忽然发现,点击SaveAll按钮后,两个子View都会执行各自的OnClick方法,比如说弹出对话框。但我现在的需求是把这些填入的信息都汇总到Button所在的主View中,从而一次性提交所有数据到数据库。
如何在主View中搜集这些信息呢?这就到了Prism中Event出场的时候了。
关于Prism中的Event机制,读者可以参加我的另一篇文章《Prism研究(for WPF & Silverlight)8.Event机制》。我们知道,在Prism中,Event专门用于在不同View之间传递消息。
于是上面的Demo可以修改为:在主View中subscribe,而在各个子View的OnClick方法中publish。
效果图如下所示,改良过的Demo下载:WpfPrismApplication3_new.zip
(四) 信息不完整就不能执行CompositeCommand
上面的操作有一个问题没有注意,如果有没有填写的信息,那么就不能执行CompositeCommand,这表现为Save All按钮是灰色的、不可点击的。
要解决这一问题,需要做两方面的工作:
1.每当需要检查的数据发生改变,就重新进行Command的CanExecute判断,这需要调用Command的RaiseCanExecuteChanged方法,如下所示:
{
switch (e.PropertyName)
{
case "Company":
case "Address":
this.SaveCommand.RaiseCanExecuteChanged();
break;
default:
break;
}
}
2.在Command的CanExecute方法中严格判断当前Command所包括的数据中是否有不完整的信息,比如说,“公司信息”中就要保证Company和Address不能为空。
{
//first load
if (this.Company == null || this.Address == null)
return false;
if (this.Company.Trim() == "" || this.Address.Trim() == "")
return false;
return true;
}
新版本的代码下载:WpfPrismApplication3_new_new.zip
(五) CompositeCommand的改进
试想一下,如果由你来写一个Command集合,你会提供哪些功能?
1. 类似于Add和Remove的方法,添加和删除集合中的Cmmand
2. 为Command集合也提供一个CanExecute方法,用以检测当前Command集合是否可以执行。在这个方法中,我们检查集合中的所有Command,只要有一个Command的CanExecute方法返回false,那么Command集合的CanExecute方法就返回false。换句话说,必须所有子Command都可以执行,Command集合才可以执行。
3. 为每个Command设计一个方法,当某些条件改变,从而使Command可以执行或不可以再执行时,我们就执行该方法,再次执行CompositeCommand的CanExecute方法。
4. 为每个Command提供一个bool类型的属性,我们可以将这个属性设置为false,从而在执行Command集合的时候,跳过这个Command。如果下次还要执行这个Command,把它的这个属性改为true即可。
针对第1条,Prism为CompositeCommand量身打造了RegisterCommand和UnregisterCommand方法。
针对第2条,因为CompositeCommand也实现了ICommand接口,所以也会实现它的CanExecute方法。
针对第3条,Prism在DelegateCommand中实现了RaiseCanExecuteChanged方法,它会再次调用整个CompositeCommand的CanExecute方法,以判断该CompositeCommand是否可以执行。
针对第4条,Prism提供了IActiveAware接口,定义如下:
/// 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;
}
其中,IsActive就是我们需要的那个bool属性。
DelegateCommand<T>这个泛型类既实现了ICommand接口,又实现了IActiveAware接口。
为了使用这个IsActive属性,我们要使用CompositeCommand类的第2个构造函数,把monitorCommandActivity参数设置为true:
这样,CompositeCommand就会监视其中所有DelegateCommand,监视着它们的IsActive属性,一旦它们发生变化,就会进行判断CompositeCommand的Execute是否可以执行。
如果能这样实现,那么CompositeCommand这个Command集合的功能是非常强大的。
但是,很遗憾,Prism在第2点上出了一点纰漏,就是CompositeCommand的CanExecute方法:
其实只检查集合中每个Command的CanExecute方法返回值就够了——只要查出一个false值就跟着也返回false。但是,Prism在实现上画蛇添足地还检查了每个Command的IsActive属性,如下所示,就是那个ShouldExecute私有方法:
{
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;
}
protected virtual bool ShouldExecute(ICommand command)
{
var activeAwareCommand = command as IActiveAware;
if (this.monitorCommandActivity && activeAwareCommand != null)
{
return activeAwareCommand.IsActive;
}
return true;
}
这样CanExecute方法就受IsActive属性的影响了,而这本应该是两个没关系的成员,换作我,会这么实现CanExecute方法:
{
ICommand[] commandList;
lock (this.registeredCommands)
{
commandList = this.registeredCommands.ToArray();
}
foreach (ICommand command in commandList)
{
if (!command.CanExecute(parameter))
return false;
}
return true;
}
这样就完美了。
新的CompositeCommand类,请在这里下载:CompositeCommand.cs
总要做个示例什么的,来比较修改前后的不同,以证明我的修改是合理的。
那就做一个模拟Visual Studio的例子吧:
看上图,这个是我在自己的Visual Studio上的截图。
我用VS打开了3个文件,并修改了其中2个文件,尚未保存(看见文件OrderModule.cs右上角的那个星号了没,不要说你不知道那是啥意思噢)。当我按下File菜单中的Save All的时候,带星号的两个文件都会被保存,并且星号会消失;而第一个文件,也就是没修改过的OrdersToolBar.xaml.cs不会执行任何操作。
同时,鼠标右击文件标题,会弹出“Close”的上下文菜单,选择后,关闭当前文件。
使用原先的Prism框架提供的CompositeCommand
由于CanExecute方法的错误,我们不能正常使用IActiveAware接口的IsActive属性,所以,就连Prism自带的Demo也没有采用这套机制,于是,采用CompositeCommand的无参构造函数:
这时monitorCommandActivity参数默认为false,从而不会监视DelegateCommand的IsEnabled属性了。
既然没有IsActive属性来判断CompositeCommand中哪些Command可以执行,我们只好另想办法。
偷梁换柱,我们可以使用RegisterCommand和UnregisterCommand方法,把当前不需要执行的Command从CompositeCommand中移除,如果以后还需要这个Command,再使用RegisterCommand方法把它添加进来好了。
这样做唯一的不足就是,CompositeCommand不是固定的,我们要手动维护这个Command集合。
代码下载:CompositeCommandDemo_old.zip
使用我改写的CompositeCommand
这个就比较灵活了。由IsActive来判断当前“文件”是否被修改过,从而在文件标题的右上角显示星号。
对于右击文件标题的“Close”上下文菜单,才是UnregisterCommand方法的真正用武之地,我们在关闭当前文件的时候,也把当前Command从CompositeCommand中移除了。
实现起来很简单,就是把MenuItem的Click事件转换为Command。检查了MenuItem的定义,发现它具有Command属性——这下好了,省得我们自定义AttachedBehavior了,定义如下:
<ContextMenu StaysOpen="True">
<MenuItem Header="Close" Command="{Binding CloseCommand}"/>
</ContextMenu>
</TabItem.ContextMenu>
相应的UserControlViewModel中,声明CloseCommand及其OnClose方法:
public void OnClose(object obj)
{
GlobalCommands.MyCompositeCommand.UnregisterCommand(SaveCommand);
//这里要使用Prism的Event机制通知主View把当前View从TabControl中移除,这里限于条件,就不做下去了
MessageBox.Show("Close TabItem");
}
这个Demo就写到这里吧,再写下去,就做出来一个Visual Studio了。
代码下载:CompositeCommandDemo_new.zip
总结:这个case告诉我们,不要迷信老外写的Code,要有自己的思考,批评地继承国外好的东西。
此外,对于Command的实现,Prism文档还提供了另一种方法,就是将其存储在XAML的Resource中,我们已经在《Command之必杀技——AttachedBehavior》一文中看到了它的妙用。