[WPF] Command 之简览

本文介绍了WPF中的Command,包括其作用、为什么使用以及Command的组成部分。详细讲解了基础命令、自定义RoutedCommand和实现ICommand的自定义命令,通过实例展示了它们的使用和实现方式。最后,对三类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的代码
<StackPanel>
        <!--这可以理解成命令的监听器,
        * 当监听到这个命令的时候会执行Executed事件处理函数,
        * CanExcute是命令源控件显示时就会执行的函数, 如果命令源一直处于可见状态,
        消息就会持续出发,即函数持续执行-->
        <StackPanel.CommandBindings>
            <CommandBinding Command="{x:Static ApplicationCommands.Print}" CanExecute="CommandBinding_CanExecute" Executed="CommandBinding_Executed"></CommandBinding>
        </StackPanel.CommandBindings>
        
        <Menu HorizontalAlignment="Left" Height="25" Margin="189,107,0,0" VerticalAlignment="Top" Width="75">
            <MenuItem Header="菜单" >
                <!--命令源 绑定打印命令, 点击命令源时 出发该命令-->
                <!--命令目标好像实际的作用比较小, 实际上点击命令源之后,真正是通过调用RaiseEvent出发
                一个消息传递的, 所以在CanExecute或Executed事件处理函数中真正的OriginalSource是命令目标-->
                <MenuItem Command="{x:Static ApplicationCommands.Print}" CommandTarget="{Binding ElementName=textBox}"></MenuItem>
            </MenuItem>
        </Menu>
        <TextBox x:Name="textBox" Height="126" TextWrapping="Wrap" Text="TextBox"/>
</StackPanel>

  • 再来看下后台代码

        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事件添加对应的事件处理函数, 最后把该命令绑定对象放入命令目标控件的外围控件中,外围控件即命令目标的外层控件(父控件,或父控件的父控件...类推)。 这个就相当于命令监控, 当捕获到命令的时候, 通过事件函数执行业务逻辑。如下:
<StackPanel.CommandBindings>
     <CommandBinding Command="{x:Static ApplicationCommands.Print}" CanExecute="CommandBinding_CanExecute" Executed="CommandBinding_Executed"></CommandBinding>
</StackPanel.CommandBindings>
  • 定义CanExecute和Executed这两个事件的事件函数,请参考之前的后台代码。
    了解了这些主要的步骤之后, 很容易就能掌握怎么使用基础命令, 所以如果有兴趣,请尝试下看看。

2.2. 自定义命令 之RoutedCommand

2.2.1. 例子

    这个就先直接看例子。
  • 先创建一个类似WPF提供的标准命令的 自定义命令
    class SelfDefineCommand1
    {
        /// <summary>
        /// 这就是自定义的命令了, 其实就是创建了一个RoutedCommand的静态对象
        /// </summary>
        public static RoutedCommand MyFirstCommandForPrint = new RoutedCommand("MyCommand", typeof(SelfDefineCommand1), 
            new InputGestureCollection() { new KeyGesture(Key.P, ModifierKeys.Control) });
    }
  • 像使用WPF那些标准命令一样, 在Xaml中使用这些命令
	    <TabItem Header="self-define-1" Height="25" VerticalAlignment="Bottom">
                <!--和使用基础命令一样
                将命令赋值给一个命令绑定对象,并添加到目标控件的外围控件-->
                <TabItem.CommandBindings>
                    <CommandBinding Command="{x:Static local:SelfDefineCommand1.MyFirstCommandForPrint}" CanExecute="CommandBinding_CanExecute" 
                                    Executed="CommandBinding_Executed"></CommandBinding>
                </TabItem.CommandBindings>
                <Grid Background="#FFE5E5E5">
                    <Menu HorizontalAlignment="Left" Height="26" VerticalAlignment="Top" Width="103">
                        <MenuItem Header="菜单" Height="26" Width="103">
                            <!--和使用基础命令一样-->
                            <MenuItem Header="我的打印"
                                Command="{x:Static local:SelfDefineCommand1.MyFirstCommandForPrint}" CommandTarget="{Binding ElementName=textBox2}"/>
                        </MenuItem>
                    </Menu>
                    <TextBox x:Name="textBox2" Height="126" TextWrapping="Wrap" Text="TextBox"/>
                </Grid>
            </TabItem>

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);
            }
        }
        /// <summary>
        /// 自定义一个静态的SelfDefineCommand2对象
        /// </summary>
        public static SelfDefineCommand2 MyCommandForPrint2 = new SelfDefineCommand2();
    }
}
    如代码所示, SelfDefineCommand2类实现了ICommand接口的CanExecute和Execute函数, 以及CanExecuteChanged事件,具体解释清参见代码注释。
除了实现ICommand接口以外, 还创建了SelfDefineCommand2的静态实例, 你可以发现这一步和RoutedCommand的自定义实例是一样的,并且实际上怎么使用也是
差不多的。
    关于怎么使用请参见下节。

2.3.3 怎么使用这个自定义类

请参见代码:
	    <TabItem Header="self-define-2" HorizontalAlignment="Left" Height="25" VerticalAlignment="Bottom" Width="126" Margin="-2,-2,-62,2">
                <Grid Background="#FFE5E5E5">
                    <Menu HorizontalAlignment="Left" Height="26" VerticalAlignment="Top" Width="103">
                        <MenuItem Header="菜单" Height="26" Width="103">
                            <!--除了多加了一个Parameter参数, 其他和使用基础命令一样-->
                            <MenuItem Header="我的打印2"
                                Command="{x:Static local:SelfDefineCommand2.MyCommandForPrint2}" CommandTarget="{Binding ElementName=textBox3}" CommandParameter="{Binding ElementName=textBox3}"/>
                        </MenuItem>
                    </Menu>
                    <TextBox x:Name="textBox3" Height="126" TextWrapping="Wrap" Text="TextBox"/>
                </Grid>
            </TabItem>
  • 将命令绑定到命令源控件
<MenuItem Header="我的打印2"  Command="{x:Static local:SelfDefineCommand2.MyCommandForPrint2}"
  • 指定命令目标控件,这里这个暂时没用。以后想到使用场景了再补充 。
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内部就已经实现了业务逻辑的命令。
这部分就不在这里描述了, 否则篇幅太长, 内容太杂, 不易于大家阅读。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值