一、引言
WPF命令相對來說是一個嶄新的概念,因為命令對於之前的WinForm根本沒有實現這個概念,但是這並不影響我們學習WPF命令,因為設計模式中有命令模式,關於命令模式可以參考我設計模式的博文:http://www.cnblogs.com/zhili/p/CommandPattern.html。命令模式的要旨在於把命令的發送者與命令的執行者之間的依賴關系分割開了。對此,WPF中的命令也是一樣的,WPF命令使得命令源(即命令發送者,也稱調用程序)和命令目標(即命令執行者,也稱處理程序)分離。現在是不是感覺命令是不是親切了點了呢?下面就詳細分享下我對WPF命令的理解。
二、命令是什么呢?
上面通過命令模式引出了WPF命令的要旨,那在WPF中,命令是什么呢?對於程序來說,命令就是一個個任務,例如保存,復制,剪切這些操作都可以理解為一個個命令。即當我們點擊一個復雜按鈕時,此時就相當於發出了一個復制的命令,即告訴文本框執行一個復雜選中內容的操作,然后由文本框控件去完成復制的操作。在這里,復雜按鈕就相當於一個命令發送者,而文本框就是命令的執行者。它們之間通過命令對象分割開了。如果采用事件處理機制的話,此時調用程序與處理程序就相互引用了。
所以對於命令只是從不同角度理解問題的一個詞匯,之前理解點擊一個按鈕,觸發了一個點擊事件,在WPF編程中也可以理解為觸發了一個命令。說到這里,問題又來了,WPF中既然有了命令了?那為什么還需要路由事件呢?對於這個問題,我的理解是,事件和命令是處理問題的兩種方式,它們之間根本不存在沖突的,並且WPF命令中使用了路由事件。所以准確地說WPF命令應該是路由命令。那為什么說WPF命令是路由的呢?這個疑惑將會在WPF命令模型介紹中為大家解答。
另外,WPF命令除了使命令源和命令目標分割的優點外,它還具有另一個優點:
- 使得控件的啟用狀態和相應的命令狀態保持同步,即命令被禁用時,此時綁定命令的控件也會被禁用。
三、WPF命令模型
經過前面的介紹,大家應該已經命令了WPF命令吧。即命令就是一個操作,任務。接下來就要詳細介紹了WPF命令模型了。
WPF命令模型具有4個重要元素:
- 命令——命令表示一個程序任務,並且可跟蹤該任務是否能被執行。然而,命令實際上不包含執行應用程序的代碼,真正處理程序在命令目標中。
- 命令源——命令源觸發命令,即命令的發送者。例如Button、MenuItem等控件都是命令源,單擊它們都會執行綁定的命令。
- 命令目標——命令目標是在其中執行命令的元素。如Copy命令可以在TextBox控件中復制文本。
- 命令綁定——前面說過,命令是不包含執行程序的代碼的,真正處理程序存在於命令目標中。那命令是怎樣映射到處理程序中的呢?這個過程就是通過命令綁定來完成的,命令綁定完成的就是紅娘牽線的作用。
WPF命令模型的核心就在於ICommand接口了,該接口定義命令的工作原理。該接口的定義如下所示:
public interface ICommand { // Events event EventHandler CanExecuteChanged; // Methods bool CanExecute(object parameter); void Execute(object parameter); }
該接口包括2個方法和一個事件。CanExecute方法返回命令的狀態——指示命令是否可執行,例如,文本框中沒有選擇任何文本,此時Copy命令是不用的,CanExecute則返回為false。
Execute方法就是命令執行的方法,即處理程序。當命令狀態改變時,會觸發CanExecuteChanged事件。
當自定義命令時,不會直接去實現ICommand接口。而是使用RoutedCommand類,該類實是WPF中唯一現了ICommand接口的類。所有WPF命令都是RoutedCommand類或其派生類的實例。然而程序中處理的大部分命令不是RoutedCommand對象,而是RoutedUICommand對象。RoutedUICommand類派生與RoutedCommand類。
接下來介紹下為什么說WPF命令是路由的呢?實際上,RoutedCommand上Execute和CanExecute方法並沒有包含命令的處理邏輯,而是將觸發遍歷元素樹的事件來查找具有CommandBinding的對象。而真正命令的處理程序包含在CommandBinding的事件處理程序中。所以說WPF命令是路由命令。該事件會在元素樹上查找CommandBinding對象,然后去調用CommandBinding的CanExecute和Execute來判斷是否可執行命令和如何執行命令。那這個查找方向是怎樣的呢?對於位於工具欄、菜單欄或元素的FocusManager.IsFocusScope設置為”true“是從元素樹上根元素(一般指窗口元素)向元素方向向下查找,對於其他元素是驗證元素樹根方向向上查找。
WPF中提供了一組已定義命令,命令包括以下類:ApplicationCommands、NavigationCommands、MediaCommands、EditingCommands 以及ComponentCommands。 這些類提供諸如 Cut、BrowseBack、BrowseForward、Play、Stop 和 Pause 等命令。
四、使用命令
前面都是介紹了一些命令的理論知識,下面介紹了如何使用WPF命令來完成任務。XAML具體實現代碼如下所示:
1 <Window x:Class="WPFCommand.MainWindow" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 Title="MainWindow" Height="200" Width="300"> 5 <!--定義窗口命令綁定,綁定的命令是New命令,處理程序是NewCommand--> 6 <Window.CommandBindings> 7 <CommandBinding Command="ApplicationCommands.New" Executed="NewCommand"/> 8 </Window.CommandBindings> 9 10 <StackPanel> 11 <Menu> 12 <MenuItem Header="File"> 13 <!--WPF內置命令都可以采用其縮寫形式--> 14 <MenuItem Command="New"></MenuItem> 15 </MenuItem> 16 </Menu> 17 18 <!--獲得命令文本的兩種方式--> 19 <!--直接從靜態的命令對象中提取文本--> 20 <Button Margin="5" Padding="5" Command="ApplicationCommands.New" ToolTip="{x:Static ApplicationCommands.New}">New</Button> 21 22 <!--使用數據綁定,獲得正在使用的Command對象,並提取其Text屬性--> 23 <Button Margin="5" Padding="5" Command="ApplicationCommands.New" Content="{Binding RelativeSource={RelativeSource Self},Path=Command.Text}"/> 24 <Button Margin="5" Padding="5" Visibility="Visible" Click="cmdDoCommand_Click" >DoCommand</Button> 25 </StackPanel> 26 </Window>
其對應的后台代碼實現如下所示:
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); //// 后台代碼創建命令綁定 //CommandBinding bindingNew = new CommandBinding(ApplicationCommands.New); //bindingNew.Executed += NewCommand; //// 將創建的命令綁定添加到窗口的CommandBindings集合中 //this.CommandBindings.Add(bindingNew); } private void NewCommand(object sender, ExecutedRoutedEventArgs e) { MessageBox.Show("New 命令被觸發了,命令源是:" + e.Source.ToString()); } private void cmdDoCommand_Click(object sender, RoutedEventArgs e) { // 直接調用命令的兩種方式 ApplicationCommands.New.Execute(null, (Button)sender); //this.CommandBindings[0].Command.Execute(null); } }
上面程序的運行結果如下圖所示:
五、自定義命令
在開發過程中,自然少不了自定義命令來完成內置命令所沒有提供的任務。下面通過一個例子來演示如何創建一個自定義命令。
首先,定義一個Requery命令,具體的實現如下所示:
public class DataCommands { private static RoutedUICommand requery; static DataCommands() { InputGestureCollection inputs = new InputGestureCollection(); inputs.Add(new KeyGesture(Key.R, ModifierKeys.Control, "Ctrl+R")); requery = new RoutedUICommand( "Requery", "Requery", typeof(DataCommands), inputs); } public static RoutedUICommand Requery { get { return requery; } } }
上面代碼實現了一個Requery命令,為了演示效果,我們需要把該命令應用到XAML標簽上,具體的XAML代碼如下所示:
<!--要使用自定義命令,首先需要將.NET命名空間映射為XAML名稱空間,這里映射的命名空間為local--> <Window x:Class="WPFCommand.CustomCommand" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WPFCommand" Title="CustomCommand" Height="300" Width="300" > <Window.CommandBindings> <!--定義命令綁定--> <CommandBinding Command="local:CustomCommands.Requery" Executed="RequeryCommand_Execute"/> </Window.CommandBindings> <StackPanel> <!--應用命令--> <Button Margin="5" Command="local:CustomCommands.Requery" Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"></Button> </StackPanel> </Window>
接下來,看看程序的運行效果,具體的運行結果如下圖所示:
六、實現可撤銷的命令程序
WPF命令模型缺少的一個特征就是Undo命令,盡管提供了一個ApplicationCommands.Undo命令,但是該命令通常被用於編輯控件,如TextBox控件。如果希望支持應用程序范圍內的Undo操作,就需要在內部跟蹤以前的命令,並且觸發Undo操作時還原該命令。這個實現原理就是保持用一個集合對象保存之前所有執行過的命令,當觸發Undo操作時,還要上一個命令的狀態。這里除了需要保存執行過的命令外,還需要保存觸發命令的控件以及狀態,所以我們需要抽象出一個類來保存這些屬性,我們取名這個類為CommandHistoryItem。為了保存命令和命令的狀態,自然就需要在完成命令之前進行保存,所以自然聯想到是否有Preview之類的事件呢?實際上確實有,這個事件就是PreviewExecutedEvent,所以我們需要在窗口加載完成后把這個事件注冊到窗口上,這里在觸發這個事件的時候就可以保存即將要執行的命令、命令源和命令源的內容。另外,之前的命令自然需要保存到一個列表中,這里使用ListBox控件作為這個列表,如果不希望用戶在界面上看到之前的命令列表的話,也可以使用List等集合容器。
上面講解完了主要實現思路之后,下面我們梳理下實現思路:
- 抽象一個CommandHistoryItem來保存命令相關的屬性。
- 注冊PreviewExecutedEvent事件,為了在命令執行完之前保存命令、命令源以及命令源當前的狀態。
- 在PreviewExecutedEvent事件處理程序中,把命令相關屬性添加到ListBox列表中。
- 當執行撤銷操作時,可以從ListBox.Items列表中取出上一個執行的命令進行恢復之前命令的狀態。
有了上面的實現思路之后,實現這個可撤銷的命令程序也就是碼代碼的過程了。具體的后台代碼實現如下所示:
1 public partial class CommandsMonitor : Window 2 { 3 private static RoutedUICommand undo; 4 public static RoutedUICommand Undo 5 { 6 get { return CommandsMonitor.undo; } 7 } 8 9 static CommandsMonitor() 10 { 11 undo = new RoutedUICommand("Undo", "Undo", typeof(CommandsMonitor)); 12 } 13 14 public CommandsMonitor() 15 { 16 InitializeComponent(); 17 // 按下菜單欄按鈕時,PreviewExecutedEvent事件會被觸發2次,即CommandExecuted事件處理程序被觸發了2次 18 // 一次是菜單欄按鈕本身,一次是目標源觸發命令的執行,所以在CommandExecuted要過濾掉不關心的命令源 19 this.AddHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(CommandExecuted)); 20 } 21 22 public void CommandExecuted(object sender, ExecutedRoutedEventArgs e) 23 { 24 // 過濾掉命令源是菜單按鈕的,因為我們只關心Textbox觸發的命令 25 if (e.Source is ICommandSource) 26 return; 27 // 過濾掉Undo命令 28 if (e.Command == CommandsMonitor.Undo) 29 return; 30 31 TextBox txt = e.Source as TextBox; 32 if (txt != null) 33 { 34 RoutedCommand cmd = e.Command as RoutedCommand; 35 if (cmd != null) 36 { 37 CommandHistoryItem historyItem = new CommandHistoryItem() 38 { 39 CommandName = cmd.Name, 40 ElementActedOn = txt, 41 PropertyActedOn = "Text", 42 PreviousState = txt.Text 43 }; 44 45 ListBoxItem item = new ListBoxItem(); 46 item.Content = historyItem; 47 lstHistory.Items.Add(item); 48 } 49 50 } 51 } 52 53 private void window_Unloaded(object sender, RoutedEventArgs e) 54 { 55 this.RemoveHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(CommandExecuted)); 56 } 57 58 private void UndoCommand_Executed(object sender, RoutedEventArgs e) 59 { 60 ListBoxItem item = lstHistory.Items[lstHistory.Items.Count - 1] as ListBoxItem; 61 62 CommandHistoryItem historyItem = item.Content as CommandHistoryItem; 63 if (historyItem == null) 64 { 65 return; 66 } 67 68 if (historyItem.CanUndo) 69 { 70 historyItem.Undo(); 71 } 72 lstHistory.Items.Remove(item); 73 } 74 75 private void UndoCommand_CanExecuted(object sender, CanExecuteRoutedEventArgs e) 76 { 77 if (lstHistory == null || lstHistory.Items.Count == 0) 78 { 79 e.CanExecute = false; 80 } 81 else 82 { 83 e.CanExecute = true; 84 } 85 } 86 } 87 88 public class CommandHistoryItem 89 { 90 public String CommandName { get; set; } 91 public UIElement ElementActedOn { get; set; } 92 93 public string PropertyActedOn { get; set; } 94 95 public object PreviousState { get; set; } 96 97 public bool CanUndo 98 { 99 get { return (ElementActedOn != null && PropertyActedOn != ""); } 100 } 101 102 public void Undo() 103 { 104 Type elementType = ElementActedOn.GetType(); 105 PropertyInfo property = elementType.GetProperty(PropertyActedOn); 106 property.SetValue(ElementActedOn, PreviousState, null); 107 } 108 } 109 }
其對應的XAML界面設計代碼如下所示:
<Window x:Class="WPFCommand.CommandsMonitor" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="CommandsMonitor" Height="300" Width="350" xmlns:local="clr-namespace:WPFCommand" Unloaded="window_Unloaded"> <Window.CommandBindings> <CommandBinding Command="local:CommandsMonitor.Undo" Executed="UndoCommand_Executed" CanExecute="UndoCommand_CanExecuted"/> </Window.CommandBindings> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <ToolBarTray Grid.Row="0"> <ToolBar> <Button Command="ApplicationCommands.Cut">Cut</Button> <Button Command="ApplicationCommands.Copy">Copy</Button> <Button Command="ApplicationCommands.Paste">Paste</Button> </ToolBar> <ToolBar> <Button Command="local:CommandsMonitor.Undo">Reverse Last Command</Button> </ToolBar> </ToolBarTray> <TextBox Margin="5" Grid.Row="1" TextWrapping="Wrap" AcceptsReturn="True"> </TextBox> <TextBox Margin="5" Grid.Row="2" TextWrapping="Wrap" AcceptsReturn="True"> </TextBox> <ListBox Grid.Row="3" Name="lstHistory" Margin="5" DisplayMemberPath="CommandName"></ListBox> </Grid> </Window>
上面程序的運行效果如下圖所示:
七、小結
到這里,WPF命令的內容就介紹結束了,關於命令主要記住命令模型四要素——命令、命令綁定、命令源和命令目標。后面繼續為大家分享WPF的資源和樣式的內容。
本文所有源碼:WPFCommandDemo.zip