WPF教程(十四)命令

2018年09月26日 13:40:55 yangwenxue1989 阅读数:80  

命令这东西很有意思,简明一点说,它是一种逻辑,但这种逻辑可以被多种源调用,可以作用于多种目标上。如我们常见的"复制","剪切"等命令,它们本身就是一种逻辑——对剪切板进行操作的逻辑行为,不过,你会发现,它们不仅可以在菜单项中使用,也可以在工具栏按钮上使用,也可以通过快捷键Ctrl + C等来调用。

(一)命令模型

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。

命令(Command):WPF的命令实际上就是实现了ICommand接口的类,平时使用最多的就是RoutedCommand类。我们还会学习使用自定义命令。

命令源(Command Source):即命令的发送者,是实现了ICommandSource接口的类。很多界面元素都实现了这个接口,其中包括Button,ListBoxItem,MenuItem等。一旦把命令指派给了命令源,那么命令源就会受命令的影响,当命令不能被执行的时候命令源的控件处于不可用状态。

命令目标(Command Target):即命令发送给谁,或者说命令作用在谁的身上。命令目标必须是实现了IInputElement接口的类。一旦被命令源指定,无论这个控件是否拥有焦点他都会收到这个命令。如果没有为源指定命令目标,则WPF系统认为当前拥有焦点的对象就是命令目标。

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

因为Winform里没有命令这个概念,直接接触上面的定义可能不知所云,我们举个生活中的例子,比如两军对阵,将军命令士兵放箭进攻:命令就是敌人进一步就射箭;命令源就是将军;命令目标是士兵;命令关联就相当于瞭望台士兵,不可能将军喊放箭就放箭,瞭望台先要观察下对方有没有进,如果进了就去请求将军要不要下达命令,将军若是下令放,那么瞭望台举旗示意,然后命令目标执行。或许可能还有这种情况,就是敌方也有个瞭望台,且跟我方一样的进攻指令下发程序,那么就还需要考虑到这个命令是我方还是敌方发布的。乘热打铁,写个命令程序来活演这个将军指挥战斗。

  1.   <StackPanel Name= "sp1">
  2.   <TextBox x:Name= "txtA" Margin="5,0" Height="200"></TextBox>
  3.   <Button Content= "Send Command" x:Name="btn1" Margin="5"></Button>
  4.   </StackPanel>
  1.   public partial class MainWindow : Window
  2.   {
  3.   public MainWindow()
  4.   {
  5.   InitializeComponent();
  6.   InitializeCommand();
  7.   }
  8.   //清除命令(相当于敌军进一步放箭)
  9.   private RoutedCommand rouutedCommand = new RoutedCommand("Clear",typeof(MainWindow));
  10.   private void InitializeCommand()
  11.   {
  12.   //把命令赋值给命令源,并定义快捷键(将军可以发布这个命令)
  13.   this.btn1.Command = rouutedCommand;
  14.   //(做好标记)
  15.   this.btn1.CommandParameter = this.btn1.Name.ToString();
  16.   this.rouutedCommand.InputGestures.Add(new KeyGesture(Key.C, ModifierKeys.Alt));
  17.   //指定命令目标(放箭士兵)
  18.   this.btn1.CommandTarget = txtA;
  19.   //创建命令关联
  20.   CommandBinding commandBinding = new CommandBinding();
  21.   commandBinding.Command = rouutedCommand; //只关注与rouutedCommand相关的命令
  22.   //目标动作(士兵放箭)
  23.   commandBinding.CanExecute += new CanExecuteRoutedEventHandler(cb_CanExecute);
  24.   //判断是否可以动作(观察敌情,有没有达到可以请求放箭命令)
  25.   commandBinding.Executed += new ExecutedRoutedEventHandler(cb_Execute);
  26.   //把命令关联安置在外围控件上
  27.   this.sp1.CommandBindings.Add(commandBinding);
  28.   }
  29.   //当命令到达目标之后,此方法被调用
  30.   private void cb_Execute(object sender, ExecutedRoutedEventArgs e)
  31.   {
  32.   txtA.Clear();
  33.   //避免事件继续向上传递而降低程序性能
  34.   e.Handled = true;
  35.   }
  36.   //当探测命令是否可执行的时候该方法会被调用
  37.   private void cb_CanExecute(object sender,CanExecuteRoutedEventArgs e)
  38.   {
  39.   if (string.IsNullOrEmpty(txtA.Text))
  40.   {
  41.   e.CanExecute = false;
  42.   }
  43.   else
  44.   {
  45.   //(验证指令来自哪方将军,我方就执行)
  46.   if (e.Parameter.ToString() == "btn1")
  47.   e.CanExecute = true;
  48.   }
  49.   //避免事件继续向上传递而降低程序性能
  50.   e.Handled = true;
  51.   }
  52.   }

(二)剖析命令模型

如果只是简单的应用的话,掌握上面写法就行了,但如果拷问为什么这么写,就要理解里面的原理。

1.命令——ICommand

  1.   public interface ICommand
  2.   {
  3.   // 摘要:
  4.   // 当出现影响是否应执行该命令的更改时发生。
  5.   event EventHandler CanExecuteChanged;
  6.   // 摘要:
  7.   // 定义用于确定此命令是否可以在其当前状态下执行的方法。
  8.   // 参数:
  9.   // parameter:
  10.   // 此命令使用的数据。如果此命令不需要传递数据,则该对象可以设置为 null。
  11.   // 返回结果:
  12.   // 如果可以执行此命令,则为 true;否则为 false。
  13.   bool CanExecute(object parameter);
  14.   // 摘要:
  15.   // 定义在调用此命令时调用的方法。
  16.   // 参数:
  17.   // parameter:
  18.   // 此命令使用的数据。如果此命令不需要传递数据,则该对象可以设置为 null。
  19.   void Execute(object parameter);
  20.   }

WPF的命令模型的核心是System.Window.Input.ICommand接口,包含两个方法和一个事件。当CanExecute方法返回命令状态,当返回true时,Execute方法得到执行,在这个方法中进行逻辑处理,当命令状态改变时,CanExecuteChanged事件触发,可以根据命令状态相应控制控件(命令源)的状态。
2.命令——RoutedCommand

在WPF中,RoutedCommand类是唯一实现ICommand接口的类,它除了实现ICommand接口外,还支持事件冒泡和隧道传递,可以使命令在WPF元素层次间以冒泡或隧道方式传递时可以被恰当的处理程序处理。除了私有实现CanExecute方法和Execute方法外,还公开重载了这两个方法:

  1.   // 摘要:
  2.   // 确定此 System.Windows.Input.RoutedCommand 在其当前状态是否可以执行。
  3.   //
  4.   // 参数:
  5.   // parameter:
  6.   // 用户定义的数据类型。
  7.   //
  8.   // target:
  9.   // 命令目标。
  10.   //
  11.   // 返回结果:
  12.   // 如果可以对当前命令目标执行此命令,则为 true;否则为 false。
  13.   //
  14.   // 异常:
  15.   // System.InvalidOperationException:
  16.   // target 不是 System.Windows.UIElement 或 System.Windows.ContentElement。
  17.   [ SecurityCritical]
  18.   public bool CanExecute(object parameter, IInputElement target);
  19.   //
  20.   // 摘要:
  21.   // 对当前命令目标执行 System.Windows.Input.RoutedCommand。
  22.   //
  23.   // 参数:
  24.   // parameter:
  25.   // 要传递到处理程序的用户定义的参数。
  26.   //
  27.   // target:
  28.   // 要在其中查找命令处理程序的元素。
  29.   //
  30.   // 异常:
  31.   // System.InvalidOperationException:
  32.   // target 不是 System.Windows.UIElement 或 System.Windows.ContentElement。
  33.   [ SecurityCritical]
  34.   public void Execute(object parameter, IInputElement target);

重载方法后面多了一个IInputElement接口类型(UIElement类就实现了该接口)的参数表示开始处理事件的元素,另外还多了三个属性:

  1.   public InputGestureCollection InputGestures { get; }
  2.   public string Name { get; }
  3.   public Type OwnerType { get; }

第一个属性是获取与此命令关联的 System.Windows.Input.InputGesture 对象的集合。第二个属性是获取命令名称。第三个参数是获取命令所有者类型。

3.命令目标——CommandTarget

在命令源发送命令之后,我们需要一个命令接受者,或者叫命令的作用者,即命令目标。它实现了IInputElement接口。在小节2.命令中RoutedCommand类中重载的CanExecute方法和Execute方法的第二个参数就是传递命令目标。UIElement类就实现了该接口,也就是说所有的控件都可以作为命令目标。

4.命令关联——CommandBinding

命令目标发送路由事件,为了让命令得到恰当的响应,我们需要一个命令关联。我们来看下CommandBinding类的定义:

  1.   public ICommand Command { get; set; }
  2.   public event CanExecuteRoutedEventHandler CanExecute;
  3.   public event ExecutedRoutedEventHandler Executed;
  4.   public event CanExecuteRoutedEventHandler PreviewCanExecute;
  5.   public event ExecutedRoutedEventHandler PreviewExecuted;

Command属性是获取与命令关联相关的命令;PreviewCanExecute/CanExecute事件是当与命令关联相关的命令启动检查以确定是否可以在当前命令目标上执行命令时发生;PreviewExecuted/Executed事件是当执行与该命令关联相关的命令时发生。其实,这四个附加事件是定义在CommandManager类中然后附加给命令目标的。

当命令源确定了命令目标后(人为指定或焦点判断),就会不停地向命令目标询问,命令目标就会不停地发送PreviewCanExecute和CanExecute事件,这两个附加事件就会沿着元素树向上传递,然后被命令关联捕获,命令关联将命令能不能发送报告给命令源。若可以发送命令,则命令源将发送命令给命令目标,命令目标会发送PreviewExecuted和Executed事件,这两个附加也会沿着元素树向上传递,然后被命令关联捕获,完成一些后续工作。

到这边,应该对这个命令发送原理有个比较深的了解,这时候我们再将最上面的纯后台程序改造成前后台结合:

  1.   <!--<相当于命令关联附加到这个控件>-->
  2.   <Window.CommandBindings>
  3.   <CommandBinding Command= "{x:Static local:MainWindow.rouutedCommand}"
  4.   CanExecute= "cb_CanExecute"
  5.   Executed= "cb_Execute">
  6.   </CommandBinding>
  7.   </Window.CommandBindings>
  8.   <StackPanel Name= "sp1">
  9.   <TextBox x:Name= "txtA" Margin="5,0" Height="200"></TextBox>
  10.   <!--<相当于去实现后台的控件属性>-->
  11.   <Button Content= "Send Command" x:Name="btn1" Margin="5" CommandParameter="{Binding RelativeSource={RelativeSource Self},Path=Name}" CommandTarget="{Binding ElementName=txtA}" Command="{x:Static local:MainWindow.rouutedCommand}"></Button>
  12.   </StackPanel>
  1.   public static RoutedCommand rouutedCommand = new RoutedCommand("Clear", typeof(MainWindow));
  2.   private void cb_Execute(object sender, ExecutedRoutedEventArgs e)
  3.   {
  4.   txtA.Clear();
  5.   //避免事件继续向上传递而降低程序性能
  6.   e.Handled = true;
  7.   }
  8.   //当探测命令是否可执行的时候该方法会被调用
  9.   private void cb_CanExecute(object sender,CanExecuteRoutedEventArgs e)
  10.   {
  11.   if (string.IsNullOrEmpty(txtA.Text) )
  12.   {
  13.   e.CanExecute = false;
  14.   }
  15.   else
  16.   {
  17.   if (e.Parameter.ToString() == "btn1")
  18.   e.CanExecute = true;
  19.   }
  20.   //避免事件继续向上传递而降低程序性能
  21.   e.Handled = true;
  22.   }

(三)自定义命令

上面例子使用了RoutedCommand实现了自我需求的命令,但这并不是真正意义上的自定义命令,偏应用,下面我们实现源码级的自定义,不照办固定形式,通常有以下几种方式:

1.是直接实现ICommand接口,这是最彻底的方式;

2.是继承自RoutedCommand类和RoutedUICommand类,这种方式可以命令路由;

3.是使用RoutedCommand类和RoutedUICommand类实例,严格来讲,这种方式只是命令的应用。

例子先放一下

(总结)

命令按本文思路理解下来还是比较清楚的,注意把ICommand、RoutedCommand及CommandBinding理解清楚,基本上应用不是大问题。

扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄