- WPF命令模型
ICommand接口
WPF命令模型的核心是System.Windows.Input.ICommand接口,該接口定義了命令的工作原理,它包含了兩個方法和一個事件:
public interface ICommand { void Execute(object parameter); //定義在調用此命令時調用的方法。 bool CanExecute(object parameter); //此方法返回命令的狀態,如果命令可用則返回true,否則返回false. event EventHandler CanExecuteChanged; //當命令狀態改變時,引發該事件。 }
RoutedCommand類
當創建自己的命令時,不會直接實現ICommand接口,而是使用System.Windows.Input.RoutedCommand類。它是WPF中唯一實例了ICommand接口的類,它為事件冒泡和隧道添加了一些額外的基礎結構。為了支持路由事件,RoutedCommand類私有地實現了ICommand接口,並且添加了ICommand接口方法的一些不同的版本,最明顯的變化是,Execute()和CanExecute()方法使用了一個額外參數。代碼示例如下:public void Execute(object parameter, IInputElement target) { }
public bool CanExecute(object parameter, IInputElement target) { }參數target是開始處理事件的元素,事件從target元素開始,然后冒泡至高層的容器,直到應用程序為了執行合適的任務而處理了事件。
RoutedUICommand類
RoutedCommand類還引入了三個屬性:Name(命令名稱)、OwnerType(包含命令的類)及InputGestures集合(可以被用於觸發命令的按鍵或鼠標操作)。
RoutedUICommand類只增加了一個屬性 Text,它是命令顯示的文本。在程序中處理的大部分命令不是RoutedCommand對象,而是RoutedUICommand類的實例,RoutedUICommand類繼承自RoutedCommand類。而WPF提供的所有預先構建好的命令都是RoutedUICommand對象。RoutedUICommand類用於具有文本的命令,這些文本顯示在用戶界面中的某些地方(如菜單項文本,工具欄按鈕的工具提示)。
命令庫
因為每個應用程序可能都有大量的命令,且對於許多不同的應用程序,很多命令是通用的,為了減少創建這些命令所需要的工作,WPF提供了一個基本命令庫,這些命令通過以下5個專門的靜態類的靜態屬性提供:
許多命令對象都是有一個額外的特征:默認輸入綁定,例如,ApplicationCommands.Open命令被映射到Ctrl+O組合鍵,只要將命令綁定到一個命令源,並為窗口添加該命令源,這個組合鍵就會被激活,即使沒有在用戶界面的任何地方顯示該命令也同樣如此。 - 命令源
命令源是一個實現了ICommandSource接口的控件,它定義了三個屬性:
例如,下面的按鈕使用Command屬性連接到ApplicationCommands.New命令:
<Button Command="New">New</Button>
此時,會看到按鈕是被禁用的狀態,這是因為按鈕查詢到命令還沒有進行操作綁定,命令的狀態為不可用,所以按鈕也設置為不可用。
-
為命令進行操作綁定
下在的代碼片段為New命令創建綁定,可將這些代碼添加到窗口的構造函數中:CommandBinding binding = new CommandBinding(ApplicationCommands.New); binding.Executed += new ExecutedRoutedEventHandler(binding_Executed); this.CommandBindings.Add(binding); void binding_Executed(object sender, ExecutedRoutedEventArgs e) { MessageBox.Show("New command triggered by " + e.Source.ToString()); }
盡管習慣上為窗口創建所有綁定,但CommandBindings屬性實際上是在UIElement基類中定義的,所以任何元素都支持該屬性,但為了得到最大的靈活性,命令綁定通常被添加到頂級窗口,如果希望在多個窗口中使用相同的命令,就需要在這些窗口中分別創建命令綁定。
以上的命令綁定是使用代碼生成的,但,如果希望精簡代碼隱藏文件,使用XAML以聲明方式關聯命令也很容易,如下所示:
<Window.CommandBindings> <CommandBinding Command="ApplicationCommands.New" Executed="binding_Executed"></CommandBinding> </Window.CommandBindings> <Button Command="ApplicationCommands.New">New</Button>
-
使用多命令源,如下是為命令New創建了一個MenuItem的命令源:
<Menu> <MenuItem Header="File"> <MenuItem Command="New"></MenuItem> </MenuItem> </Menu>
注意,沒有為New命令的MenuItem對象設置Header屬性,這是因為MenuItem類足夠智能,如果沒有設置Header屬性,它將從命令中提取文本(Button不具有此特性)。雖然該特性帶來的便利看起來很小,但是如果計划使用不同的語言本地化應用程序,這一特性就很重要了。MunuItem類還有另一個功能,它能夠自動提取Command.InputBindings集合中的第一個快捷鍵,對於以上的New命令,在菜單旁邊會顯示快捷鍵:Ctrl+N。
-
使Button這種不能自動提取命令文本的控件來提取命令文本,有兩種技術來重用命令文本,一種是直接從靜態的命令對象中提取文本,XAML可以使用Static標記擴展完成這一任務,該方法的問題在於它只是調用命令對象的ToString()方法,因此,得到的是命令的名稱,而不是命令的文本。最好的方法是使用數據綁定表達式,以下第二條代碼示例綁定表達式綁定到當前元素,獲取正在使用的Command對象,並且提取其Text屬性:
<Button Command="ApplicationCommands.New" Content="{x:Static ApplicationCommands.New}"></Button> <Button Command="ApplicationCommands.New" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"></Button>
-
直接調用命令
不是只有實現了ICommandSource接口的類才能觸發命令的執行,也可以使用Execute()方法直接調用來自任何事件處理程序的方法:ApplicationCommands.New.Execute(null,targetElement);
targetElement是WPF開始查找命令綁定的地方。可以使用包含窗口(具有命令綁定)或嵌套的元素(實際引發事件的元素)。也可以在關聯的CommandBinding對象中調用Execute()方法,對於這種情況,不需要提供目標元素,因為會自動公開正在使用的CommandBindings集合的元素設置為目標元素:this.CommandBindings[0].Command.Execute(null);
-
禁用命令
例如有一個由菜單、工具欄及一個大的文本框構成的文本編輯器的應用程序,該應用程序可以打開文件,創建新的文檔以及保存所進行的操作。在應用程序中,只有文本框中的內容發生了變化才啟用Save命令,我們可以在代碼中使用一個Boolean變量isUpdate來跟蹤是否發生了變化。當文本發生了變化時設置標志。private bool isUpdate = false; private void txt_TextChanged(object sender, TextChangedEventArgs e) { isUpdate = true; }
現在需要從窗口向命令綁定傳遞信息,從而使連接的控件可以根據需要進行更新,技巧是處理命令綁定的CanExecute事件,代碼如下:
CommandBinding binding = new CommandBinding(ApplicationCommands.Save); binding.Executed += new ExecutedRoutedEventHandler(binding_Executed); binding.CanExecute += new CanExecuteRoutedEventHandler(binding_CanExecute); this.CommandBindings.Add(binding);
或者使用聲明方式:
<CommandBinding Command="Save" Executed="CommandBinding_Executed_1" CanExecute="binding_CanExecute"></CommandBinding>
在事件處理程序中,只需要檢查isUpdate變量,並設置CanExecuteRoutedEventArgs.CanExecute屬性:
void binding_CanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = isUpdate; }
如果isUpdate的值為false,就會禁用Save命令,否則會啟用Save命令。
當使用CanExecute事件時,是由WPF負責調用RoutedCommand.CanExecute()方法觸發事件處理程序,並且確定命令的狀態。當WPF命令管理器探測到一個確信是重要的變化時,例如,當焦點從一個控件移動到另一個控件,或者執行了一個命令之后,WPF命令管理器就會完成該工作。控件還能引發CanExecuteChanged事件以通知WPF重新評估命令,例如,當用戶在文本框中按下一個鍵時就會發生該事件,總之,CanExecute事件會被頻繁的觸發,所以不應當在該事件的處理程序中使用耗時的代碼。 然而,其化因素有可能會影響命令的狀態,在以上的示例中,為了響應其它操作,isUpdate標志可能會被修改,如果注意到命令狀態沒有在正確的時間更新,可以強制WPF為所有正在使用的命令調用CanExecute()方法,通過調用靜態的CommandManager.InvalidateRequerySuggested()方法完成該工作。然后命令管理器觸發RequerySuggested事件,通知窗口中的命令源。然后命令源會查詢它們連接的命令並相應地更新它們的狀態。 -
具有內置命令的控件
一些輸入控件自身可以處理命令事件,如TextBox類的Cut、Copy及Paste命令,以及一些來自EditingCommand類的用於選擇文本以及將光標移到不同位置的命令,把此類命令綁定到命令源會自動獲取對應命令的功能,而不需要再為命令綁定操作。如:<ToolBar> <Button Command="Cut" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"></Button> <Button Command="Copy" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"></Button> <Button Command="Paste" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"></Button> </ToolBar>
此外,文本框還處理了CanExecute事件,如果在文本框中當前沒有選中任何內容,剪切和復制命令就會被禁用,當焦點改變到其他不支持這些命令的控件時,這些命令都會被禁用。
在以上代碼中使用了ToolBar控件,它提供了一些內置邏輯,可以將它的子元素的CommandTarget屬性自動設置為具有焦點的控件。但如果在不同的容器(不是ToolBar或Menu控件)中放置按鈕,就不會得到這一優點而按鈕不能正常工作,此時就需要手動設置CommandTarget屬性,為此,必須使用命名目標元素的綁定表達式。如:<StackPanel Grid.Row="1"> <Button Command="Cut" CommandTarget="{Binding ElementName=txt}" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"/> <Button Command="Copy" CommandTarget="{Binding ElementName=txt}" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"/> <Button Command="Paste" CommandTarget="{Binding ElementName=txt}" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"/> </StackPanel>
另一個較簡單的選擇是使用FocusManager.IsFocusScope附加屬性創建新的焦點范圍,當命令觸發時,該焦點范圍會通知WPF在父元素的焦點范圍中查找元素:
<StackPanel FocusManager.IsFocusScope="True" Grid.Row="1"> <Button Command="Cut" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"></Button> <Button Command="Copy" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"></Button> <Button Command="Paste" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"></Button> </StackPanel>
在有些情況下,可能發現控件支持內置命令,但不想啟用它,此時有三種方法可以禁用命令:
-
理想情況下,控件會提供用於關閉命令支持的屬性,例如TextBox控件的IsUndoEnabled屬性。
-
如果控件沒有提供關閉命令支持的屬性,還可以為希望禁用的命令添加一個新的命令綁定,然后該命令綁定可以提供新的事件處理程序。且總是將CanExecute屬性設置為false,下面是一個使用該技術刪除文本框Cut特性支持的示例:
CommandBinding binding = new CommandBinding(ApplicationCommands.Cut, null, SuppressCommand); this.CommandBindings.Add(binding); private void SuppressCommand(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute= false; e.Handled= true; }
上面的代碼設置了Handled標志,以阻止文本框自我執行計算,而文本框可能將CanExecute屬性設置為true.
-
使用InputBinding集合刪除觸發命令的輸入,例如,可以使用代碼禁用觸發TextBox控件中Cut命令的Ctrl+X組合鍵,如下所示:
KeyBinding keyBinding = new KeyBinding(ApplicationCommands.NotACommand, Key.X, ModifierKeys.Control); txt.InputBindings.Add(keyBinding);
ApplicationCommands.NotACommand命令不做任何事件,它專門用於禁用輸入綁定。
文本框默認顯示上下文菜單,可以通過將ContextMenu屬性設置為null刪除上下文本菜單:<TextBoxGrid.Row="3" Name="txt" ContextMenu="{x:Null}" TextWrapping="Wrap" TextChanged="txt_TextChanged" />
-
-
自定義命令
下面的示例定義了一個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", typeof(DataCommands), inputs); } public static RoutedUICommand Requery //通過靜態屬性提供自定義的命令 { get { return requery; } } }
使用Requery命令時需要將它的.Net名稱空間映射為一個XML名稱空間,XAML代碼如下:
<Window x:Class="WpfApplication1.Test4" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfApplication1" Title="Test4" Height="300" Width="300"> <Window.CommandBindings> <CommandBinding Command="local:DataCommands.Requery" Executed="Requery_Executed"></CommandBinding> </Window.CommandBindings> <Grid> <Button Command="local:DataCommands.Requery" CommandParameter="ai" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}" /> </Grid> </Window>
在以上代碼中使用CommandParameter為命令傳遞了參數,命令的事件處理方法中就可以使用Parameter屬性獲取該參數:
private void Requery_Executed(object sender, ExecutedRoutedEventArgs e) { string parameters = e.Parameter.ToString(); }
-
在不同的位置使用相同的命令
在WPF命令模型中,一個重要的思想是Scope。盡管每個命令只有一個副本,但是使用命令的效果卻會根據觸發命令位置而不同,例如,如果有兩個文本框,它們都支持Cut、Copy、Paste命令,操作只會在當前具有焦點的文本框中發生。但是對於自定實現的命令如New、Open、Requery及Save命令就區分不出是哪一個文本框觸發的命令,盡管ExecuteRoutedEventArgs對象提供了Source屬性,但是該屬性反映的是具有命令綁定的元素,也就是容器窗口。此問題的解決方法是使用文本框的CommandBindings集合為每個文本框分別綁定命令。 -
跟蹤和翻轉命令
創建自己的用於支持命令翻轉的數據結構,示例中定義一個名為CommandHistoryItem的類用於存儲命令狀態:public class CommandHistoryItem { public string CommandName { get; set; } //命令名稱 public UIElement ElementActedOn { get; set; } //執行命令的元素 public string PropertyActedOn { get; set; } //在目標元素中被改變了的屬性 public object PreviousState { get; set; } //用於保存受影響元素以前狀態的對象 public CommandHistoryItem(string commandName) : this(commandName, null, "", null) { } public CommandHistoryItem(string commandName, UIElement elementActedOn, string propertyActed, object previousState) { this.CommandName = commandName; this.ElementActedOn = elementActedOn; this.PropertyActedOn = propertyActed; this.PreviousState = previousState; } public bool CanUndo { get { return (ElementActedOn != null && PropertyActedOn != ""); } } /// <summary> /// 使用反射為修改過的屬性應用以前的值 /// </summary> public void Undo() { Type elementType = ElementActedOn.GetType(); PropertyInfo property = elementType.GetProperty(PropertyActedOn); property.SetValue(ElementActedOn, PreviousState, null); } }
需要自定義一個執行應用程序范圍內翻轉操作的命令,如下所示:
private static RoutedUICommand applicationUndo; public static RoutedUICommand ApplicationUndo { get { return applicationUndo; } } static ApplicationUndoDemo() { applicationUndo = new RoutedUICommand("Applicaion Undo", "ApplicationUndo", typeof(ApplicationUndoDemo)); }
可以使用CommandManager類來跟蹤任何命令的執行情況,它提供了幾個靜態事件:Executed及PreviewExecuted,無論何時,當執行任何一個命令時都會觸發它們。 盡管CommandManager類掛起了Executed事件,但是仍然可以使用UIElement.AddHandler()方法關聯事件處理程序,並且為可選的第三個參數傳遞true值,從而允許接收事件。下面的代碼在窗口的構造函數中關聯PreviewExecuted事件處理程序,且在關閉窗口時解除關聯:
public ApplicationUndoDemo() { InitializeComponent(); this.AddHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(CommandPreviewExecute),true); } private void Window_Unloaded(object sender, RoutedEventArgs e) { this.RemoveHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(CommandPreviewExecute)); }
當觸發PreviewExecute事件時,需要確定准備執行的命令是否是我們所關心的,如果是就創建CommandHistoryItem對象,且將其添加到歷史命令集合中。
private void CommandExecuted(object sender, ExecutedRoutedEventArgs e) { // Ignore menu button source. if (e.Source is ICommandSource) return; // Ignore the ApplicationUndo command. if (e.Command == MonitorCommands.ApplicationUndo) return; // Could filter for commands you want to add to the stack // (for example, not selection events). TextBox txt = e.Source as TextBox; if (txt != null) { RoutedCommand cmd = (RoutedCommand)e.Command; CommandHistoryItem historyItem = new CommandHistoryItem( cmd.Name, txt, "Text", txt.Text); ListBoxItem item = new ListBoxItem(); item.Content = historyItem; lstHistory.Items.Add(historyItem); // CommandManager.InvalidateRequerySuggested(); } }
使用CanExecute事件處理程序,確保只有當Undo歷史中有一項時,才能執行翻轉操作:
<Window.CommandBindings> <CommandBinding Command="local:ApplicationUndoDemo.ApplicationUndo" Executed="CommandBinding_Executed" CanExecute="CommandBinding_CanExecute"></CommandBinding> </Window.CommandBindings>
private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e) { //不知lstHistory.Items[lstHistory.Items.Count - 1]為什么強制轉化不成CommandHistoryItem,有待解決 CommandHistoryItem historyItem = (CommandHistoryItem)lstHistory.Items[lstHistory.Items.Count - 1]; if (historyItem.CanUndo) historyItem.Undo(); lstHistory.Items.Remove(historyItem); } private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e) { if (lstHistory == null || lstHistory.Items.Count == 0) e.CanExecute = false; else e.CanExecute = true; }