WPF中的命令

锦囊妙计的本质就是命令。

有了路由事件为什么还需要命令系统呢?事件的作用是发布、传播一些消息,消息送达接收者,事件的使命也就完成了,至于如何响应事件送来的消息事件并不做规定,每个接收者可以使用自己的行为来响应事件。也就是说,事件不具有约束力。命令与事件的区别就在于命令是具有约束力的

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

WP的命令系统由一下几个基本要素构成:

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

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

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

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

这些基本元素之间体现在使用命令的过程中。命令的使用大概分为以下几步:

1) 创建命令类:即获得一个实现ICommand接口的类,如果命令与具体业务逻辑无关则使用WPF类库中的RoutedCommand类即可。如果想要得到与业务逻辑相关的专有命令,则需要创建RoutedCommand(或者ICommand接口)的派生类。

2) 声明命令实例:使用命令时需要创建命令类的实例。这里有个技巧,一般情况下程序中某种操作只需要一个命令实例与之对应即可。比如对应”保存“这个操作,你可以拿同一个实例去命令每个组件执行保存功能,因此程序中的命令多使用单件模式(Singletone Pattern)以减少代码的复杂度。

3) 指定命令的源:即指定由谁来发送这个命令。如果把命令当做炮弹,那么命令源就相当于火炮。同一个命令可以有多个源。比如保存命令,既可以由菜单中的保存项来发送,也可以由工具栏中的保存图标来发送。需要注意的是,一旦把命令指派给命令源,那么命令源就会受命令的影像,当命令不能被执行的时候作为命令源的控件将处在不可用状态。看来命令这种炮弹很智能,当不满足发射条件时还会给用来发射它的火炮上一道保险、避免走火。还需要注意,各种控制发送命令的方法不尽相同,比如Button和MenuItem是在单击时发送命令,而ListBoxItem单击时表示被选中所以双击才会发送命令。

4) 指定命令目标:命令目标不是命令的属性而是命令源的属性,指定命令目标是告诉命令源向哪个组件发送命令,无论这个组件是否拥有焦点它都会受到这个命令。如果没有命令源指定命令目标,则WPF系统认为当前拥有焦点的对象就是命令目标。这个步骤有点向为火炮指定目标。

5) 设置命令关联:炮兵是不能单独作战的,就像炮兵需要侦查兵在射击前观察敌情、判断发射时机,在射击后观测射击效果、帮助修正一样,WPF命令需要CommandBinding在执行前来帮助是不是可以执行、在执行后做一些事情来“打扫战场”。

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

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

WPF命令系统基本元素的关系图:


DEMO

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

XAML:

<Window x:Class="Chapter9.Page178.CommandDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel x:Name="stackPanel">
        <Button x:Name="button1" Content="Send Command" Margin="5"/>
        <TextBox x:Name="textBoxA" Margin="5" Height="100"/>
    </StackPanel>
</Window>

后台代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace Chapter9.Page178.CommandDemo
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            InitializeCommand();
        }

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

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

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

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

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

        //当命令送达目标后,此方法被调用
        void cb_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            this.textBoxA.Clear();
            //避免继续上传而降低程序性能
            e.Handled = true;
        }

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

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

对于上面的代码需要注意:

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

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

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

4) CommandBinding一定要设置在命令目标的外围控件上,不然无法捕获到CanExecute和Executed等路由事件。\


WPF的命令库

上面的这个例子中我们自己生命定义了一个命令:

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

命令具有“一处声明,处处使用”的特点,比如Save命令,在程序的任何地方它都表示要求命令目标保存数据。因此,微软在WPF类库里准备了一些便捷的命令库,这些命令库包括:

1) ApplicationCommands

2) ComponentCommands

3) NavigationCommands

4) MediaCommands

5) EditingCommands

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


命令参数

前面提到命令库里有很多WPF预制的命令,这些命令都是ApplicationCommands类的静态属性,所以他们的实例永远只有一个。这就有一个问题:如果界面上有两个按钮,一个用来新建Teacher档案,另一个用来新建Students档案,都使用New命令的话,程序应该如何区别新建的是什么档案呢?

答案是使用CommandParameter。命令源一定是实现了ICommandSource接口的对象,而ICommandSource就有一个属性就是CommandParameter,如果把命令当做飞向目标的炮弹,那么CommandParameter就相当于装载在炮弹肚子里的“消息”。下面是程序的实现代码:

<Window x:Class="Chapter9.Page181.CommandParameterDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <!--为窗体添加CommandBinding-->
    <Window.CommandBindings>
        <CommandBinding Command="New" CanExecute="CommandBinding_CanExecute" Executed="CommandBinding_Executed"/>
    </Window.CommandBindings>
    <Grid Margin="6">
        <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>
</Window>

代码有两个值得注意的地方:

1) 两个按钮都使用New命令,但分别使用字符串Teacher和Student作为参数。

2) 这次是使用XAML代码为窗体添加CommandBinding、CommandBinding的CanExecute和Executed事件处理器写在后台C#代码里。

后台代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace Chapter9.Page181.CommandParameterDemo
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

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

        private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            string name = this.nameTextBox.Text;
            if (e.Parameter.ToString() == "Teacher")
            {
                this.listBoxNewItems.Items.Add(string.Format("New Teacher:{0}", name));
            }
            if (e.Parameter.ToString() == "Student")
            {
                this.listBoxNewItems.Items.Add(string.Format("New Student:{0}", name));
            }
            e.Handled = true;
        }
    }
}

运行程序,当TextBox中没有内容时两个按钮均不可用;当输入文字后按钮变成可用,单击按钮,ListBox会加入不同条目。


命令与Binding的结合

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

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

        <Button x:Name="dynamicCmdBtn" Command="{Binding Path=ppp,Source=sss}" Content="Command"/>
日常工作中一个控件的命令一经确定就很少改变。


ICommand接口与RoutedCommand

一般情况下,程序使用与逻辑无关的RoutedCommand来泡泡龙套就足够了,但为了使程序的结构更加简洁,我们通常需要定义自己的命令。
WPF的命令是实现了ICommand接口的类。ICommand接口非常简单,只包含两个方法和一个事件:

1) Execute方法:命令执行,或者说命令作用于命令目标之上。需要注意的是,现实世界中的命令是不会自己“执行”的,它只能“被执行”,而在这时,执行变成了命令的方法。

2) CanExecute方法:在执行之前用来探知命令是否可被执行。

3) CanExecuteChanged事件:当命令可执行状态发生改变时,可激发此事件来通知其他对象。

RoutedCommand就是这样一个实现了ICommand接口的类。RoutedCommand在实现ICommand接口时,并未向Execute和CanExecute方法中添加任何逻辑。


自定义Command

自定义命令可以分为两个层次来理解:

第一层次是比较浅,指的是当WPF命令库中没有包含想要的命令时,我们就得声明定义自己的RoutedCommand实例。比如想让命令目标在命令到达时发出笑声,WPF命令库里没有这个命令,那就可以定义一个名为Laugh的RoutedCommand实例。很难说着是真正意义上的“自定义命令”,这只是对RoutedCommand的使用。

第二个层次是指从实现ICommand接口开始,定义自己的命令并且把某些业务逻辑也包含在命令之中,这才称得上是真正意义上的自定义命令。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值