zoukankan      html  css  js  c++  java
  • 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》一文中看到了它的妙用。

  • 相关阅读:
    解决ecshop进入后台服务器出现500的问题
    Java8新特性(拉姆达表达式lambda)
    使用Optional优雅处理null
    Arrays.asList 存在的坑
    Java提供的几种线程池
    冒泡排序及优化详解
    如何让MySQL语句执行加速?
    关于https的五大误区
    127.0.0.1和0.0.0.0地址的区别
    宽带网络技术-大题重点
  • 原文地址:https://www.cnblogs.com/Jax/p/1583593.html
Copyright © 2011-2022 走看看