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

  • 相关阅读:
    MOSS中的User的Title, LoginName, DisplayName, SID之间的关系
    如何在Network Monitor中高亮间隔时间过长的帧?
    SharePoint服务器如果需要安装杀毒软件, 需要注意什么?
    如何查看SQL Profiler? 如何查看SQL死锁?
    什么是Telnet
    The name or security ID (SID) of the domain specified is inconsistent with the trust information for that domain.
    Windows SharePoint Service 3.0的某个Web Application无搜索结果
    网络连接不上, 有TCP错误, 如果操作系统是Windows Server 2003, 请尝试一下这里
    在WinDBG中查看内存的命令
    The virtual machine could not be started because the hypervisor is not running
  • 原文地址:https://www.cnblogs.com/Jax/p/1583593.html
Copyright © 2011-2022 走看看