在上一篇中,我們學習了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了解: