[WPF] Command 之简览

1. Command 的简述

这一节可看可不看, 类似传销。

1.1. 其作用

    简而言之,Command就是响应用户 键盘快捷键输入或者控件事件 (如button点击, 工具条,菜单栏等等),
从而完成如复制、黏贴、打印等操作的一个过程。

1.2. 为什么选择Command

    如果你之前是开发MFC界面程序,  并第一次接触WPF, 你肯定会说, 那不就是消息处理吗。就算是WPF
中,事件(牛人们都说事件是消息的封装,我还没搞过原理, 所以不能解释更多)不也能轻松搞定这些工作吗。
    你的理解是对的, 至少和我的理解是一样的, 事件能搞定Command想要做的所有工作。但是如果你知道
了使用Command的好处的话, 你就会觉的 那我们还是多用Command吧。
    
    我举几个说明好处的例子,Command都是实现了ICommand的类的对象, 如RoutedCommand、RoutedUICommand类. 那么这些类必须实现
其中的三个成员: 一个事件CanExecuteChanged, 两个函数CanExecute及Execute.
  • Execute是用于执行具体的逻辑, 比如具体怎么打印(创建一个打印框实例等等)。
  • CanExecuteChange 和 CanExecute是控制 发出命令的控件的状态,如Undo工具按钮,如果你什么都没有编辑过的话,这个按钮就应该是无法使用的状态。如果你要控制这个状态, 代码非常简单, 只需要将CanExecute函数返回false (按钮无法使用)或true(按钮可用)就可以了。
    并且还有一个非常大的好处, 就是实现一个Command就可以直接关联到多个发命令的控件, 如一个Command可以同时关联菜单栏中的打印按钮, 工具条中的打印按钮以及快捷键。这些只需要简单的关联操作就可以了, 从而使你不必在Execute或CanExecute中做 任何判断命令来源的逻辑。如果你开发过MFC, 如果是不同的消息源发出的命令消息, 处理的时候必须对消息的来源进行判断, 然后进行具体的业务逻辑处理。
    Command的好处就是上面的两点, 一个是Execute和CanExecute函数只需处理业务逻辑, 不需要处理控件状态、 判断命令来源等繁琐的事情;一个是可以通过非常简单的操作关联多个命令源。 那么如果是使用事件来实现呢? 你可以想到就没有这两个好处了, 很多情都需要你自己做了。

1.3. 一个命令有那几部分组成

这段完全是引用自《深入浅出WPF》这本书的。其实只要记住了这几个组成部分,再看些源代码,就至少能初步理解命令了(我现在就是这个水平)。 

1) 命令(Command):WPF的命令实际上就是实现了ICommand接口的类,平时使用最多的是RoutedCommand类。

2) 命令源(Command Source):即命令的发送者,是实现了ICommandSource接口的类。很多界面元素都实现了这个接口,包括Button、MenuItem、ListBoxItem等。

3) 命令目标(Command Target):即命令将发送给谁,或者说命令将作用在谁身上。命令目标必须是实现了IInputElement接口的类。

4) 命令关联(Command Binding):负责把一些外围逻辑与命令关联起来,比如执行之前对命令是否可以执行进行判断、命令执行之后还有哪些后续工作等。

2. 三类 Command

    在我看来命令一共有三类, 
  • 一类就是WPF已经定义好了的, 我们只需要直接拿来用就可以了,如ApplicationCommands, EditingCommands等等类中定义了很多复制,黏贴,打印等等命令。这些命令都是RoutedUICommand。
  • 一类是自定义的,直接创建RoutedCommand或RoutedUICommand的实例。
  • 还有一类也是自定义的, 自己实现ICommand接口。
NOTE:
    RoutedUICommand是继承自RoutedCommand的, 多一个Text 属性, 这个属性在菜单项MenuItem中能自动显示为MenuItem中的显示文本。 如打印菜单项,如果关联了ApplicationCommands.Print命令的话, 会自动出现 "打印" 文本。简言之RoutedUICommand就是不需要自己动手实现给控件设置文本, 却没有实际业务逻辑的事情了。

2. 三类Command的介绍,实现及使用

2.1. 基础命令

2.1.1. 介绍

    基础命令, 这是我的叫法, 有其他叫法的人忽略就可以了。

基础命令指的是WPF已经给我们定义好了的命令, 类似已经定义好了的事件(MouseDown)。

主要分为以下几类:

  • ApplicationCommands 主要是一些 大家常用的命令, 如打印,复制, 剪贴, Undo, Redo, 查找等等。
  • ComponentCommands 主要是滚动页面, 选择内容等命令。
  • EditingCommands 主要是编辑文本或富文本的命令, 如改变字体,文本对齐, 移动光标等等。
  • MediaCommands 主要是播放相关的命令,开始播放, 暂停, 停止等等。
  • NavigationCommands 主要是类似浏览器中的上页 下页, 暂停刷新, 放大缩小等等。

2.1.2 例子

这些已经定义的命令就不需要我们再定义或实现了, 直接拿来用就可以了。
所以请看下面一个简单的使用例:
  • 先来看下Xaml的代码

        
        
            
        
        
        
            
                
                
                
            
        
        

  • 再来看下后台代码
        public void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            //设置成true, 其实最终就是RoutedCommand的CanExcute函数返回true.
            //所以结果就是命令源一直可以使用
            if (null != textBox && textBox.Text.Length > 0)//有内容才允许打印
                e.CanExecute = true;
            else //如果 e.CanExecute = false的话命令源就不能使用
                e.CanExecute = false;
        }

        public void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            //具体的打印逻辑
            MessageBox.Show("我要打印了");
        }

2.1.3. 例子代码分析

  • 首先是打印命令Command绑定到命令源控件, 即
MenuItem Command="{x:Static ApplicationCommands.Print}"
  • 然后制定命令目标,这里请注意, 命令目标是命令源的成员,如下:
CommandTarget="{Binding ElementName=textBox}"
  • 然后创建一个命令绑定对象, 将命令放入该对象中,然后给CanExecute和Executed事件添加对应的事件处理函数, 最后把该命令绑定对象放入命令目标控件的外围控件中,外围控件即命令目标的外层控件(父控件,或父控件的父控件...类推)。 这个就相当于命令监控, 当捕获到命令的时候, 通过事件函数执行业务逻辑。如下:
     
  • 定义CanExecute和Executed这两个事件的事件函数,请参考之前的后台代码。
    了解了这些主要的步骤之后, 很容易就能掌握怎么使用基础命令, 所以如果有兴趣,请尝试下看看。

2.2. 自定义命令 之RoutedCommand

2.2.1. 例子

    这个就先直接看例子。
  • 先创建一个类似WPF提供的标准命令的 自定义命令
    class SelfDefineCommand1
    {
        /// 
        /// 这就是自定义的命令了, 其实就是创建了一个RoutedCommand的静态对象
        /// 
        public static RoutedCommand MyFirstCommandForPrint = new RoutedCommand("MyCommand", typeof(SelfDefineCommand1), 
            new InputGestureCollection() { new KeyGesture(Key.P, ModifierKeys.Control) });
    }
  • 像使用WPF那些标准命令一样, 在Xaml中使用这些命令
	    
                
                
                    
                
                
                    
                        
                            
                            
                        
                    
                    
                
            

2.2.2. 该小结总结

    从代码中可以看出, 所谓的自定义 实际上就是创建了一个类似WPF提供的命令,然后使用也是和使用WPF标准命令是一样的。

2.3. 自定义命令 之实现ICommand

2.3.1. 介绍

    通过查看RoutedCommand的类型申明,你会发现RoutedCommand就是实现了ICommand接口的类。
也就是说,我们也可以自己定义一个实现了ICommand接口的类,然后来替代RoutedCommand.
但是你肯定会问, 既然RoutedCommand已经实现了, 为什么我们自己还要去定义这样一个类呢?
这个问题不在这里表述, 大家有兴趣的话可以去看看 深入浅出WPF 的作者关于MVVM的介绍。
    这一节我们就介绍怎么定义这个类, 并且怎么使用这个类。

2.3.2. 实现ICommand的自定义命令类

请参见代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace CommandDemo
{
    class SelfDefineCommand2 : ICommand
    {
        //CommandManager.RequerySuggested WPF底层代码自动调用,比如
        //MenuItem显示时, TextBox聚焦时, 最终通过CanExecute返回的值设置
        //控件是否可用的状态
        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
        //见CanExcuteChanged注释
        public bool CanExecute(object parameter)
        {
            TextBox textBox = parameter as TextBox;
            if(null != textBox && textBox.Text.Length > 0)//存在内容, 允许打印
            {
                return true;
            }
            else//不存在内容,不允许打印
            {
                return false;
            }
        }
        //执行具体的业务逻辑
        public void Execute(object parameter)
        {
            TextBox textBox = parameter as TextBox;
            if(null != textBox)
            {
                MessageBox.Show(textBox.Text);
            }
        }
        /// 
        /// 自定义一个静态的SelfDefineCommand2对象
        /// 
        public static SelfDefineCommand2 MyCommandForPrint2 = new SelfDefineCommand2();
    }
}
    如代码所示, SelfDefineCommand2类实现了ICommand接口的CanExecute和Execute函数, 以及CanExecuteChanged事件,具体解释清参见代码注释。
除了实现ICommand接口以外, 还创建了SelfDefineCommand2的静态实例, 你可以发现这一步和RoutedCommand的自定义实例是一样的,并且实际上怎么使用也是
差不多的。
    关于怎么使用请参见下节。

2.3.3 怎么使用这个自定义类

请参见代码:
	    
                
                    
                        
                            
                            
                        
                    
                    
                
            
  • 将命令绑定到命令源控件
  • 指定命令目标控件,这里这个暂时没用。以后想到使用场景了再补充 。
CommandTarget="{Binding ElementName=textBox3}" 
  • 命令参数, 将命令目标赋值给parameter对象, 这样CanExecute和Execute中就可以得到这个命令目标, 并进行相应的业务逻辑处理了。
CommandParameter="{Binding ElementName=textBox3}
  • 大家可能也发现了, 为什么没有CommandBinding监听器了? 这个在这里简单提一下, 因为MenuItem在触发命令的时候直接执行Execute函数了, 并没有再去执
行发送事件等操作, 实际上CommandBinding拦截到的是一个事件; 所以现在因为不需要发送事件了, 也就不需要事件监听器了。 具体的请参见第3节的原理分析。

2.3.4 该小节总结

    直接实现ICommand的方式, 实际上和创建RoutedCommand命令对象非常类似,主要差异在于需要自己实现ICommand接口, 并且不需要CommandBinding监听器。
因为没有了监听器, 所以也不需要事件处理函数了。

3. 三类Command的原理分析

3.1. 分析 - 基础命令及RoutedCommand自定义命令

    因为基础命令和自定义的RoutedCommand命令对象, 实际上都是RoutedCommand命令对象, 所以在使用方式 及 原理上都是完全一样的。所以在原理分析的时候
直接一起分析了。如下
  • 命令的最早的触发点, 以Button控件为例。
        protected virtual void OnClick()
        {
            RoutedEventArgs newEvent = new RoutedEventArgs(ButtonBase.ClickEvent, this);
            RaiseEvent(newEvent);

            MS.Internal.Commands.CommandHelpers.ExecuteCommandSource(this);
        }
    这个是Button父类的OnClick函数, Button.OnClick()总是会调用父类的Onclick函数,并在父类OnClick函数中调用下面这个函数
CommandHelpers.ExecuteCommandSource(this)
这句代码就是WPF内部模块的代码, ExecuteCommandSource函数里最终执行了RoutedCommand的ExecuteCore
  • 如下是CommandHelpers类的代码
	[SecurityCritical, SecurityTreatAsSafe]
        internal static void ExecuteCommandSource(ICommandSource commandSource)
        {
            CriticalExecuteCommandSource(commandSource, false);
        }
        [SecurityCritical]
        internal static void CriticalExecuteCommandSource(ICommandSource commandSource, bool userInitiated)
        {
            ICommand command = commandSource.Command;
            if (command != null)
            {
                object parameter = commandSource.CommandParameter;
                IInputElement target = commandSource.CommandTarget;

                RoutedCommand routed = command as RoutedCommand;
                if (routed != null)
                {
                    if (target == null)
                    {
                        target = commandSource as IInputElement;
                    }
                    if (routed.CanExecute(parameter, target))
                    {
                        routed.ExecuteCore(parameter, target, userInitiated);
                    }
                }
                else if (command.CanExecute(parameter))
                {
                    command.Execute(parameter);
                }
            }
        }
你一定看到了CriticalExecuteCommandSource函数中的这个代码片段,
                if (routed != null)
                {
                    if (target == null)
                    {
                        target = commandSource as IInputElement;
                    }
                    if (routed.CanExecute(parameter, target))
                    {
                        routed.ExecuteCore(parameter, target, userInitiated);
                    }
                }
routed局部变量就是RoutedCommand的对象引用,意思就是如果command对象的类型是RoutedCommand的话, 就执行RoutedCommand的ExecuteCore,
那么接下去就明朗了, 我们只需要看看RoutedCommand的ExecuteCore中到底做了些什么就可以了。
  • RoutedCommand类ExecuteCore函数代码
        [SecurityCritical]
        internal bool ExecuteCore(object parameter, IInputElement target, bool userInitiated)
        {
            if (target == null)
            {
                target = FilterInputElement(Keyboard.FocusedElement);
            }

            return ExecuteImpl(parameter, target, userInitiated);
        }
	[SecurityCritical]
        private bool ExecuteImpl(object parameter, IInputElement target, bool userInitiated)
        {
            // If blocked by rights-management fall through and return false
            if ((target != null) && !IsBlockedByRM)
            {
                UIElement targetUIElement = target as UIElement;
                ContentElement targetAsContentElement = null;
                UIElement3D targetAsUIElement3D = null;

                // Raise the Preview Event and check for Handled value, and
                // Raise the regular ExecuteEvent.
                ExecutedRoutedEventArgs args = new ExecutedRoutedEventArgs(this, parameter);
                args.RoutedEvent = CommandManager.PreviewExecutedEvent;
                
                if (targetUIElement != null)
                {
                    targetUIElement.RaiseEvent(args, userInitiated);
                }
                else
                {
                    targetAsContentElement = target as ContentElement;
                    if (targetAsContentElement != null)
                    {
                        targetAsContentElement.RaiseEvent(args, userInitiated);
                    }
                    else
                    {
                        targetAsUIElement3D = target as UIElement3D;
                        if (targetAsUIElement3D != null)
                        {
                            targetAsUIElement3D.RaiseEvent(args, userInitiated);
                        }
                    }                    
                }

                if (!args.Handled)
                {
                    args.RoutedEvent = CommandManager.ExecutedEvent;
                    if (targetUIElement != null)
                    {
                        targetUIElement.RaiseEvent(args, userInitiated);
                    }
                    else if (targetAsContentElement != null)
                    {
                        targetAsContentElement.RaiseEvent(args, userInitiated);
                    }
                    else if (targetAsUIElement3D != null)
                    {
                        targetAsUIElement3D.RaiseEvent(args, userInitiated);
                    }
                }

                return args.Handled;
            }

            return false;
        }
    可以看到最终是调用了ExecuteImpl函数, 这个函数中主要看如下代码片段:
		ExecutedRoutedEventArgs args = new ExecutedRoutedEventArgs(this, parameter);
                args.RoutedEvent = CommandManager.PreviewExecutedEvent;
                
                if (targetUIElement != null)
                {
                    targetUIElement.RaiseEvent(args, userInitiated);
                }
    简而言之, 就是命令目标控件发送了一个 PreviewExecutedEvent事件,最终来触发CommandBinding监听器的Executed事件处理函数。这也是为什么RoutedCommand需要
CommandBinding监听器的理由, 就是为了拦截事件的。

  • NOTE:
    PreviewExecutedEvent事件触发以后, 最终会触发ExecutedEvent。

3.2. 分析 - 自定义命令 之实现ICommand

    如果仔细看了RoutedCommand的原理的话, 你也就知道为什么Command不需要CommandBinding了。
其关键还是在CriticalExecuteCommandSource函数上,如下:
        [SecurityCritical]
        internal static void CriticalExecuteCommandSource(ICommandSource commandSource, bool userInitiated)
        {
            ICommand command = commandSource.Command;//命令源中去处Command对象
            if (command != null)
            {
                object parameter = commandSource.CommandParameter;
                IInputElement target = commandSource.CommandTarget;

                RoutedCommand routed = command as RoutedCommand;//强转Command对象为RoutedCommand对象
                if (routed != null)
                {
                    if (target == null)
                    {
                        target = commandSource as IInputElement;
                    }
                    if (routed.CanExecute(parameter, target))
                    {
                        routed.ExecuteCore(parameter, target, userInitiated);
                    }
                }
                else if (command.CanExecute(parameter))//如果Command对象不为RoutedCommand类型的话,就直接执行Command中的Execute.
                {
                    command.Execute(parameter);
                }
            }
        }
    请关注代码中注释的地方, 关键在于 " 如果Command对象不为RoutedCommand类型的话,就直接执行Command中的Execute" 这句话, 这样也就不会发送事件了。
所以自定义的实现了ICommand接口的Command类,和RoutedCommand之间的差别也就在这里, 就是需不需要CommandBinding的区别。

好, 至此关于Command, 已经粗浅的讲完了。在以后的学习过程中,如果还有进一步的理解的话, 会补充到新的篇章中。

这是鄙人第一次写博客, 以前总觉的没什么好写的, 因为本身也没多少技术能力。 现在因为开始学习WPF, 所以强压着自己开始写博客 以巩固自己的知识, 同时也起到和大家相互探讨的目的。

另外, 如果文章中有任何的错误, 或理解不对的地方, 请各位赐教, 谢谢。

4. Command进阶 之Control 內建命令

这个主题也是Command中的其中一部分, 就是类似TextBox这种编辑命令, 如复制,黏贴等, 在TextBox内部就已经实现了业务逻辑的命令。
这部分就不在这里描述了, 否则篇幅太长, 内容太杂, 不易于大家阅读。


你可能感兴趣的:(WPF)