WPF命令

简介

这是一篇记录笔者早期阅读学习刘铁猛老师的《深入浅出WPF》的读书笔记,如果文中内容阅读不畅,推荐购买正版书籍详细阅读。

什么是命令

诸葛亮的”锦囊妙计“就是命令,锦囊是精美的“包装”,妙计是“内容”。

为什么需要命令

因为事件不具有约束力,所以需要命令来提供有效的约束。命令不仅可以约束代码,还可以约束步骤逻辑。

实际编程工作中就算只使用事件、不适用命令,程序的逻辑也一样可以被驱动得很好,但我们不能阻止程序员按自己得习惯去编写代码。比如保存事件得处理器,程序员们可以写Save()、SaveHandler()、SaveDocument()······这些都符合代码规范,但迟早有一天整个项目会变得无法被读懂,新来得程序员或修改bug的程序员会很抓狂。如果使用命令,情况会号很多—当Save命令到达某个组件时,命令会主动去调用组件的Save()方法,而这个方法可能被定义在基类或者接口里(即保证了这个方法一定是存在的),这就在代码结构和命名上做了约束。不但如此,命令还可以控制接收者“先做校验、再保存、然后关闭”,也就是说,命令除了可以约束代码,还可以约束步骤逻辑,这让新来的程序员想犯错都难,也让修改bug的程序员很快能找到规律、容易上手。

此前我们学习了WPF的路由事件,而在本节将学习一个更为抽象且松耦合的事件版本,即命令。最明显的区别是,事件是与用户动作相关联的,而命令是那些与用户界面想分离的动作,例如我们最熟悉的剪切(Cut)、复制(Copy)和粘贴(Paste)命令。这带来的好处是:命令可以实现复用,减少了代码量,从而可以在不破坏后台逻辑的条件下,更加灵活地控制你的用户界面。在WPF之前使用命令是一件很烦琐的事情,因为需要考虑状态间的同步问题。WPF为了解决这个问题,增加了两个重要特性:一是将事件委托到适当的命令;二是将控件的启用状态与相应命令的状态保持一致。

原文:https://www.cnblogs.com/jellochen/p/archive.html

命令的基本元素与关系

命令系统的基本元素

WPF的命令系统由几个基本要素构成,它们是:

  • 命令(Command):WPF的命令实际上就是实现了ICommand接口的类,平时使用最多的就是RoutedCommand类。我们还会学习使用自定义命令。
  • 命令源(Command Source):即命令的发送者,是实现了ICommandSource接口的类。像Button、MenuItem等界面元素都实现了这个接口,单击它们都会执行绑定的命令。
  • 命令目标(Command Target):即命令将发送给谁,或者说命令将作用在谁身上。命令目标必须是实现了实现了IInputElement接口的类。
  • 命令关联(Command Binding):负责把一些外围逻辑和命令关联起来,比如执行之前对命令是否可以执行进行判断、命令执行之后还有那些后续工作等。

基本元素之间的关系

命令的使用步骤:

  1. 创建命令类:即获得一个实现ICommand接口的类,如果命令与具体业务逻辑无关则使用WPF类库中的RoutedCommand类即可。如果想得到与业务逻辑相关的专有命令,则需创建RoutedCommand(或者ICommand接口)的派生类。
  2. 声明命令实例:使用命令时需要创建命令类的实例。一般情况下程序中某种操作只需要一个命令实例与之对应即可。比如对应“保存”这个操作,你可以拿同一个实例去命令每个组件执行其保存功能,因此程序中的命令多使用单件模式(Singletone Pattern)以减少代码的复杂度。
  3. 指定命令的源:即指定由谁来发送这个命令。如果把命令看作炮弹,那么命令源就相当于火炮。同一个命令可以有多个源。比如保存命令,既可以由菜单中的保存项来发送,也可以由工具栏中的保存图标发送。
  4. 指定命令目标:命令目标并不是命令的属性而是命令源的属性,指定命令目标时告诉命令源向那个组件发哦是那个命令,无论这个组件是否拥有焦点它都会收到这个命令。如果没有为命令源指定命令目标,则WPF系统认为当拥有焦点的对象就是命令目标。这个步骤有点像为火炮指定目标。
  5. 设置命令关联:炮兵时不能单独战斗的,就像炮兵需要侦察兵在射击前观察敌情、判断发射时机,在射击后观测射击效果、帮助修正一样,WPF命令需要CommandBinding在执行前帮助判断是不是可以执行、在执行后做一些是事情来“打扫战场”。

在命令和命令关之之间还有一个微妙的关系。一旦某个UI组件被命令源“瞄上”,命令源就会不停的向命令目标“投石问路”,命令目标就会不停地发送可路由的PreviewCanExecute和CanExecute附加事件,事件就会沿着UI元素树项上传递并被命令关联所捕捉,命令关联捕捉到这些事件后就会把命令能不能发送实时报告给命令。类似的,如果命令被发送出来并到达命令目标,命令目标就会发送PreviewExecute和Execute两个附加事件,这两个事件也会沿着UI元素树向上传递并被命令关联所捕捉,命令关联会完成一些后续的任务。别小看是“后续任务”,对于那些于业务逻辑无关的通用命令,这些后续任务才是最重要的。

PreviewCanExecute、CanExecute、PreviewExecute、Execute,这4个事件都是附加事件,是被CommandManager类“附加”给命令目标的。另外PreviewCanExecute、CanExecute的执行时机不由程序员控制,而且执行频率比较高,这不但会给系统性能带来血降低,偶尔还会引入几个意想不到的bug并且比较难调试,务请多加小心。

下图所示是WPF命令基本元素的关系图:

img

小试命令

需求:定义一个命令,使用Button来发送这个命令,当命令到达TextBox时TextBox会被清空(如果TextBox中没有文字则命令不可被发送)

运行程序,在TextBox中输入文字后Button在命令可执行状态的影响下变为可用,此时单击按钮,TextBox都会被清空。

注意事项:

第一,使用命令可以避免自己写代码判断Button是否可用以及添加快捷键

第二,RoutedCommand是一个与业务逻辑无关的类,只负责程序中“跑腿”而并不对命令目标做任何操作,TextBox并不是由它清空的,而是由CommandBinding清空的。因为无论是探测命令是否执行还是命令送达目标,都会激发命令目标发送路由事件,这些路由事件沿UI树项上传递最终被CommandBinding所捕捉。本例中,CommandBinding被安装在外围的StackPanel上,CommandBinding"站在高处"起一个侦听器的作用,而且专门针对clearCmd命令捕捉于其相关的路由事件。本例中,当CommandBinding捕捉到CanExecute事件就会调用cb_CanExecute方法(判断命令执行的条件是否满足,并反馈给命令供其影响命令源的状态);当捕捉到的是Executed事件(表示命令的Executed方法已经执行了,或说命令已经作用在了命令目标上,RoutedCommand的Execute方法不包含业务逻辑,只负责让命令目标激发Executed),则调用cb_Executed方法。

第三,因为CanExecute事件的激发频率比较高,为了避免降低性能,在处理完后建议把e.Handled设为true。

第四,CommandBinding一定要设置在命令目标的外围空间上,不然无法捕捉到CanExecute和Executed等路由事件

XAML界面如下:

<Grid>
        <StackPanel x:Name="stackPanel">
            <Button x:Name="button1" Content="发送命令" Margin="5"/>
            <TextBox x:Name="textboxA" Margin="5" Height="100"/>
        </StackPanel>
    </Grid>

后台代码为:

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            InitializeCommand();
        }

        //01 声明并定义命令
        private RoutedCommand clerCmd = new RoutedCommand("Clear",typeof(MainWindow));

        private void InitializeCommand()
        {
            //02 把命令赋值给命令源(发送者)并指定快捷键
            this.button1.Command = this.clerCmd;
            this.clerCmd.InputGestures.Add(new KeyGesture(Key.C,ModifierKeys.Alt));

            //03 指定命令目标
            this.button1.CommandTarget = this.textboxA;

            //04 创建命令关联
            CommandBinding cb = new CommandBinding();            
            cb.Command = this.clerCmd;  //只关注与clearCmd相关的事件
            cb.CanExecute += new CanExecuteRoutedEventHandler(Cb_CanExecute) ;
            cb.Executed += new ExecutedRoutedEventHandler(Cb_Executed);

            //把命令关联安置在外围控件上
            this.stackPanel.CommandBindings.Add(cb);
        }

        //当命令到达目标后,此方法被调用
        private void Cb_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            this.textboxA.Clear();
            e.Handled = true;
        }

        //当探测命令是否可以执行时,此方法被调用
        private void Cb_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            if (string.IsNullOrEmpty(this.textboxA.Text))
            {
                e.CanExecute = false;
            }
            else
            {
                //避免继续向上传而降低程序性能
                e.CanExecute = true;
            }
            e.Handled = true;
        }
    }

WPF的命令库

命令具有“一处声明、处处使用”的特点,因此WPF类库准备了一些便捷的命令库,这些命令库包括:

  • ApplicationCommands
  • CommponentCommands
  • NavigationCommands
  • MediaCommands
  • EditingCommands

它们都是静态类,而命令就是用这些类的静态只读属性以单件模式暴露出来的。

public static class ApplicationCommands
    {
        //     获取表示“属性”命令的值。
        public static RoutedUICommand Properties { get; }
        //     获取表示“打印预览”命令的值。
        public static RoutedUICommand PrintPreview { get; }
        //     获取表示“打印”命令的值。
        public static RoutedUICommand Print { get; }
        //     获取表示“粘贴”命令的值。
        public static RoutedUICommand Paste { get; }
        //     获取表示“停止”命令的值。
        public static RoutedUICommand Stop { get; }
        //     获取表示“打开”命令的值。
        public static RoutedUICommand Open { get; }
        //     获取表示 New 命令的值。
        public static RoutedUICommand New { get; }
        //     获取表示 Help 命令的值。
        public static RoutedUICommand Help { get; }
        //     获取表示 Find 命令的值。
        public static RoutedUICommand Find { get; }
        //     获取表示“删除”命令的值。
        public static RoutedUICommand Delete { get; }
        //     获取表示“剪切”命令的值。
        public static RoutedUICommand Cut { get; }
        //     获取表示“更正列表”命令的值。
        public static RoutedUICommand CorrectionList { get; }
        //     获取表示“复制”命令的值。
        public static RoutedUICommand Copy { get; }
    	//···········

    }

命令参数

命令源一定是实现了ICommandSource接口的对象,而ICommandSource有一个属性就是CommandPrameter,如果把命令看作飞向目标的炮弹,那么CommandPrameter就相当于装载在炮弹肚子里的“消息”。

示例XAML代码:

<Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="24"/>
            <RowDefinition Height="4"/>
            <RowDefinition Height="24"/>
            <RowDefinition Height="4"/>
            <RowDefinition Height="24"/>
            <RowDefinition Height="4"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <!--命令和命令参数-->
        <TextBlock Text="Name:" VerticalAlignment="Center" HorizontalAlignment="Left" Grid.Row="0"/>
        <TextBox x:Name="nameTextBox" Margin="60,0,0,0" Grid.Row="0"/>
        <Button Content="New Teacher" Command="New" CommandParameter="Teacher" Grid.Row="2"/>
        <Button Content="New Student" Command="New" CommandParameter="Student" Grid.Row="4"/>
        <ListBox x:Name="listBoxNewItems" Grid.Row="6"/>
    </Grid>
<!--为窗体添加CommandBinding-->
<Window.CommandBindings>
        <CommandBinding Command="New" CanExecute="New_CanExecute" Executed="New_Executed"/>
    </Window.CommandBindings>

后台代码:

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void New_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            if (string.IsNullOrEmpty(this.nameTextBox.Text))
            {
                e.CanExecute = false;
            }
            else
            {
                e.CanExecute = true;
            }
        }

        private void New_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            string name = this.nameTextBox.Text;
            if (e.Parameter.ToString()=="Teacher")
            {
                this.listBoxNewItems.Items.Add(string.Format($"New Teacher:{name},学而不厌、诲人不倦。"));
            }
            if (e.Parameter.ToString()=="Student")
            {
                this.listBoxNewItems.Items.Add(string.Format($"New Student:{name},好好学习、天天向上。"));
            }
        }
    }

命令与Binding的结合

控件有很多事件,可以让我们进行各种各样不同的操作,可控件只有一个Command属性、而命令库中却有数十中命令,这样怎么可能使用这个唯一的Command属性来调用那么多种命令呢?答案是:使用Binding,Binding作为一种间接的、不固定的赋值手段,可以让你有机会选择在某个条件下为目标赋特定的值(有时候需要借助Converter)

例如,如果一个Button所关联命令有可能根据某些条件而改变,我们可以把代码写成这样:

<Button x:Name="dynamicCmdBtn" Command="{Binding Path=ppp,Source=sss}"Content="Command"/>

因为大多数命令按钮都有相对应的图标来表示固定的含义,所以日常工作中一个控件的命令一经确定就很少改变。

近观命令

一般情况下,程序中使用与逻辑无关的RoutedCommand来跑跑龙套就足够了,但为了使程序的结构更简洁(比如去掉外围的CommandBinding和与之相关的事件处理器),常需要定义自己的命令。

ICommand接口与RoutedCommand

WPF的命令使实现了ICommand接口的类,ICommand接口非常简单,只包含两个方法和一个事件:

  • Execute方法:执行命令,或者说命令作用于命令目标之上。
  • CanExecute方法:是否执行,在执行前用来探知命令是否可被执行。
  • CanExecuteChanged事件:当命令可执行状态发生改变时,可激发此事件来通知其他对象。

RoutedCommand就是这样一个实现了ICommand接口的类,但是并未向Execute和CanExecute方法中添加任何逻辑,它是通用的、与具体业务逻辑无关的。

从外部来看,ApplicationCommands命令库里的命令们,它们具体执行Copy还是Cut(即业务逻辑)不是有命令决定的,而是外围的CommandBinding捕捉到命令目标受命令激发而发送的路由事件后在其Executed事件处理器中完成的。

public static class ApplicationCommands
    {
        //     获取表示“属性”命令的值。
        public static RoutedUICommand Properties { get; }
        //     获取表示“打印预览”命令的值。
        public static RoutedUICommand PrintPreview { get; }
        //     获取表示“打印”命令的值。
        public static RoutedUICommand Print { get; }
        //     获取表示“剪切”命令的值。
        public static RoutedUICommand Cut { get; }
        //     获取表示“更正列表”命令的值。
        public static RoutedUICommand CorrectionList { get; }
        //     获取表示“复制”命令的值。
        public static RoutedUICommand Copy { get; }
    	//···········

    }

从内部看,从ICommand接口继承来的Execute并没有被公开(甚至可以说是废弃不同了),仅仅是调用了新声明的带两个参数的Execute方法。新声明的带两个参数的Execute方法是对外公开的,可以使用第一个参数向命令传递一些数据,第二个参数是命令的目标,如果目标为null,Execute方法就把当前拥有焦点的控件作为命令的目标。新的Execute方法会调用命令执行逻辑的核心—ExecuteImpl方法,而这个方法内部并没有什么神秘的东西,就是“借用”命令目标的RaiseEvent把RoutedEvent发送出去。显然这个事件会被外围的CommandBinding捕获到然后执行程序员预设的与业务相关的逻辑。

自定义Command

与业务逻辑无关的命令,使用 RoutedCommand,业务逻辑要依靠外围的CommandBinding来实现。这样一来,如果对CommandBinding管理不善就可能造成代码混乱无章,毕竟一个CommandBinding要牵扯到谁是它的宿主以及它的两的事件处理器。

创建自定义Command

  1. 为了简化使用CommandBinding来处理业务逻辑的程序结构,我们可能会希望把业务逻辑移入命令的Execute方法内。比如,我们可以自定义一个名为Save的命令,当命令到达命令目标的时候先通过命令目标的IsChanged属性判断命令目标的内容是否已经被改变,如果已经改变则命令可以执行,命令的执行会直接调用命令目标的Save方法、驱动命令目标以自己的方式保存数据。很显然,这回是命令直接在命令目标上起作用了,而不像RoutedCommand那样现在命令目标上激发处路由事件等外围控件捕捉到事件后再“翻过头来”对命令目标加以处理。你可能会问:“如果命令目标不包含IsChanged和Save方法怎么版?“这就要靠接口来约束了,在程序中定义这样一个接口:

    并且要求每个需要接受命令的组件都必须实现这个接口,这样就确保了命令可以成功地对它们执行操作。

public interface IView
{
    //属性
    bool IsChanged { get; set; }

    //方法
    void SetBinding();
    void Refresh();
    void Clear();
    void Save();        
}
  1. 实现ICommand接口,创建一个专门作用于IView派生类的命令

在实现这个方法时,我们将这个方法唯一的参数作为命令的目标,如果目标是IView接口的派生类则调用其Clear方法—显然,我们已经把业务逻辑引入了命令的Execute方法中。

public class ClearCommand : ICommand
{
    //当命令可执行状态发生改变时,应当被激活
    public event EventHandler CanExecuteChanged;

    //用于判断命令是否可以执行(暂不实现)
    public bool CanExecute(object parameter)
    {
        throw new NotImplementedException();
    }

    //命令执行,带有与业务相关的Clear逻辑
    public void Execute(object parameter)
    {
        IView view = parameter as IView;
        if (view!=null)
        {
            view.Clear();
        }
    }
}
  1. 创建命令源,我们有了自定义命令,我们拿什么命令源来”发射“它呢?WPF命令系统的命令源是专门为RoutedCommand准备的并且不能重写,所以我们智能通过实现ICommandSource接口来创建自己的命令源:

ICommandSource接口只包含Command、CommandParameter、CommandTarget三个属性,至于这三个属性之间有什么样的关系就要看我们怎么实现了。在本例中,CommandParameter完全没有被用到,而CommandTarget被当作参数传递给了Command的Execute方法。命令不会自己被发出,所以一定要为命令的执行选择一个合适的时机,本例中我们在控件被单击时执行命令。

//自定义命令源
public class MyCommandSource : UserControl, ICommandSource
{
    //继承自ICommandSource的三个属性
    public ICommand Command { get; set; }

    public object CommandParameter { get; set; }

    public IInputElement CommandTarget { get; set; }

    //在组件被单击时连带执行命令
    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        base.OnMouseLeftButtonDown(e);

        //在命令目标上执行命令,或称让命令用于命令目标
        if (this.CommandTarget!=null)
        {
            this.Command.Execute(this.CommandTarget);
        }
    }
}
  1. 可用的命令目标,因为我们的ClearCommand专门作用于IView的派生类,所以合格的ClearCommand命令目标必须实现IView接口。设计这种既有UI有需要实现接口的类可以先用XAML编译器实现其UI部分再找到他的后台C#代码实现接口。

XAML代码

<Border CornerRadius="5" BorderBrush="LawnGreen" BorderThickness="2">
    <StackPanel>
        <TextBox x:Name="textBox1" Margin="5"/>
        <TextBox x:Name="textBox2" Margin="5,0"/>
        <TextBox x:Name="textBox3" Margin="5"/>
        <TextBox x:Name="textBox4" Margin="5,0"/>
    </StackPanel>
</Border>

后台C#代码

public partial class MiniView : UserControl,IView
    {
        public MiniView()
        {
            InitializeComponent();
        }

        //继承自IView的成员们
        public bool IsChanged { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }

        public void Refresh()
        {
            throw new NotImplementedException();
        }

        public void Save()
        {
            throw new NotImplementedException();
        }

        public void SetBinding()
        {
            throw new NotImplementedException();
        }

        //用于清楚内容的业务逻辑
        public void Clear()
        {
            this.textBox1.Clear(); 
            this.textBox2.Clear(); 
            this.textBox3.Clear(); 
            this.textBox4.Clear();
        }
    }
  1. 把自定义的命令、命令源和命令目标集合起来。
<StackPanel>
    <local:MyCommandSource x:Name="ctrlClear" Margin="10">
        <TextBlock Text="清除" FontSize="16" TextAlignment="Center" Background="LightBlue" Width="80"/>
    </local:MyCommandSource>
    <local:MiniView x:Name="miniView"/>
</StackPanel>

本例中使用了简单的文本作为命令源的显示内容,实际工作中可以使用图标、按钮等内容来填充它,但要注意适当更改激发命令的发时。比如你打算放置一个按钮,那么就不要用重写OnMouseLeftButtonDown的方法来执行命令了,而应该捕捉Button.Click事件并在事件处理器中执行方法(Mouse事件会被Button"吃掉")。

后台C#代码如下:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        //声明命令并使命令源和目标与之关联
        //声明命令
        ClearCommand clearCommand = new ClearCommand();
        //命令源命令
        this.ctrlClear.Command = clearCommand;
        //命令源目标
        this.ctrlClear.CommandTarget = this.miniView;
    }
}

我们创建了一个ClearCommand命令的实例并把它赋值给自定义命令源的Command属性,自定义命令源的CommandTarget属性值是自定义命令目标MiniView的实例。提醒一句:为了讲解清晰才把命令声明放在这里,正规的方法应该是把命令声明在静态全局的地方供所有对象使用。

登峰造极的成就源于自律
原文地址:https://www.cnblogs.com/fishpond816/p/13642930.html