了解 WPF 中的路由事件和命令
目錄
路由事件概述
WPF 元素樹
事件路由
路由事件和組合
附加事件
路由命令概述
操作中的路由命令
命令路由
定義命令
命令插入
路由命令的局限
避免命令出錯
超越路由命令
路由處理程序示例
要想盡快熟悉 Windows? Presentation Foundation (WPF),必須要面對的一個難題是有許多需要掌握的新結構。甚至 Microsoft? .NET Framework 屬性和事件這類簡單的事物,在 WPF 中也有新的對應項,功能有所更新且更為復雜——尤其是依賴關系屬性和路由事件,這一特點更為顯著。還有就是那些全新的內容,如動畫、樣式設定、控制模板和路由命令等。要學習的東西太多了。
在 本文中,我將重點介紹兩個極為重要的 WPF 新元素項。這兩個元素項就是相互關聯的路由事件和路由命令。它們是用戶界面上不同部件進行通信的基礎——這些部件可以是一個大的 Window 類的單個控件,也可以是用戶界面上單獨分離部件的控件及其支持代碼。在本文中,我假定您已經對 WPF 有了一定的了解,比如說,知曉如何使用內置 WPF 控件并通過以 XAML 聲明 UI 布局來構建 UI。
路由事件概述
剛開始接觸 WPF 時,您可能會在自己并不知曉的情況下就用到了路由事件。例如,當您在 Visual Studio? 設計器中向窗口添加一個按鈕,并將其命名為 myButton,然后雙擊該按鈕時,Click 事件將掛接在您的 XAML 標記之內,它的事件處理程序會添加到 Window 類的代碼隱藏中。這種感覺與在 Windows 窗體和 ASP.NET 中掛接事件并無二致。實際上,它比較接近 ASP.NET 的代碼編寫模型,但更類似 Windows 窗體的運行時模型。具體來說,在按鈕的 XAML 標記中,代碼的結尾類似如下所示:
?復制代碼
<Button Name="myButton" Click="myButton_Click">Click Me</Button>
掛 接事件的 XAML 聲明就象 XAML 中的屬性分配,但結果是針對指定事件處理程序的對象產生一個正常的事件掛接。此掛接實際上出現在編譯時生成的窗口局部類中。要查看這一掛接,轉到類的構造 函數,右鍵單擊 InitializeComponent 方法調用,然后從上下文菜單中選擇“轉到定義”。編輯器將顯示生成的代碼文件(其命名約定為 .i.g.cs 或 .i.g.vb),其中包括在編譯時正常生成的代碼。在顯示的局部類中向下滾動到 Connect 方法,您會看到下面的內容:
?復制代碼
#line 6 "..\..\Window1.xaml"
this.myButton.Click +=
? new System.Windows.RoutedEventHandler(
? this.myButton_Click);
這一局部類是在編譯時從 XAML 中生成的,其中包含那些需要設計時編譯的 XAML 元素。大部分 XAML 最終都會成為編譯后程序集中嵌入了二進制的資源,在運行時會與二進制標記表示的已編譯代碼合并。
如果看一下窗口的代碼隱藏,您會發現 Click 處理程序如下所示:
?復制代碼
private void myButton_Click(
? object sender, RoutedEventArgs e) { }
到 目前為止,它看起來就象任何其他 .NET 事件掛接一樣——您有一個顯式聲明的委托,它掛接到一個對象事件且委托指向某個處理方法。使用路由事件的唯一標記是 Click 事件的事件參數類型,即 RoutedEventArgs。那么路由事件究竟有何獨特之處呢?要理解這一點,首先需要了解 WPF 元素化的組合模型。
WPF 元素樹
如果您在項目中開啟一個新窗口并在設計器中將按鈕拖入窗口內,您會得到 XAML 格式的元素樹,如下所示(為了清楚略去了屬性):
?復制代碼
<Window>
? <Grid>
??? <Button/>
? </Grid>
</Window>
其 中的每個元素都代表對應 .NET 類型的一個運行時實例,元素的聲明分層結構形成了所謂的邏輯樹。此外,WPF 中的許多控件不是 ContentControl 就是 ItemsControl,這代表他們可以有子元素。例如,Button 是一個 ContentControl,它可以將復雜的子元素做為其內容。您可以展開邏輯樹,如下所示:
?復制代碼
<Window>
? <Grid>
??? <Button>
????? <StackPanel>
??????? <Image/>
??????? <TextBlock/>
????? </StackPanel>
??? </Button>
? </Grid>
</Window>
生成的 UI 如圖 1 所示。
?
圖 1 包含按鈕內容的簡單窗口
如 您所想,樹可以有多個分支(Grid 中的另一 Button),因此邏輯樹會變得極為復雜。對于邏輯樹的 WPF 元素,您需要意識到您所見到的并不是您在運行時真正得到的內容。每個這樣的元素通常都會在運行時擴展為更為復雜的可視元素樹。在本例中,元素的邏輯樹擴展 為可視元素樹,如圖 2 所示。
?
圖 2簡單窗口可視樹
我使用名為 Snoop 的工具 (blois.us/Snoop) 查看圖 2 中所示可視樹的元素。您可以看到窗口 (EventsWindow) 實際是將其內容置入 Border 和 AdornerDecorator 之內,用 ContentPresenter 顯示其中的內容。按鈕也與此類似,將其內容置入 ButtonChrome 對象,然后用 ContentPresenter 顯示內容。
單 擊按鈕時,我可能實際根本沒有單擊 Button 元素,可能是單擊可視樹中的某一子元素,甚至是邏輯樹中未顯示的元素(如 ButtonChrome)。例如,假設我在按鈕內的圖像上方單擊鼠標。這一單擊操作在一開始實際是將其表達為 Image 元素中的 MouseLeftButtonDown 事件。但卻需要轉化為 Button 層級的 Click 事件。這就要引入路由事件中的路由。
事件路由
對邏輯樹和可視樹有所了解很有必要,因為路由事件主要是根據可視樹進行路由。路由事件支持三種路由策略:氣泡、隧道和直接。
氣 泡事件最為常見,它表示事件從源元素擴散(傳播)到可視樹,直到它被處理或到達根元素。這樣您就可以針對源元素的上方層級對象處理事件。例如,您可向嵌入 的 Grid 元素附加一個 Button.Click 處理程序,而不是直接將其附加到按鈕本身。氣泡事件有指示其操作的名稱(例如,MouseDown)。
隧道事件采用另一種方式,從根元素開始,向下遍歷元素樹,直到被處理或到達事件的源元素。這樣上游元素就可以在事件到達源元素之前先行截取并進行處理。根據命名慣例,隧道事件帶有前綴 Preview(例如 PreviewMouseDown)。
直接事件類似 .NET Framework 中的正常事件。該事件唯一可能的處理程序是與其掛接的委托。
通 常,如果為特殊事件定義了隧道事件,就會有相應的氣泡事件。在這種情況下,隧道事件先觸發,從根元素開始,下行至源元素,查找處理程序。一旦它被處理或到 達源元素,即會觸發氣泡事件,從源元素上行,查找處理程序。氣泡或隧道事件不會僅因調用事件處理程序而停止路由。如果您想中止隧道或氣泡進程,可使用您傳 遞的事件參數在事件處理程序中將事件標記為已處理。
?復制代碼
private void OnChildElementMouseDown(object sender,
? MouseButtonEventArgs e) {
? e.Handled = true;
}
一 旦您的處理程序將事件標記為已處理,該事件便不會傳給任何其他處理程序。這一論斷只是部分正確。實際上,事件路由仍在繼續起作用,您可利用 UIElement.AddHandler 的替換方法在代碼中顯式掛接事件處理程序,該方法有一個額外的標記,可以有效指出“即使事件被標記為已處理也可調用我”。您用類似如下所示的調用指定該標 記:
?復制代碼
m_SomeChildElement.AddHandler(UIElement.MouseDownEvent,
? (RoutedEventHandler)OnMouseDownCallMeAlways,true);
AddHandler 的第一個參數是您想要處理的 RoutedEvent。第二個參數是對事件處理方法(它需要有事件委托的正確簽名)的委托。第三個參數指明如果另一個處理程序已將事件標記為已處理,您 是否想得到通知。您調用 AddHandler 的元素就是在路由期間觀察事件流動的元素。
路由事件和組合
現在我們來看一看 Button.Click 事件的形成過程,以了解為什么它如此重要。如前所述,用戶將對 Button 可視樹中的某些子元素(例如上一示例中的 Image)使用 MouseLeftButtonDown 事件啟動 Click 事件。
在 Image 元素內發生 MouseLeftButtonDown 事件時,PreviewMouseLeftButtonDown 在根元素啟動,然后沿隧道下行至 Image。如果沒有處理程序為 Preview 事件將 Handled 標記設置為 True,MouseLeftButtonDown 即會從 Image 元素開始向上傳播,直至到達 Button。按鈕處理這一事件,將 Handled 標記設為 True,然后引發其自身的 Click 事件。本文中的示例代碼包括一個應用程序,它帶有整個路由鏈掛接的處理程序,可幫您查看這一進程。
其 蘊含的意義不可小視。例如,如果我選擇通過應用包含 Ellipse 元素的控件模板替換默認按鈕外觀,可以保證在 Ellipse 外部單擊即可觸發 Click 事件。靠近 Ellipse 的外緣單擊仍處于 my button 的矩形邊界內,但 Ellipse 有其自身的 MouseLeftButtonDown 擊中檢測,而 Ellipse 外部按鈕的空白區域則沒有。
因 此,只有在 Ellipse 內部的單擊才會引發 MouseLeftButtonDown 事件。它仍由附加此模板的 Button 類進行處理,所以,即便是自定義的按鈕,您也能得到預測的行為。在編寫自己自定義的復合控件時也需牢記這一非常重要的概念,因為您的操作很可能類似 Button 對控件內子元素的事件處理。
附加事件
為了讓元素能處理在不同元素中聲明的事件,WPF 支持附加事件。附加事件也是路由事件,它支持元素 XAML 形式的掛接,而非聲明事件所用的類型。例如,如果您想要 Grid 偵聽采用氣泡方式通過的 Button.Click 事件,僅需按如下所示進行掛接即可。
?復制代碼
<Grid Button.Click="myButton_Click">
? <Button Name="myButton" >Click Me</Button>
</Grid>
在編譯時生成的局部類中的最終代碼現在如下所示:
?復制代碼
#line 5 "..\..\Window1.xaml"
((System.Windows.Controls.Grid)(target)).AddHandler(
System.Windows.Controls.Primitives.ButtonBase.ClickEvent,
new System.Windows.RoutedEventHandler(this.myButton_Click));
附加事件可在掛接事件處理程序位置方面給予您更大的靈活性。但如果元素包含在同一類中(如本例所示),其差異并不會顯露出來,這是由于處理方法針對的仍是 Window 類。
它 在兩方面產生影響。第一,事件處理程序根據處理元素在氣泡或隧道元素鏈中的位置進行調用。第二,您可額外執行一些操作,如從所用控件內封裝的對象處理事 件。例如,您可以象處理 Grid 中所示的事件一樣處理 Button.Click 事件,但這些 Button.Click 事件可以從窗口中包含的用戶控件內部向外傳播。
?
提示:事件處理程序命名
如果您不想一味使用事件處理程序的默認命名約定(objectName_eventName),僅需輸入您需要的事件處理程序名稱,右鍵單擊,然后單擊上下文菜單中的“瀏覽到事件處理程序”即可。Visual Studio 隨即按指定的名稱生成事件處理程序。
在 Visual Studio 2008 SP1 中,“屬性”窗口會有一個事件視圖,它與 Windows 窗體中的視圖類似,因此如果您有 SP1,即可以在那里指定事件名稱。但如果您采用的是 XAML,這是生成顯式命名的處理程序的便捷方法。
?
生成事件處理程序(單擊圖像可查看大圖)
并非所有事件都聲明為附加事件。實際上,大部分事件都不是這樣。但當您需要在控件來源之外處理事件時,附加事件會提供相當大的幫助。
路由命令概述
您已看到了路由事件,接下來我來介紹路由命令。WPF 的路由命令為您提供了一種特定的機制,用于將工具欄按鈕和菜單項這類 UI 控件掛接到處理程序,并且無需在應用程序中加入許多關聯性很強的重復代碼。與正常事件處理相比,路由命令有三大優點:
路由命令源元素(調用程序)能夠與命令目標(處理程序)分離——它們不需要彼此引用,如果是通過事件處理程序鏈接,就需要相互引用。
處理程序指出命令被禁用時,路由命令將自動啟用或禁用所有相關的 UI 控件。
您可以使用路由命令將鍵盤快捷方式與其他形式的輸入手勢(例如,手寫)相關聯,作為調用命令的另一種方式。
此外,路由命令特有的 RoutedUICommand 類可以定義單一 Text 屬性,用做任何控件(命令調用程序)的命令提示。與訪問每個相關的調用程序控件相比,Text 屬性的本地化更為容易。
要在調用程序上聲明命令,僅需在觸發命令的控件上設置 Command 屬性即可。
?復制代碼
<Button Command="ApplicationCommands.Save">Save</Button>
MenuItem、Button、RadioButton、CheckBox、Hyperlink 和許多其他控件都支持 Command 屬性。
對于您想用做命令處理程序的元素,可設置 CommandBinding:
?復制代碼
<UserControl ...>
? <UserControl.CommandBindings>
??? <CommandBinding Command="ApplicationCommands.Save"???
????? CanExecute="OnCanExecute" Executed="OnExecute"/>
? </UserControl.CommandBindings>
? ...
</UserControl>
CommandBinding 的 CanExecute 和 Executed 屬性指向聲明類代碼隱藏中的方法,這些方法會在命令處理進程中被調用。此處的要點是命令調用程序既不需要了解,也不需要引用命令處理程序,處理程序不必知道是哪個元素將要調用命令。
調用 CanExecute 來確定是否應啟用命令。要啟用命令,應將事件參數的 CanExecute 屬性設置為 True,如下所示:
?復制代碼
private void OnCanExecute(object sender,
? CanExecuteRoutedEventArgs e) {
? e.CanExecute = true;
}
如 果命令處理程序帶有定義的 Executed 方法,但沒有 CanExecute 方法,命令也會被啟用(在這種情況下,CanExecute 隱式為 true)。通過 Executed 方法,根據調用的命令執行相應的操作。這類與命令相關的操作可以是保存文檔、提交訂單、發送電子郵件等。
操作中的路由命令
為了使這一概念更為具體并讓路由命令的益處立竿見影,我們來看一個簡單的示例。在圖 3 中,您可看到一個簡單的 UI,它有兩個輸入文本框,一個對文本框中的文本執行 Cut 操作的工具欄按鈕。
?
圖 3 包含 Cut 命令工具欄按鈕的簡單示例
要 使用事件完成掛接,需要為工具欄按鈕定義 Click 處理程序,且該代碼需要引用兩個文本框。您需要根據控件中的文本選擇確定哪個文本框是焦點項并調用相應的剪貼板操作。還要根據焦點項的位置和文本框中是否 有選項,在適當的時候啟用或禁用工具欄按鈕。代碼十分凌亂且復雜。
對于這一簡單示例,問題還不大,但如果這些文本框深入用戶控件或自定義控件的內部,且窗口代碼隱藏無法直接訪問它們,情況又會如何?您不得不在用戶控件的邊界顯示 API 以便能從容器實現掛接,或公開顯露文本框,兩者皆不是理想的方法。
如使用命令,只需將工具欄按鈕的 Command 屬性設為在 WPF 中定義的 Cut 命令即可。
?復制代碼
<ToolBar DockPanel.Dock="Top" Height="25">
? <Button Command="ApplicationCommands.Cut">
??? <Image Source="cut.png"/>
? </Button>
</ToolBar>
現在您運行應用程序,會看到工具欄按鈕一開始是被禁用的。在其中一個文本框中選擇了文本后,工具欄按鈕會啟用,如果單擊該按鈕,文本會被剪切到剪貼板。這一操作適用于 UI 中任何位置的任何文本框。喔,很不錯吧?
實際上,TextBox 類實現有一個針對 Cut 命令的內置命令綁定,并為您封裝了該命令(Copy 和 Paste)的剪貼板處理。那么,命令如何只調用所關注的文本框,消息如何到達文本框并告訴它處理命令?這便是路由命令中路由部件發揮作用的地方。
命令路由
路由命令與路由事件的區別在于命令從其調用程序路由至處理程序的方法。具體來說,路由事件是從幕后在命令調用程序和處理程序之間路由消息(通過將其掛接至可視樹中的命令綁定)。
這 樣,眾多元素間都存在關聯,但在任何時刻都實際只有一個命令處理程序處于活動狀態。活動命令處理程序由可視樹中命令調用程序和命令處理程序的位置、以及 UI 中焦點項的位置共同決定。路由事件用于調用活動命令處理程序以詢問是否應啟用命令,并調用命令處理程序的 Executed 方法處理程序。
通 常,命令調用程序會在自己在可視樹中的位置與可視樹根項之間查找命令綁定。如找到,綁定的命令處理程序會確定是否啟用命令并在調用命令時一并調用其處理程 序。如果命令掛接到工具欄或菜單中的一個控件(或將設置 FocusManager.IsFocusScope = true 的容器),則會運行一些其他的邏輯,沿可視樹路徑從根項到命令綁定的焦點元素進行查看。
在圖 3 的簡單應用程序中,實際發生的情況是:由于 Cut 命令按鈕位于工具欄內,所以由具備焦點項的 TextBox 實例處理 CanExecute 和 Execute。如圖 3 中的文本框包含在用戶控件之內,您就有機會對窗口、包含 Grid 的用戶控件、包含文本框的用戶控件或單個文本框設置命令綁定。有焦點項的文本框將確定其路徑的終點(它的起點是根項)。
要理解 WPF 路由命令的路由,需要認識到一旦調用一個命令處理程序,就不能再調用其他處理程序。因此,如果用戶控件處理 CanExecute 方法,就不會再調用 TextBox CanExecute 實現。
定義命令
ApplicationCommands.Save 和 ApplicationCommands.Cut 是 WPF 提供的諸多命令中的兩個命令。圖 4 中顯示了 WPF 中五個內置命令類及其所包含的一些命令示例。
?圖 4 WPF 命令類
命令類 示例命令
ApplicationCommands Close、Cut、Copy、Paste、Save、Print
NavigationCommands BrowseForward、BrowseBack、Zoom、Search
EditingCommands AlignXXX、MoveXXX、SelectXXX
MediaCommands Play、Pause、NextTrack、IncreaseVolume、Record、Stop
ComponentCommands MoveXXX、SelectXXX、ScrollXXX、ExtendSelectionXXX
XXX 代表操作的集合,例如 MoveNext 和 MovePrevious。每一類中的命令均定義為公用靜態(在 Visual Basic? 中共享)屬性,以便您可輕松掛接。通過使用以下方式,您可以輕松定義自己的自定義命令。稍后我會提供相應的示例。
您也可搭配使用一個簡短的注釋,如下所示:
?復制代碼
? <Button Command="Save">Save</Button>
如 您使用此縮寫版本,WPF 中的類型轉換器將嘗試從內置命令集合找到命名的命令。在此例中結果完全相同。我傾向于使用長名版本,這樣代碼更為明確、更易維護。不會對命令的定義位置產 生歧義。即使是內置命令,在 EditingCommands 類和 ComponentCommands 類之間也會有一些重復。
命令插入
路由命令是 WPF 所定義的 ICommand 界面的一種特殊實現。ICommand 的定義如下:
?復制代碼
? public interface ICommand {
??? event EventHandler CanExecuteChanged;
??? bool CanExecute(object parameter);
??? void Execute(object parameter);
? }
內置的 WPF 命令類型為 RoutedCommand 和 RoutedUICommand。這兩種類均實現 ICommand 界面并使用我先前所介紹的路由事件執行路由。
我 們期望命令調用程序調用 CanExecute 來確定是否啟用任何相關的命令調用代碼。命令調用程序可通過訂閱 CanExecuteChanged 事件來確定何時調用該方法。在 RoutedCommand 類中,根據狀態或 UI 中焦點項的變化觸發 CanExecuteChanged。調用命令時,會調用 Executed 方法并通過路由事件沿可視樹分派至處理程序。
支持 Command 屬性的類(如 ButtonBase)實現 ICommandSource 界面:
?復制代碼
public interface ICommandSource {
? ICommand Command { get; }
? object CommandParameter { get; }
? IInputElement CommandTarget { get; }
}
Command 屬性在調用程序和它將調用的命令之間建立關聯。CommandParameter 允許調用程序在調用命令的同時傳遞某些數據。您可使用 CommandTarget 屬性根據焦點項的路徑替換默認路由,并通知命令系統使用指定的元素做為命令處理程序,而不是依賴路由事件和命令處理程序基于焦點項所做的決定。
路由命令的局限
路 由命令非常適合單用戶界面,掛接工具欄和菜單項以及處理與鍵盤焦點項目(如剪貼板操作)相關的條目。但是,如果您要構建復雜的用戶界面,即命令處理邏輯位 于視圖定義的支持代碼之內,且命令調用程序不總是在工具欄或菜單之內,在這種情況下,路由命令就顯得力不從心了。使用 UI 復合模式時,如 Model View Controller 或 MVC (msdn.microsoft.com/magazine/cc337884)、Model View Presenter 或 MVP (msdn.microsoft.com/magazine/cc188690)、Presentation Model,在 WPF 循環中亦稱做 Model View ViewModel (msdn.microsoft.com/library/cc707885),通常會出現這種情況。
此時的問題是啟用并處理命令邏輯可能不是直接歸屬于可視樹,而是位于表示器或表示模型。此外,確定是否啟用命令的狀態與命令調用程序和視圖在可視樹中的位置無關。有時,您會遇到一個特殊命令在給定時間有多個處理程序的情形。
要了解在哪些情況下路由命令會出現問題,請查看圖 5。它是一個簡單的窗口,包含一對用戶控件,這兩個控件以 MVP 或 MVC 模式表示視圖。主窗口包含一個 File 菜單和工具欄,其中有 Save 命令按鈕。在主窗口上方還有一個輸入文本框,以及一個將 Command 設為 Save 的 Button。
?
圖 5 復合用戶界面(單擊圖像可查看大圖)
提示:掛接匿名方法
在圖 6 所示的代碼中,我使用了我同事 Juval Lowy 傳授給我的技巧,向聲明中的委托掛接一個空的匿名方法。
?復制代碼
Action<string> m_ExecuteTargets = delegate { };這樣,在調用委托前,您就不必再檢查是否有空值,因為在調用列表中始終都有一個 no-op 訂戶。您還可能通過在多線程環境中取消訂閱避免可能的爭用,如果您檢查空值,經常會出現爭用。
有關此技巧的詳細信息,請參閱 Juval Lowy 撰寫的《Programming .NET Components, Second Edition》。
?
UI 的其余部分由兩個視圖提供,每個都是簡單用戶控件的實例。每個用戶控件實例的邊界顏色各不相同,這是為更清楚地顯示它們所提供的 UI 內容。每個用戶控件實例都有一個 Save 按鈕,它將 Command 屬性設為 Save 命令。
路由命令(與可視樹中的位置密切相關)帶來的困難在這一簡單示例中一覽無余。在圖 5 中,窗口本身沒有針對 Save 命令的 CommandBinding。但它的確包含該命令的兩個調用程序(菜單和工具欄)。在此情形中,我不想讓頂層窗口在調用命令時必須了解采取何種操作。而 是希望由用戶控件表示的子視圖處理命令。此例中的用戶控件類有針對 Save 命令的 CommandBinding,它為 CanExecute 返回 true。
但在圖 5 中,您可以看到焦點項位于頂部文本框的窗口內,而此級別的命令調用程序卻被禁用。此外,盡管用戶控件中沒有焦點項,但用戶控件中的 Save 按鈕卻被啟用。
如果您將焦點項從一個文本框更改到一個用戶控件實例內,菜單和工具欄中的命令調用程序會變為啟用狀態。但窗口本身的 Save 按鈕不會變為啟用狀態。實際上,在這種情況下無法用正常路由啟用窗口上方文本框旁的 Save 按鈕。
原 因仍與單個控件的位置相關。由于在窗口級沒有命令處理程序,盡管焦點項位于用戶控件之外,但可視樹上方或焦點項路徑上仍沒有命令處理程序會啟用掛接為命令 調用程序的控件。因此一旦涉及這些控件,會默認禁用命令。但是,對于用戶控件內的命令調用程序,由于處理程序在可視樹的位置靠上,所以會啟用命令。
一旦您將焦點項轉到其中一個用戶控件內,位于窗口和焦點項路徑上文本框之間的用戶控件即會提供命令處理程序,用于為工具欄和菜單啟用命令,這是因為它們會檢查焦點項路徑以及其在可視樹中的位置與根項之間的路徑。由于窗口級按鈕和根項之間沒有處理程序,所以無法啟用該按鈕。
要是這個簡單的小示例中的可視樹和焦點項路徑的繁文縟節就讓您倍感頭疼,如果 UI 相當復雜,在可視樹中眾多不同位置有命令調用程序和處理程序,要想理順命令啟用和調用有多難就可想而知了。那會您聯想起電影《Scanners》中的怕人情節,讓人頭昏眼花。
?
避免命令出錯
要 防止路由命令出現與可視樹位置相關的問題,您需要保持簡潔。通常應確保命令處理程序位于相同的元素,或在可視樹中處于調用命令的元素上方。您可以從包含命 令處理程序的控件使用 CommandManager.RegisterClassCommandBinding 方法,在窗口級加入命令綁定,這樣就能實現上述目標。
如果您實現的是本身接受鍵盤焦點項(像文本框)的自定義控件,那么屬于例外情況。在這種情形下,如果您想在控件本身嵌入命令處理且該命令處理僅在焦點項處于您的控件上時產生關聯,您可實現這一目標,它的工作狀況類似先前所示的 Cut 命令示例。
您也可通過 CommandTarget 屬性明確指定命令處理程序來解決上述問題。例如,對于圖 5 中從未啟用過的窗口級 Save 按鈕,您可將其命令掛接更改為如下所示:
?復制代碼
<Button Command="Save"?? CommandTarget="{Binding ElementName=uc1}"? Width="75" Height="25">Save</Button>
在 此代碼中,Button 專門將其 CommandTarget 設為 UIElement 實例,該實例中包含一個命令處理程序。在本例中,它指定名為 uc1 的元素,該元素恰好為示例中兩個用戶控件實例之一。由于該元素有一個始終返回 CanExecute = true 的命令處理程序,窗口級的 Save 按鈕始終處于啟用狀態,并僅調用該控件的命令處理程序,無論調用程序相對于命令處理程序的位置如何都是如此。
?
超越路由命令
由于路由命令存在一定的限制,許多用 WPF 構建復雜 UI 的公司已轉為使用自定義 ICommand 實現,這些實現能為它們提供自己的路由機制,特別是與可視樹無關聯且支持多個命令處理程序的機制。
創建自定義命令實現并不困難。針對類實現 ICommand 界面后,會為掛接命令處理程序提供一種方式,然后可在調用命令時執行路由。您還必須確定使用何種標準確定引發 CanExecuteChanged 事件的時機。
創建自定義命令時最好先使用委托。委托已支持調用目標方法,并支持多個訂戶。
圖 6 顯示了名為 StringDelegateCommand 的命令類,它使用委托來允許掛接多個處理程序。它支持向處理程序傳遞字符串參數,并使用調用程序的 CommandParameter 確定向處理程序傳遞的消息。
?圖 6 自定義命令類
?復制代碼
public class StringDelegateCommand : ICommand {? Action<string> m_ExecuteTargets = delegate { };? Func<bool> m_CanExecuteTargets = delegate { return false; };? bool m_Enabled = false;? public bool CanExecute(object parameter) {??? Delegate[] targets = m_CanExecuteTargets.GetInvocationList();??? foreach (Func<bool> target in targets) {????? m_Enabled = false;????? bool localenable = target.Invoke();????? if (localenable) {??????? m_Enabled = true;??????? break;????? }??? }??? return m_Enabled;? }? public void Execute(object parameter) {??? if (m_Enabled)????? m_ExecuteTargets(parameter != null ? parameter.ToString() : null);? }? public event EventHandler CanExecuteChanged = delegate { };? ...}
如 您所見,我選擇使用 Func<bool> 委托掛接確定是否啟用命令的處理程序。在 CanExecute 實現中,類遍歷掛接到 m_CanExecuteTargets 委托的處理程序,查看是否有處理程序想執行的委托。如果有,它為要啟用的 StringDelegateCommand 返回 true。調用 Execute 方法時,它僅需檢查是否啟用了命令,如啟用,則調用所有掛接到 m_ExecuteTargets Action<string> 委托的處理程序。
要將處理程序掛接到 CanExecute 和 Execute 方法,StringDelegateCommand 類公開圖 7 中所示的事件訪問器,從而允許處理程序從基礎委托輕松訂閱或取消訂閱。注意,您還可以在處理程序訂閱或取消訂閱時使用事件訪問器觸發 CanExecuteChanged 事件。
?圖 7 命令事件訪問器
?復制代碼
public event Action<string> ExecuteTargets {? add {??? m_ExecuteTargets += value;? }? remove {??? m_ExecuteTargets -= value;? }}public event Func<bool> CanExecuteTargets {? add {??? m_CanExecuteTargets += value;??? CanExecuteChanged(this, EventArgs.Empty);? }? remove {??? m_CanExecuteTargets -= value;??? CanExecuteChanged(this, EventArgs.Empty);? }}
路由處理程序示例
在 代碼下載的示例應用程序中,我掛接了這個類。該示例有一個簡單視圖,隱含一個表示器(沿用 MVP,但沒有模型)。表示器向視圖公開一個表示模型以綁定數據(您可將表示模型想象成位于表示器和視圖之間,而 MVP 模型位于表示器之后)。表示模型通常公開視圖可以綁定數據的屬性。在本例中,它僅公開了一個命令屬性,以便可以通過數據綁定在視圖的 XAML 中輕松實現掛接。
?復制代碼
<Window x:Class="CustomCommandsDemo.SimpleView" ...>? <Grid>??? <Button Command="{Binding CookDinnerCommand}"?????? CommandParameter="Dinner is served!" ...>Cook Dinner</Button>??? <Button Click="OnAddHandler" ...>Add Cook Dinner Handler</Button>? </Grid></Window>
Binding 聲明只查找當前 DataContext 的屬性(名為 CookDinnerCommand),如找到,則將它傳給 Icommand。我們在前面提到過 CommandParameter,調用程序可以用它隨同命令傳遞某些數據。在本例中,請注意我只傳遞了將通過 StringDelegateCommand 傳遞給處理程序的字符串。
此處所示為視圖的代碼隱藏(Window 類):
?復制代碼
public partial class SimpleView : Window {? SimpleViewPresenter m_Presenter = new SimpleViewPresenter();? public SimpleView() {??? InitializeComponent();??? DataContext = m_Presenter.Model;? }? private void OnAddHandler(object sender, RoutedEventArgs e) {??? m_Presenter.AddCommandHandler();? }}
視圖構建其表示器,從表示器取得表示模型,然后將其設置為 DataContext。它還有按鈕 Click 的處理程序,該處理程序調入表示器,讓它為命令添加處理程序。
?
圖 8 顯示了運行中的這一應用程序。第一個窗口處于初始狀態,未掛接命令處理程序。由于沒有命令處理程序,所以您會看到第一個按鈕(調用程序)被禁用。按第二個 按鈕時,它會調入表示器并掛接新的命令處理程序。此時會啟用第一個按鈕,您再單擊它時,它會調用其通過數據綁定松散聯接的命令處理程序和基礎命令的訂戶列 表。
?
圖 8 運行中的自定義命令示例(單擊圖像可查看大圖)
表示器代碼如圖 9 中所示。您可以看到表示器構建了表示模型,并通過 Model 屬性將其公開給視圖。從視圖調用 AddCommandHandler 時(響應第二個按鈕 Click 事件),它會向模型的 CanExecuteTargets 和 ExecuteTargets 添加一個訂戶。這些訂閱方法是表示器中的簡單方法,它們分別返回 true 并顯示 MessageBox。
?圖 9 表示器類
?復制代碼
public class SimpleViewPresenter {? public SimpleViewPresenter() {??? Model = new SimpleViewPresentationModel();? }? public SimpleViewPresentationModel Model { get; set; }? public void AddCommandHandler() {??? Model.CookDinnerCommand.CanExecuteTargets += CanExecuteHandler;??? Model.CookDinnerCommand.ExecuteTargets += ExecuteHandler;? }? bool CanExecuteHandler() {??? return true;? }? void ExecuteHandler(string msg) {??? MessageBox.Show(msg);? }}
本 例顯示數據綁定、UI 模式和自定義命令的組合將為您帶來清晰獨立的命令途徑,可以擺脫路由命令的限制。由于命令是通過綁定以 XAML 形式掛接的,您甚至可以通過此方式完全用 XAML 定義視圖(沒有代碼隱藏)、從 XAML 使用綁定命令觸發表示模型中的操作、啟動您原本需要表示模型代碼隱藏執行的操作。
您 需要控制器來構建視圖并為其提供表示模型,但您不用代碼隱藏就能編寫交互視圖。如果無需代碼隱藏,在代碼隱藏文件中添加相互糾結、不可測試的復雜代碼的機 率會大大降低,在 UI 應用程序中,這種情況經常出現。此方式剛在 WPF 中試用。但它的確值得考慮,您應該了解更多的示例。
代碼下載(175 KB)
本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/wmjcom/archive/2009/05/22/4208406.aspx
轉載于:https://www.cnblogs.com/angells/archive/2009/10/20/1586273.html
總結
以上是生活随笔為你收集整理的了解 WPF 中的路由事件和命令的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 文字图片垂直居中对齐
- 下一篇: 中文字符串提交乱码的解决方法