[Prism]Composite Application Guidance for WPF(9)——命令

                  [Prism]Composite Application Guidance for WPF(9)——命令

                                               周银辉

     这里的“命令”即Command模式中的“Command”,几乎每个应用程序都有该模式的运用,如何“复制”“粘贴”“撤销”等操作。我们知道,该模式将操作的请求者和操作的执行逻辑隔离开来,并且其对请求排队以及撤销重复等操作有着良好的支持,所以被广泛应用。而WPF将其做了进一步的封装和改进,使得WPF程序能够很容易地使用命令和打造自定义命令,另外,WPF内置的数十种常用命令以及先进的命令路由模式(Routed)使得这一切显得那么容易和高效。

     那么是不是WPF命令(RoutedCommand和RoutedUICommand)足以解决一切情况了呢?并非如此,事实上其有着许多的不足,这也就是为什么Prism要打造一套自己的Command的原因。本文将简单探讨一下这些问题。

 

-------------------------------WPF 的 Command----------------------------

1,理解WPF RoutedCommand中的“Routed”(路由)
     WPF提供了RoutedCommand和RoutedUICommand两种命令,其中RoutedUICommand继承于RoutedCommand,翻开MSDN,我们可以看到这样的解释“其定义一个实现 ICommand 并通过元素树路由的命令,RoutedCommand 上的 Execute 和 CanExecute 方法不包含命令的应用程序逻辑(例如,一个典型的 ICommand 就是这样),而是将引发遍历元素树的事件以查找具有 CommandBinding 的对象。 附加到 CommandBinding 的事件处理程序包含命令逻辑。” 这就是所谓的“路由”:其会在特定的元素树(实际是视觉树)路径上查找CommandBinding,然后去调用CommandBinding的CanExecute 和Execute 来判断是否可执行以及如何执行命令。那么该路径是怎样的呢,请看下面的例子:
      
上面两幅图中的各按钮代码形如  
<Button Command="Copy" Content="{Binding Path=Command.Text, RelativeSource={RelativeSource Self}}"/>
首先看第一幅图,当文本框(TextBox)中的文本被选中时,工具条上的“复制”按钮被启用了,而文本框右边的按钮却没有被启用;
在第二幅图中,FlowDocument中的文本被选中时,FlowDocument中的按钮和工具栏上饿按钮都被启用了。而文本框旁边的按钮始终都得不到启用。
为什么会这样呢?
 
观察程序中涉及到的主要元素树(省略了不重要的),在这些元素中有设置针对ApplicationCommands.Copy的CommandBinding的元素分别是“文本框”和“FlowDocument(实际是其Viewer)”当“文本框右侧按钮”的Command设置为Copy后,它从其所在位置沿着元素树向根方向查找有针对ApplicationCommands.Copy的CommandBinding的元素,可惜,没找到,所以该按钮始终被禁用。同理,FlowDocument中的按钮向元素树根方向查找,Ok,其找到了FlowDocumentPageViewer上有针对ApplicationCommands.Copy的CommandBinding,所以其会去调用FlowDocumentPageViewer的CommandBinding中的CanExecute 和Execute 来判断是否可执行以及如何执行命令。但是,“工具栏按钮”为何能被启用呢,这是因为沿着元素树查找CommandBinding的查找方向除了上述的由元素向根方向上查找外还可以反过来:从元素根(这里也就是我们的“窗口”)向元素方向查找,而工具栏上的按钮便是采用的这种特殊方式。至于何时采用何种方式:当将元素位于工具栏、菜单栏 或者 元素的FocusManager.IsFocusScope设置为"True"时则采用后一种方式,否则默认前一种。
如果我们这样设置一下“文本框右侧的按钮”<Button FocusManager.IsFocusScope="True" Command="Copy" Content="{Binding Path=Command.Text, RelativeSource={RelativeSource Self}}"/>, 再看看效果(文本框右侧的按钮可以被启用了):

RoutedCommand局限之一:
     当软件UI很复杂时其路由方式会让开发人员显得头晕,易出错,尤其是当命令元素不在工具栏或菜单栏时。一种折中的方式是设计Command的CommandTarget属性,明确指定其命令作用的目标。但这在Composite Application中似乎不太容易做到,除非View将命令目标暴露出来或提供相应的API,况且到底要暴露哪些呢,不得而知,因为你不知道其他模块会将你的哪些元素作为CommandTarget。

 

2,理解WPF 的CommandBinding
     CommandBinding扮演着牵线搭桥的红娘角色。其将Command,CanExecute(实际的判定逻辑) 和Execute(实际的执行逻辑)联系起来,当外界有指定Command挂接到实际逻辑所在的类时,其便CanExecute和Execute注册到指定的命令。在WPF中CommandBindings属性以便将CommandBinding添加到其中的类型有:UIElement,ContentElement, UIElement3D,说白了,全是可视元素。为啥?因为WPF内置支持的是RoutedCommand,不是可视化元素就无所谓Routed。即便是使用CommandManager.RegisterClassCommandBinding(Type type, CommandBinding commandBinding)方法,你传入的仍然应该是上述三中可视元素。那么,这样看来,WPF的CommandBinding是严重和View层元素耦合在一起的,你不能直接将Command的CanExecute和Execute直接挂接到其他非UI层的类上。

RoutedCommand局限之二:
     其和UI元素耦合在一起,你不能直接将Command的CanExecute和Execute直接挂接到其他非UI层的类上。这为采用MVC、MVP等模式的应用程序带来了不便,除非你在顶级窗口或View的根上对每一个Command使用CommandBinding在View层添加一个CanExecute和Execute处理函数,该处理函数再调用其他层的相应方法。

 

3,组合命令
     组合命令是时常被用到的,比如在编辑文本时,撤销堆栈中可能已经存储了数十步操作,这是应用程序也许会提供一个“全部撤销”按钮来撤销所有的这些操作。(注意,可能你会误认为这仅仅是对多个撤销操作的For循环,非也,当点击“全部撤销”按钮以后再点击“重复”按钮观察一下便知。其实际是“组合模式”的实现)。但可惜的是WPF内置的RoutedCommand却不支持命令组合

RoutedCommand局限之三
     不支持组合模式。这为复杂UI的操作带来不便,比如界面上有2个文本框以及各自对应一个Save按钮,你可以分别点击各自的Save按钮来保存相应文本框的文本。但如果菜单栏想添加一个“SaveAll”按钮却不是那么容易的事情。

 

4,为一个Command挂接多个CanExecute和Execute逻辑
     WPF的RoutedCommand是不支持多个CanExecute和Execute逻辑挂接的,比如说,你不能调用一次命令来将一段文本同时粘贴到两个文本框中。RoutedCommand一次只会调用一个处理器(比如两个文本框中的当前焦点获得者)。这也就是为什么在采用其默认路由方式进行CommandBinding查找时你不用担心同时查找到两个的原因了。     

RoutedCommand局限之四
     不能挂接多个处理逻辑。而多个处理逻辑的挂接往往在复杂UI中很有用的,这也就是为啥不少厂商会书写自己的命令模式来满足自己的特定需求。

 更多的关于WPF Command可以参考这里 WPF中的命令与命令绑定(一) WPF中的命令与命令绑定(二)

 

-------------------------------打造自己的Command----------------------------

自定义Command相对比较简单,主要在于两点:一是找一个地方存储外面挂接进来的CanExecute和Execute代理,二是何时引发CanExecuteChanged事件。后者非常重要,也容易出错,如果CanExecuteChanged没有正确引发将导致在决策是否应该执行Command的Execute方法时出现错误,在UI上最直观的表现是UI元素的禁用和启用状态不正常。

下面的代码打造了一个可以挂接多个CanExecute和Execute代理的命令,也就是说你可以向其注册的多个处理方法将在命令被Execute时依次调用:
    public class GreetingsCommand : ICommand
    {
        private Func<bool> canExecuteHandler = () => false;
        private Action<string> executeHandler = obj => {};
        public event EventHandler CanExecuteChanged = (sender, e) =>{};

        public event Func<bool> CanExecuteHandler
        {
            add
            {
                canExecuteHandler += value;
                CanExecuteChanged(this, EventArgs.Empty);
            }
            remove
            {
                canExecuteHandler -= value;
                CanExecuteChanged(this, EventArgs.Empty);
            }
        }

        public event Action<string> ExecuteHandler
        {
            add
            {
                executeHandler += value;
            }
            remove
            {
                executeHandler -= value;
            }
        }

        public void Execute(object parameter)
        {
            Delegate[] delegates = executeHandler.GetInvocationList();
           
            foreach(Action<string> act in delegates)
            {
                act.Invoke(parameter != null ? parameter.ToString() : string.Empty);
            }
        }

        public bool CanExecute(object parameter)
        {
            Delegate[] delegates = canExecuteHandler.GetInvocationList();

            foreach (Func<bool> fun in delegates)
            {
                if (fun.Invoke())
                {
                    return true;
                }
            }

            return false;
        }


        public void RaiseCanExecuteChanged()
        {
            CanExecuteChanged(this, EventArgs.Empty);
        }

    }
     注意到这里有一个公开的RaiseCanExecuteChanged()方法,其目的是让外不调用者来手动引发CanExecuteChanged事件,比如当Presenter发现Model中的数据发生改变后,Presenter将引发该事件,以便界面元素更改其状态:
        void Model_PropertyChanged(object sender,PropertyChangedEventArgs e)
        {
            if(string.Equals(e.PropertyName,"Greetings"))
            {
                Greetings.RaiseCanExecuteChanged();
            }
           
        }


     完整的代码下载 


 

-------------------------------Prism 的 Command----------------------------

     P&P小组也看到了WPF Command的诸多限制,特别是与UI元素耦合以及不支持命令组合,所以他们在Prism中便增加了另外一套Command:DelegateCommand和CompositeCommand。
     DelegateCommand:实现了WPF的ICommand接口,仍只支持一个CanExecute和Execute挂接,但其实现一个称为IActiveAware的接口用于指示是否处于集合状态,非激活状态的DelegateCommand始终得不到执行。
     CompositeCommand:也是WPF的ICommand接口的一个实现,但其同时也是DelegateCommand的组合,可以向其中注册或取消注册DelegateCommand,当其中所有处于激活状态的内置DelegateCommand都可以被执行时其CanExecute才返回true。当CompositeCommand被执行时其内部的DelegateCommand将被依次执行(如果可执行的话)
     DelegateCommand和CompositeCommand的语法层面的东西这里就略过不讲了。
     另外,DelegateCommand和CompositeCommand并不是RouteCommand的替代品,而是强有力的补充,RouteCommand的路由机制让我们不必关心目前用户焦点在哪里,况且,WPF内置的数十种常用Command为我们节约了不少开发时间。

 

 

 

 

 

posted @ 2008-10-25 17:21  周银辉  阅读(5300)  评论(3编辑  收藏  举报