在上一篇中,我们学习了WPF的路由事件,而在本节将学习一个更为抽象且松耦合的事件版本,即命令。最明显的区别是,事件是与用户动作相关联的,而命令是那些与用户界面想分离的动作,例如我们最熟悉的剪切(Cut)、复制(Copy)和粘贴(Paste)命令。这带来的好处是:命令可以实现复用,减少了代码量,从而可以在不破坏后台逻辑的条件下,更加灵活地控制你的用户界面。然而,命令并不是WPF特有的,早在MFC中已经有了类似的机制,然而,在WPF之前使用命令是一件很烦琐的事情,因为需要考虑状态间的同步问题。WPF为了解决这个问题,增加了两个重要特性:一是将事件委托到适当的命令;而是将控件的启用状态与相应命令的状态保持一致。在Caliburn.Micro中完全用Command来代替事件。本篇将从WPF命令模型、自定义命令和WPF内置命令来学习。
1.WPF命令模型
WPF命令模型主要包含以下几个基本元素:
命令(Command):指的是实现了ICommand接口的类,例如RoutedCommand类及其子类RoutedUICommand类,一般不包含具体逻辑。
命令源(Command Source):即命令的发送者,指的是实现了ICommandSource接口的类。像Button、MenuItem等界面元素都实现了这个接口,单击它们都会执行绑定的命令。
命令目标(Command Target):即命令的接受者,指的是实现了IInputElement接口的类。
命令关联(Command Binding):即将一些外围逻辑和命令关联起来。
下面通过一个关系图来看下:
由上图,不难发现,命令目标需要通过命令关联来影响命令源。
1.1命令
WPF的命令模型的核心是System.Window.Input.ICommand接口,包含两个方法和一个事件。
public interface ICommand { // 摘要: // 当出现影响是否应执行该命令的更改时发生。 event EventHandler CanExecuteChanged; // 摘要: // 定义用于确定此命令是否可以在其当前状态下执行的方法。 // // 参数: // parameter: // 此命令使用的数据。如果此命令不需要传递数据,则该对象可以设置为 null。 // // 返回结果: // 如果可以执行此命令,则为 true;否则为 false。 bool CanExecute(object parameter); // // 摘要: // 定义在调用此命令时调用的方法。 // // 参数: // parameter: // 此命令使用的数据。如果此命令不需要传递数据,则该对象可以设置为 null。 void Execute(object parameter); }
当CanExecute方法返回命令状态,当返回true时,Execute方法得到执行,在这个方法中进行逻辑处理,当命令状态改变时,CanExecuteChanged事件触发,可以根据命令状态相应控制控件(命令源)的状态。
在WPF中,RoutedCommand类是唯一实现ICommand接口的类,它除了实现ICommand接口外,还支持事件冒泡和隧道传递,可以使命令在WPF元素层次间以冒泡或隧道方式传递时可以被恰当的处理程序处理。除了私有实现CanExecute方法和Execute方法外,还公开重载了这两个方法:
// 摘要: // 确定此 System.Windows.Input.RoutedCommand 在其当前状态是否可以执行。 // // 参数: // parameter: // 用户定义的数据类型。 // // target: // 命令目标。 // // 返回结果: // 如果可以对当前命令目标执行此命令,则为 true;否则为 false。 // // 异常: // System.InvalidOperationException: // target 不是 System.Windows.UIElement 或 System.Windows.ContentElement。 [SecurityCritical] public bool CanExecute(object parameter, IInputElement target); // // 摘要: // 对当前命令目标执行 System.Windows.Input.RoutedCommand。 // // 参数: // parameter: // 要传递到处理程序的用户定义的参数。 // // target: // 要在其中查找命令处理程序的元素。 // // 异常: // System.InvalidOperationException: // target 不是 System.Windows.UIElement 或 System.Windows.ContentElement。 [SecurityCritical] public void Execute(object parameter, IInputElement target);
多了一个IInputElement类型(UIElement类就实现了该接口)的参数表示开始处理事件的元素,另外还有三个属性:
public InputGestureCollection InputGestures { get; } public string Name { get; } public Type OwnerType { get; }
第一个属性是获取与此命令关联的 System.Windows.Input.InputGesture 对象的集合。第二个属性是获取命令名称。第三个参数是获取命令所有者类型。
还有个继承自RoutedCommand的类RoutedUICommand类,它在RoutedCommand类的基础上只增加了一个Text属性用于设置命令的文本,该属性进行了本地化的处理。
1.2命令源
在上面我们提到的命令,只是单纯的命令,没有任何硬编码功能。为了让一个命令被触发,我们需要一个命令源(命令发送者)。前面已经讲到,并不是所有的控件都可以触发命令,只有实现了ICommandSource接口的控件才可以,这样的控件主要有继承自ButtonBase类的Button和CheckBox控件、Hyperlink控件和MenuItem等。ICommandSource接口定义如下:
ICommand Command { get; } object CommandParameter { get; } IInputElement CommandTarget { get; }
第一个属性是获取将在调用命令源时执行的命令。第二个属性是获取传递给命令的参数。第三个参数是获取命令目标。
1.3命令目标
在命令源发送命令之后,我们需要一个命令接受者,或者叫命令的作用者,即命令目标。它实现了IInputElement接口。在1.1中RoutedCommand类中重载的CanExecute方法和Execute方法的第二个参数就是传递命令目标。UIElement类就实现了该接口,也就是说所有的控件都可以作为命令目标。
1.4命令关联
命令目标发送路由事件,为了让命令得到恰当的响应,我们需要一个命令关联。我们来看下CommandBinding类的定义:
public ICommand Command { get; set; } public event CanExecuteRoutedEventHandler CanExecute; public event ExecutedRoutedEventHandler Executed; public event CanExecuteRoutedEventHandler PreviewCanExecute; public event ExecutedRoutedEventHandler PreviewExecuted;
Command属性时获取与命令关联相关的命令;PreviewCanExecute/CanExecute事件是当与命令关联相关的命令启动检查以确定是否可以在当前命令目标上执行命令时发生;PreviewExecuted/Executed事件是当执行与该命令关联相关的命令时发生。其实,这四个附加事件是定义在CommandManager类中然后附加给命令目标的。
当命令源确定了命令目标后(人为指定或焦点判断),就会不停地向命令目标询问,命令目标就会不停地发送PreviewCanExecute和CanExecute事件,这两个附加事件就会沿着元素树向上传递,然后被命令关联捕获,命令关联将命令能不能发送报告给命令。若可以发送命令,则命令源将发送命令给命令目标,命令目标会发送PreviewExecuted和Executed事件,这两个附加也会沿着元素树向上传递,然后被命令关联捕获,完成一些后续工作。
2.自定义命令
由上一节我们熟悉了WPF的命令模型,本节我们自己来定义一个命令。我们有这么几种方式来自定义命令:
一是直接实现ICommand接口,这是最彻底的方式;
二是继承自RoutedCommand类和RoutedUICommand类,这种方式可以命令路由;
三是使用RoutedCommand类和RoutedUICommand类实例,严格来讲,这种方式只是命令的应用。
这里,我们以直接实现ICommand接口来自定义一个SayCommand命令:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows.Input; namespace CommandDemo { public class SayCommand:ICommand { Action<string> _executedTargets = delegate { }; Func<bool> _canExecuteTargets = delegate { return false; }; bool _enableState = false;//启用状态 public bool CanExecute(object parameter) { Delegate[] canExecuteTargets = _canExecuteTargets.GetInvocationList(); foreach (Func<bool> item in canExecuteTargets) { bool flag = item.Invoke(); if (flag) { _enableState = true; break; } } return _enableState; } public event EventHandler CanExecuteChanged = delegate { }; public void Execute(object parameter) { if (_enableState) _executedTargets.Invoke(parameter == null ? null : parameter.ToString()); } public event Action<string> ExecutedTargets { add { _executedTargets += value; } remove { _executedTargets -= value; } } public event Func<bool> CanExecuteTargets { add { _canExecuteTargets += value; CanExecuteChanged.Invoke(this, EventArgs.Empty); } remove { _canExecuteTargets -= value; CanExecuteChanged.Invoke(this, EventArgs.Empty); } } } }
Xaml代码:
<Window x:Class="CommandDemo.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:CommandDemo" Title="Window1" Height="300" Width="300"> <Window.Resources> <local:SayCommand x:Key="cmd" /> </Window.Resources> <Grid> <StackPanel> <Button Content="Say" Command="{StaticResource ResourceKey=cmd}" CommandParameter="Hi,WPF"/> <Button Content="AddCommandHandler" Click="Button_Click" /> </StackPanel> </Grid> </Window>
cs代码:
private void Button_Click(object sender, RoutedEventArgs e) { SayCommand sayCmd = this.FindResource("cmd") as SayCommand; if(sayCmd == null)return; sayCmd.CanExecuteTargets += () => { return true; }; sayCmd.ExecutedTargets += (p) => { MessageBox.Show(p); }; }
当未点击AddCommandHandler按钮时,Say按钮的执行时机和执行操作都是未知的,所以按钮状态为禁用。效果如下:
当点击了AddCommandHandler按钮后,为命令指定了执行时机和执行操作,按钮状态由禁用变为启用。效果如下:
给命令的命令参数(CommandParameter,Object类型)设置了一个值,点击Say按钮,将会显示出来。效果如下:
自定义命令的第二种和第三种差不多,我们以第三种实例化一个RoutedUICommand类来举个例子:
这里定义了一个RoutedUICommand类型的SortCommand命令,用来进行按字段排序,并设置了快捷键。
首先,看下XAML代码:
<Window x:Class="CommandDemo.Window2" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:CommandDemo" Title="Window2" Height="300" Width="300"> <Window.CommandBindings> <CommandBinding Command="local:Window2.SortCommand" CanExecute="CommandBinding_CanExecute" Executed="CommandBinding_Executed" /> <CommandBinding Command="Delete" CanExecute="CommandBinding_CanExecute_1" Executed="CommandBinding_Executed_1" /> </Window.CommandBindings> <Grid> <StackPanel> <ListBox x:Name="stuList"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <StackPanel.ContextMenu> <ContextMenu> <MenuItem Command="Delete" CommandTarget="{Binding PlacementTarget,RelativeSource={RelativeSource AncestorType=ContextMenu}}"/> </ContextMenu> </StackPanel.ContextMenu> <TextBlock Text="{Binding ID}" Foreground="Red" Width="30" /> <TextBlock Text="{Binding Name}" Foreground="Green" Width="60" /> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <Button Command="local:Window2.SortCommand" CommandParameter="Name" Content="{Binding Path=Command.Text,RelativeSource={RelativeSource Mode=Self}}"/> </StackPanel> </Grid> </Window>
在XAML代码中,主要有两个控件:ListBox和Button,两个命令:自定义的SortCommand和内建的ApplicationCommands.Delete命令。
cs代码:
public partial class Window2 : Window { public ObservableCollection<Student> StuList; public Window2() { InitializeComponent(); StuList = DataService.StuList; this.stuList.ItemsSource = StuList; } //实例化一个RoutedUICommand对象来进行升序排列,指定其Text属性,并设置快捷键 public static RoutedUICommand SortCommand = new RoutedUICommand("Sort", "Sort", typeof(Window2) , new InputGestureCollection(new KeyGesture[] { new KeyGesture(key: Key.F3, modifiers: ModifierKeys.None) })); private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e) { string orderBy = e.Parameter == null ? "ID" : e.Parameter.ToString();//默认按ID排序 ICollectionView view = CollectionViewSource.GetDefaultView(this.stuList.ItemsSource); view.SortDescriptions.Clear(); view.SortDescriptions.Add(new SortDescription(orderBy, ListSortDirection.Ascending)); view.Refresh(); } private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e) { if (this.stuList == null || !this.stuList.HasItems) e.CanExecute = false; else e.CanExecute = true; e.Handled = true; } private void CommandBinding_CanExecute_1(object sender, CanExecuteRoutedEventArgs e) { if (this.stuList == null || !this.stuList.HasItems) e.CanExecute = false; else e.CanExecute = true; e.Handled = true; } private void CommandBinding_Executed_1(object sender, ExecutedRoutedEventArgs e) { Student stu = this.stuList.SelectedItem as Student; if (stu != null) { (this.stuList.ItemsSource as ObservableCollection<Student>).Remove(stu); } } }
在cs文件中主要是分别处理的两个命令的CommandBinding的CanExecute和Executed事件,在CanExecute事件中可以对命令源(此处的Button)的状态进行控制。当可用时,点击Buttion或者按下F3键,均可以执行排序命令。效果图如下:
注意:点击Sort按钮和按下F3的区别在于:前者传递了命令参数(按Name升序),而后者没有(按ID升序)。
当通过ContextMenu将这三项删除时,Sort按钮被禁用。效果如下:
3.WPF内建命令
在WPF框架中内置了许多常用的命令,主要有ApplicationCommands、NavigationCommands、EditingCommands、ComponentCommands和MediaCommands这五个静态类的静态属性提供,均为RoutedUICommand实例,如上面用到的ApplicationCommands.Delete,其优点是:
1.将命令源和命令目标解耦
2.可复用、方便使用
来看个简单的例子:
<Window x:Class="CommandDemo.Window3" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window3" Height="300" Width="300"> <StackPanel> <ToolBar> <Button Command="Copy" Content="{Binding Command.Text,RelativeSource={RelativeSource Mode=Self}}" /> <Button Command="Paste" Content="{Binding Command.Text,RelativeSource={RelativeSource Mode=Self}}" /> </ToolBar> <TextBox /> <TextBox /> </StackPanel> </Window>
没有cs代码,将这段代码Copy到XamlPad中就可以直接运行,效果如下:
未进行任何操作之前,两个按钮都是禁用的,因为没有命令目标。当TextBox获得键盘焦点后且剪切板有文本,粘贴按钮就会被启用,否则被禁用。效果如下:
当选中TextBox的文本时,复制按钮被启用。点击复制按钮,选中的文本存储到了剪切板,当下面的TextBox获得焦点时,一直点击粘贴按钮,发现可以一直进行粘贴。这里也行你已经发现了,键盘焦点貌似没切换啊。其实,键盘焦点时有切换的,只不过在点击粘贴按钮后,TextBox又获得了键盘焦点。这里需要弄清楚的是Logical Focus和Keyboard Focus,以及它们在不同Focus Scope的不同行为,以及它们对RoutedCommand的影响,这里不再赘述,请查看下面的blog了解: