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接口,如下所示: 

    // 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返回类型的方法。

      在Prism中,提供了两个Command,分别是DelegateCommand和CompositeCommand,它们都派生于ICommand接口,它们在Prism中的位置如下图所示:

      clip_image002

      分别讨论如下:


      (一) 一次执行一个Command

      这是由DelegateCommand来实现的。

      clip_image004

      如图所示,我们先声明一个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 { getprivate set; }

      这也从侧面说明了CanExecute和Execute两个方法只能在Command的构造函数中初始化。

      3. 出于惰性声明的思想,我们将Command声明为ICommand类型,而在构造函数中将其实例化为具体的类型,于是大家常常会看到这样的语句: 

      public DelegateCommand<object> ClickCommand { getprivate set; }

      //以下实例化语句出现在其它方法中,也就是需要实例化的时候
      ClickCommand = new DelegateCommand<object>(OnClick, arg => 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 { getprivate 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,来验证以上若干文字:WPF版本      Silverlight版本

      补充:在具体的项目中,我们可以把”Baobao”替换为数据绑定。不过这就麻烦了,因为button1的Command和CommandParameter都要进行数据绑定,所以要把这两个参数所要绑定的数据抽象为一个实体类Model: 

        public class Model
        {
            
public ICommand ClickCommand2 { getset; }
            
public string UserName { getset; }
        }

      然后将原先的Command声明和数据绑定进行如下修改: 

            public Model Model { getset; }

            
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>

      修改后的代码下载:WpfPrismApplication1_newversion.zip

      
      (二) 一次执行多个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 getprivate set; }

        
public ICommand ClickCommand2 getprivate 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后的效果):

      clip_image006

      分别为WPF和Silverlight准备了一个Demo,来验证以上若干文字:WPF版本      Silverlight版本


      (三) 一次执行不同View中的多个Command

      如果CompositeCommand只是这样,那它就没什么实用价值了。我们看下面这个图:

      clip_image008

      怎么理解上面这个图呢?让我们把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

      clip_image010


      (四) 信息不完整就不能执行CompositeCommand

      上面的操作有一个问题没有注意,如果有没有填写的信息,那么就不能执行CompositeCommand,这表现为Save All按钮是灰色的、不可点击的。

      要解决这一问题,需要做两方面的工作:

            1.每当需要检查的数据发生改变,就重新进行Command的CanExecute判断,这需要调用Command的RaiseCanExecuteChanged方法,如下所示:

        void OnViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
        
{
            
switch (e.PropertyName)
            
{
                
case "Company":
                
case "Address":
                    
this.SaveCommand.RaiseCanExecuteChanged();
                    
break;
                
default:
                    
break;
            }

        }

            2.在Command的CanExecute方法中严格判断当前Command所包括的数据中是否有不完整的信息,比如说,“公司信息”中就要保证Company和Address不能为空。 

        public bool CanSave(object obj)
        
{
            
//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接口,定义如下: 

    /// <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 getset; }

        
/// <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 MyCompositeCommand = new CompositeCommand(true);

      这样,CompositeCommand就会监视其中所有DelegateCommand,监视着它们的IsActive属性,一旦它们发生变化,就会进行判断CompositeCommand的Execute是否可以执行。
      如果能这样实现,那么CompositeCommand这个Command集合的功能是非常强大的。

      但是,很遗憾,Prism在第2点上出了一点纰漏,就是CompositeCommand的CanExecute方法:

      其实只检查集合中每个Command的CanExecute方法返回值就够了——只要查出一个false值就跟着也返回false。但是,Prism在实现上画蛇添足地还检查了每个Command的IsActive属性,如下所示,就是那个ShouldExecute私有方法: 

        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;
        }


        
protected virtual bool ShouldExecute(ICommand command)
        
{
            var activeAwareCommand 
= command as IActiveAware;
            
if (this.monitorCommandActivity && activeAwareCommand != null)
            
{
                
return activeAwareCommand.IsActive;
            }


            
return true;
        }

      这样CanExecute方法就受IsActive属性的影响了,而这本应该是两个没关系的成员,换作我,会这么实现CanExecute方法: 

        public virtual bool CanExecute(object parameter)
        
{
            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的例子吧:

      clip_image012

      看上图,这个是我在自己的Visual Studio上的截图。

      我用VS打开了3个文件,并修改了其中2个文件,尚未保存(看见文件OrderModule.cs右上角的那个星号了没,不要说你不知道那是啥意思噢)。当我按下File菜单中的Save All的时候,带星号的两个文件都会被保存,并且星号会消失;而第一个文件,也就是没修改过的OrdersToolBar.xaml.cs不会执行任何操作。

      同时,鼠标右击文件标题,会弹出“Close”的上下文菜单,选择后,关闭当前文件。

      使用原先的Prism框架提供的CompositeCommand

      由于CanExecute方法的错误,我们不能正常使用IActiveAware接口的IsActive属性,所以,就连Prism自带的Demo也没有采用这套机制,于是,采用CompositeCommand的无参构造函数: 

      CompositeCommand MyCompositeCommand = new CompositeCommand(true);

      这时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了,定义如下: 

    <TabItem.ContextMenu>
        
<ContextMenu StaysOpen="True">
            
<MenuItem Header="Close" Command="{Binding CloseCommand}"/>
        
</ContextMenu>
    
</TabItem.ContextMenu>

      相应的UserControlViewModel中,声明CloseCommand及其OnClose方法: 

        CloseCommand = new DelegateCommand<object>(OnClose, args => true);

        
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》一文中看到了它的妙用。

posted @ 2009-10-15 00:13  包建强  Views(10189)  Comments(11Edit  收藏  举报