前面章節已經對命令進行了深入分析,分析了基類和介面以及WPF提供的命令庫,但尚未例舉任何使用這些命令的例子,
如前所述,RoutedUICommand類沒有任何硬編碼的功能,而是只表達命令,為觸發命令,需要有命令源(也可使用代碼),為回應命令,需要有命令系結,命令系結將執行轉發給普遍的事件處理程式,
一、命令源
命令庫中的命令始終可用,觸發他們的最簡單的方法是將它們關聯到實作了ICommandSource介面的控制元件,其中包括繼承自ButtonBase類的控制元件(Button和CheckBox等)、單獨的ListBoxItem物件、HyperLink以及MenuItem,
ICommandSource介面定義了三個屬性,如下表所示,
表 ICommandSource介面的屬性

例如,下面的按鈕使用Command屬性連接到ApplicationCommands.New命令:
<Button Command="ApplicationCommands.New">New</Button>
WPF的智能程度足夠高,它能查找前面介紹的所有5個命令容器類,這意味著可使用下面的縮寫的形式:
<Button Command="New">New</Button>
然而,由于沒有指明包含命令的類,這種語法不夠明確、不夠清晰,
二、命令系結
當將命令關聯到命令源時,會看到一些有趣的現象,命令源將會被自動禁用,
例如,如果創建上一節提到的New按鈕,該按鈕的顏色就會變淺并且不能被單擊,就像將IsEnabled屬性設定為false那樣(如下圖所示),這是因為按鈕已經查詢了命令的狀態,而且由于命令還沒有與其關聯的系結,所以按鈕被認為是禁用的,

為改變這種狀態,需要為命令創建系結以明確以下三件事:
當命令被觸發時執行什么操作,
如何確定命令是否能夠被執行(這是可選的,如果未提供這一細節,只要提供了關聯的事件處理程式,命令總是可用),
命令在何處起作用,例如,命令可被限制在單個按鈕中使用,或在整個視窗中使用(這種情況更常見),
下面的代碼片段為New命令創建系結,可將這些代碼添加到視窗的建構式中:
//Create the binding CommandBinding binding=new CommandBinding(ApplicationCommands.New); //Attach the event handler binding.Executed+=NewCommand_Executed; //Register the binding this.CommandBinding.Add(binding);
注意,上面創建的CommandBinding物件唄添加到包含視窗的CommandBindings集合中,這通過事件冒泡進行作業,實際上,當單擊按鈕時,CommandBinding.Executed事件從按鈕冒泡到包含元素,
盡管習慣上為視窗添加所有系結,但CommandBindings屬性實際是在UIElement基類中定義的,這意味著任何元素都支持該屬性,例如,如果將命令系結直接添加到使用它的按鈕中,這個示例仍作業的很好(盡管不能在將該系結重用與其他高級元素),為得到最大的靈活性,命令系結通常被添加到頂級視窗,如果希望在多個視窗中使用相同相同的命令,需要在這些視窗中分別創建命令系結,
上面的代碼假定在同一個類中已有名為NewCommand_Executed的事件處理程式,該處理程式已經準備好接收命令,下面是一個示例,該例包含一些顯示命令源的簡單代碼:
private void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e) { MessageBox.Show("New command triggered by " + e.Source.ToString()); }
現在,如果允許應用成功需,按鈕處于啟用狀態,如果單擊按鈕,就會觸發Executed事件,該事件冒泡至視窗,并被上面給出的NewCommand_Executed()事件處理程式程式處理,這是,WPF會告知事件源(按鈕),通過ExecutedRoutedEventArgs物件還可獲得被呼叫的命令的參考(ExecutedRoutedEventArgs.Command),以及所有同時傳遞的額外資料(ExecutedRoutedEventArgs.Parameter),在該例中,因為沒有傳遞任何額外的資料,所以引數為null(如果希望傳遞附加資料,贏設定命令源的CommandParameter屬性;并且如果希望傳遞一些來自另一個控制元件的資訊,還需要使用資料系結運算式設定CommandParameter屬性),
在上面的示例中,使用代碼生成了命令系結,然而,如果希望精簡代碼隱藏檔案,使用XAML以生命方式關聯命令同樣容易,下面是所需的標記:
<Window x:Class="Commands.TestNewCommand" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="TestNewCommand" Height="300" Width="300"> <Window.CommandBindings> <CommandBinding Command="ApplicationCommands.New" Executed="NewCommand"></CommandBinding> </Window.CommandBindings> <StackPanel> <Button Margin="5" Padding="5" Command="ApplicationCommands.New">New</Button> </StackPanel> </Window>
使用Visual Studio沒有為定義命令系結提供任何設計時支持,對連接控制元件和命令的支持也較弱,可使用Properties視窗設定控制元件的Command屬性,但需要輸入正確的命令名稱——由于并未提供包含命令的下拉串列,因此不能方便地從串列中選擇命令,
三、使用多命令源
上面示例中觸發普通事件的方式看起來不那么直接,然而,當添加使用相同命令的更多控制元件時,額外命令層的意義就提現出出來了,例如,可添加如下也使用New命令的選單項:
<Menu > <MenuItem Header="File"> <MenuItem Command="New"></MenuItem> </MenuItem> </Menu>
注意,New命令的這個MenuItem物件沒有設定Header屬性,這是因為MenuItem類足夠智能,如果沒有設定Header屬性,它將從命令中提取文本(Button控制元件不具有這一特性),雖然該特性帶帶來的便利看起來不大,但如果計劃使用不同的語言本地化應用程式,這一特性就很重要了,在這種情況下,只需要在一個地方修改文本即可(通過設定命令的Text屬性),這比在整個視窗中進行跟蹤更容易,
MenuItem類還有一項功能:能自動提取Command.InputBinding集合中的第一個快捷鍵(如果存在的話),對于ApplicationCommands.New命令物件,這意味著在選單文本的旁邊會顯示Ctrl+N快捷鍵(如下圖所示),

四、微調命令文本
既然選單具有自動提取命令項文本的功能,肯恩改回好奇其他ICommandSource類是否也具有類似功能,如Button控制元件,
可以使用兩種技術重用命令文本,一種選擇是直接從靜態命令物件中提取文本,XAML可使用Static標記擴展完成這一任務,下面的示例獲取命令名New,并將它作為按鈕的文本:
<Button Margin="5" Padding="5" Command="New" Content="{x:Static ApplicationCommands.New}"></Button>
該方法的問題在于,它指示呼叫命令物件命令物件的ToString()方法,因此,得到的是命令名,而不是命令的文本(對于哪些名稱中包含多個單詞的命令,使用命令文本更好些,因為命令文本包含空格),雖然解決這一問題,但需要完成更多作業,這種方法還存在一個問題,一個按鈕將同一個命令使用兩次,可能會無意間從錯誤的命令獲取文本),
更好的解決方案是使用資料系結運算式,在此使用的資料系結有些不尋常,因為他系結到當前元素嗎,獲取正在使用的Command物件,并提取Text屬性,下面是非常復雜的語法:
<Button Margin="5" Padding="5" Command="New" Content="{Binding RelativeSource={RelativeSource Self},Path=Command.Text}"></Button>
可通過另一種更具想象力的方式使用該技術,例如,可使用一幅小影像設定按鈕的內容,而在按鈕的工具提示中使用資料系結運算式顯示命令名:
<Button Margin="5" Padding="5" Command="ApplicationCommands.New" ToolTip="{Binding RelativeSource={RelativeSource Self},Path=Command.Text}"> <Image Source="redx.jpg" Stretch="None"></Image> </Button>
按鈕的內容可以是形狀,也可以是顯示為縮略圖的位圖,
顯然,這種方法比直接在標記中放置命令文本更麻煩些,然而,如果準備使用不同的語言本地化應用程式,使用這個方法是值得的,當應用程式啟動時,只需要為所有命令設定命令文本即可(如果在創建了命令系結后改變命令文本,不會產生任何效果,因為Text屬性不是依賴項屬性,所以沒有自動的更改通知來更新用戶界面),
五、直接呼叫命令
并非只能使用實作了ICommandSource介面的類來觸發希望執行的命令,也可以用Execute()方法直接呼叫來自任何事件處理程式的方法,這時需要傳遞引數值(或null參考)和對目標元素的參考:
ApplicationCommands.New.Execute(null,targetElement);
目標元素是WPF開始查找命令系結的地方,可使用包含視窗(具有命令系結)或嵌套的元素(例如,實際引發事件的元素),
也可在關聯的CommandBinding物件中呼叫Execute()方法,在這種情況下,不需要提供目標元素,因為會自動將公開正在使用的CommandBindings集合的元素設定為目標元素,
this.CommandBindings[0].Command.Execute(null);
這種方法只使用了半個命令模型,雖然也觸發命令,但不能回應命令的狀態變化,如果希望實作該特性,當命令變為啟用或禁用時,也可能希望處理RoutedCommand.CanExecuteChanged事件進行回應,當引發CanExecuteChanged事件時,需要呼叫RoutedCommand.CanExecute()方法檢查命令是否處于可用狀態,如果命令不可用,可禁用或改變用戶界面中的部分內容,
六、禁用命令
如果想要創建狀態在啟用和禁用之間變化的命令,你將體會到命令模型的真正優勢,例如,分析下圖中顯示的單視窗應用程式,它是有選單、工具列以及大文本框構成的簡單文本編輯器,該應用程式可以打開檔案,創建新的(空白)檔案,以及保存所執行的操作,

在該應用程式中,保持New、Open、Save、SaveAs以及Close命令一直可用是非常合理的,但還有一種設計,只有當某些操作使文本相對于原來的檔案發生了變化時才啟用Save命令,根據約定,可在代碼中使用簡單的Boolean值來跟蹤這一細節:
private bool isDirty = false;
然后當文本發生變化時設定該標志:
private void txt_TextChanged(object sender, RoutedEventArgs e) { isDirty = true; }
現在需要從視窗命令系結傳遞資訊,使鏈接的控制元件可根據需要進行更新,技巧是處理命令系結的CanExecute事件,可通過下面的代碼為該事件關聯事件處理程式:
CommandBinding binding = new CommandBinding(ApplicationCommands.Save); binding.Executed += SaveCommand_Executed; binding.CanExecute += SaveCommand_CanExecute; this.CommandBindings.Add(binding);
或使用宣告方式:
<Window.CommandBindings> <CommandBinding Command="ApplicationCommands.Save" Executed="SaveCommand_Executed" CanExecute="SaveCommand_CanExecute"></CommandBinding> </Window.CommandBindings>
在事件處理程式中,只需要檢查isDirty變數,并相應地設定CanExecuteRoutedEventArgs.CanExecute屬性:
private void SaveCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = isDirty;
}
如果isDirty的值時false,就禁用命令,如果isDirty的值為true,就啟用命令(如果沒有設定CanExecute標志,就會保持最近的值),
當使用CanExecute事件時,還需要理解一個問題,由WPF負責呼叫RoutedCommand.CanExecute()方法來觸發事件處理程式,并確定命令的狀態,當WPF命令管理器探測到某個確信十分重要的變化——例如,當焦點從一個控制元件移到另一個控制元件上時,或執行了某個命令后,WPF命令管理器就會完成該作業,控制元件還能引發CanExecuteChanged事件以通知WPF重新評估命令——例如,當用戶在文本框中按下一個鍵時會發生該事件,總之,CanExecute事件會被頻繁地觸發,并且不應在該事件的處理程式中使用耗時的代碼,
然而,其他因素可能影響命令狀態,在當前示例中,為回應其他操作,可能會修改isDirty標志,如果發現命令狀態未在正確的時間被更新,可強制WPF為所有正在使用的命令呼叫CanExecute()方法,通過呼叫靜態方法CommandManager.InvalidateRequerySuggested()完成該作業,然后命令管理器觸發RequerySuggested事件,通知視窗中的命令源(按鈕、選單項等),此后命令源會重新查詢它們鏈接的命令并相應地更新它們的狀態,
七、具有內置命令的控制元件
一個輸入控制元件可自行處理命令事件,例如,TextBox類處理Cut、Copy以及Paste命令(還有Undo、Redo命令,以及一些來自EditingCommd類的用于選擇文本以及將游標移到不同位置的命令),
當控制元件具有自己的硬編碼命令邏輯時,為使命令作業不需要做其他任何事情,例如,對于上節示例的簡單編輯器,添加如下工具列按鈕,就會自動獲取對剪切、復制和粘貼文本的支持:
<ToolBar> <Button Command="Cut">Cut</Button> <Button Command="Copy">Copy</Button> <Button Command="Paste">Paste</Button> </ToolBar>
現在淡季這些按鈕中的任意一個(當文本框具有焦點時),就可以復制、剪切或從剪貼板粘貼文本,有趣的是,文本框還處理CanExecute事件,如果當前未在文本框中選中任何內容,就會禁用剪切和復制命令,當焦點移到其他不支持這些命令的控制元件時,會自動禁用所有這三個命令(除非關聯自己的CanExecute事件處理程式以啟動這些命令),
該例有一個有趣的細節,Cut、Copy和Paste命令被具有焦點的文本框處理,然而,由工具列上的按鈕觸發的命令時完全獨立的元素,在該例中,這個程序之所以能夠無縫作業,是因為按鈕被放到工具列上,ToolBar類提供了一些內置邏輯,可將其子元素的CommandTarget屬性動態設定為當前具有焦點的控制元件(從技術角度看,ToolBar控制元件一直在關注著其父元素,即視窗,并在背景關系中查找最近具有焦點的控制元件,即文本框,ToolBar控制元件有單獨的焦點范圍(focus scope),并且在其背景關系中按鈕是具有焦點的),
如果在不同容器(不是ToolBar或Menu控制元件)中放置按鈕,就不會獲得這些優勢,這意味著除非手動設定CommanTarget屬性,否則按鈕不能作業,為此,必須使用命令目標元素的系結的運算式,例如,如果文本框被命名為txtDocument,就應該像下面這樣定義按鈕:
<Button Command="Cut" CommandTarget="{Binding ElementName=txtDocument}">Cut</Button> <Button Command="Copy" CommandTarget="{Binding ElementName=txtDocument}">Copy</Button> <Button Command="Paste" CommandTarget="{Binding ElementName=txtDocument}">Paste</Button>
另一個較簡單的選擇是使用附加屬性FocusManager.IsFocusScope創建新的焦點范圍,當觸發命令時,該焦點范圍會通知WPF在父元素的焦點范圍內查找元素:
<StackPanel FocusManager.IsFocusScope="True"> <Button Command="Cut">Cut</Button> <Button Command="Copy">Copy</Button> <Button Command="Paste">Paste</Button> </StackPanel>
該方法還有一個附加優點,即相同的命令可應用于多個控制元件,不像上個示例那樣對CommandTarget進行硬編碼,此外,Menu和ToolBar控制元件默認將FocusManager.IsFocusScope屬性設定為true,但如果希望簡化命令路由行為,不在父元素背景關系中查找具有焦點的元素,也可將該屬性設為false,
在極少數情況下,你可能發現控制元件支持內置命令,而你并不想啟用它,在這種情況下,可以采用三種方法禁用命令,
理想情況下,控制元件提供用于關閉命令支持的屬性,從而確保控制元件移除這些特性并連貫地調整自身,例如,TextBox控制元件提供了IsUndoEnabled屬性,為阻止Undo特性,可將該屬性設定為false(如果IsUndoEnabled屬性為true,Ctrl+Z組合鍵將觸發Undo命令),
如果這種做法行不通,可為希望禁用的命令添加新的命令系結,然后該命令系結可提供新的CanExecute事件處理程式,并總是回應false,下面舉一個使用該技術洗掉文本框Cut特性支持的示例:
CommandBinding commandBinding=new CommandBinding(ApplicationCommands.Cut,null,SuppressCommand); txt.CommandBindings.Add(commandBinding);
而且該事件處理程式設定CanExecute狀態:
private void SupressCommand(object sender,CanExecuteRoutedEventArgs e) { e.CanExecute=false; e.Handled=false; }
注意,上面的代碼設定了Handled標志以阻止文本框自我執行計算,而文本框可能將CanExecute屬性設定為true,
該方法并不完美,它可成功地為文本框禁用Cut快捷鍵(Ctrl+X)和背景關系選單中的Cut命令,然而,仍會在背景關系選單中顯示處理禁用狀態的該選項,
最后一種選擇是,使用InputBinding集合洗掉觸發命令的輸入,例如,可使用帶阿媽禁用觸發TextBox控制元件中的Copy命令的Ctrl+C組合鍵,如下所示:
KeyBinding keyBinding=new KeyBinding(ApplicationCommands.NotACommand,Key.C,ModifierKeys.Control); txt.InputBinding.Add(keyBinding);
技巧是使用特定的ApplicationCommands.NotACommand值,該命令什么都不做,它專門用于禁用輸入系結,
當使用這種方法時,仍啟用Copy命令,可通過自己創建的按鈕觸發該命令(或使用文本框的背景關系選單觸發命令,除非也通過將ContextMenu屬性設定為null洗掉了背景關系選單),
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/5936.html
標籤:WPF
