就像屬性系統在WPF中得到升級、進化為依賴屬性一樣,事件系統在WPF中也被升級,從而進化成為——路由事件(Routed Event),並在其基礎上衍生出命令傳遞機制。就讓我們一起來領略這些新消息機制的風采吧!
1、近觀WPF的樹形結構。
路由(Route)一詞的大意為:起點和終點之間有若干個中轉站,從起點出發后經過每個中轉站時都要進行選擇,最終以正確(比如最短或者最快)的路徑到達終點。我們知道,WPF的UI是由布局組件和控件構成的屬樹形結構。因此,當這棵樹上的某個節點激發出某個事件的時候,程序員可以選擇以傳統的直接事件模型讓響應者響應之,也可以讓這個事件在UI組件樹沿着一定的方向傳遞且路過多個中轉點,並在這個路由中被恰當的處理。因為WPF事件的路由環境是UI組件樹,因此我們有必要先研究一下這棵樹。4
在WPF中有兩種樹,一個為邏輯樹(Logical Tree),另一個叫可視化元素樹(Visual Tree)。邏輯樹最大的特點就是它完全由布局組件和控件構成(包括列表控件中的條目元素),簡單點說,就是它的每個節點不是布局組件就是控件。相對的可視化元素樹其實是對Logical Tree的一種細分。在Logical Tree中,節點是一般都是控件,Visual Tree就是有組成控件的更加細小的組件(他們不是控件,而是一些可視化組件,派生自Visual類)來構成的。
2、事件的來龍去脈
事件的前身是消息(Message)Windows就是消息驅動的操作系統。消息驅動對於一個剛剛入門的Windows開發人員來說門檻太高,隨着微軟面向對象平台的成熟,微軟把之前的消息機制封裝成更容易讓人們理解的事件模型。整個消息機制在事件模型中被簡化成三個特點:
- 事件的擁有者:即消息的發送者。事件的宿主可以在某些條件下激發它所擁有的事件,即事件被觸發
- 事件的響應者:即消息的接收者、處理者。事件接收者使用其事件的處理器(Event Handel)對事件作出響應
- 事件的訂閱關系:事件的擁有者可以隨時的觸發事件,但是事件發生之后會不會得到響應要看有沒有事件的響應者,或者說要看這件事件有沒有被關注
直接事件模型是傳統.Net開發中對象之間相互協調、溝通信息的主要手段,它在很大程度上簡化了程序的開發。然后直接事件模型並不完美,它的不完美之處就在於事件的響應者與事件的擁有者之間必須建立事件訂閱這樣一個關系。
直接事件模型的弱點在以下兩種情況下會暴露出來:
(1)程序運行期在容器中動態生成一組相同的控件,每個控件的同一個事件都使用同一個事件處理器來響應。面對這種情況,我們在動態生成代碼的同時就需要顯示書寫事件訂閱代碼。
(2)用戶控件內部事件不能被外接所訂閱,必須對用戶控件定義新的事件向外界暴露內部事件。當模塊划分很細的時候,UI組件的層級會很多,如果想讓最外層的容器訂閱深層控件的某個事件就需要為每一層組件定義用於暴露內部事件的事件,形成事件鏈。
路由事件的出現,很好的解決了上面提到的兩個問題。
3、路由事件
為了降低由事件訂閱帶來的耦合度和代碼量,WPF退出了路由事件機制。路由事件與直接事件的區別在於:直接事件被觸發時,發送者直接將消息通過事件訂閱交給事件響應者,事件響應者在作出相應的事件;路由事件則沒有顯示的訂閱關系,事件的發送者只負責觸發事件,至於是誰響應事件它並不關心,但是事件的響應者提前安裝事件的監聽器,針對某類事件進行監聽,當有此類事件傳過來的時候,事件響應者怎會進行響應事件並決定事件是否再被傳遞。
3.1 使用WPF內置的路由事件
此處我們以Button的Click事件來說明路由事件的使用。
首先進行如下布局:(XAML代碼如下)
<Window x:Class="_01_使用WPF內置路由事件.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:_01_使用WPF內置路由事件" mc:Ignorable="d" Title="Routed" Height="200" Width="200"> <Grid> <Grid Name="gridRoot" Background="Lime"> <Grid Name="gridA" Margin="10" Background="Blue"> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> <Canvas Name="canvasLeft" Grid.Column="0" Background="Red" Margin="10"> <Button Name="buttonLeft" Content="Left" Width="40" Height="100" Margin="10"></Button> </Canvas> <Canvas Name="canvasRight" Grid.Column="1" Background="Yellow" Margin="10"> <Button Name="buttonRight" Content="Right" Width="40" Height="100" Margin="10"></Button> </Canvas> </Grid> </Grid> </Grid> </Window>
其運行效果及Logical Tree結構如下:

當單擊buttonLeft時,Button.Click事件就會沿着buttonLeft---CanvasLeft-----gridA-------gridRoot-----Window線路傳遞。因為目前還沒有哪個節點偵聽Click事件,所以單擊按鈕之后盡管事件向上傳遞卻並沒有接到響應。下面,我們讓gridRoot安裝針對Button.Click的事件偵聽器。
方法很簡單,就是在窗體的構造器中調用gridRoot的AddHandler方法把想偵聽的事件和事件處理器關聯起來:
public MainWindow() { InitializeComponent(); //為gridRoot安裝針對Button.Click事件的監聽器 this.gridRoot.AddHandler(Button.ClickEvent, new RoutedEventHandler(this.ButtonClicked)); }
上面的代碼讓最外層的Grid(gridRoot)能夠捕捉到從“內部”飄出來的按鈕單擊事件,捕捉到會用this.ButtonClicked方法來進行響應處理。ButtonClicked代碼如下:
private void ButtonClicked(object sender,RoutedEventArgs e) { MessageBox.Show((e.OriginalSource as FrameworkElement).Name); }
這里有一點非常重要:因為路由事件(的消息)是從內部一層層傳遞出來最后到達最外層的gridRoot,並且由gridRoot元素把消息事件交給Button_Click方法來處理,所以傳入Button_Click方法的參數obj實際上是gridRoot而不是被單擊的Button,這與直接的傳統事件有些不一樣。如果想查看事件的源頭(最初發起者)怎么辦呢?答案是使用e.OriginalSource,使用它的時候需要是用as/is操作符或着強制類型把它識別/轉換為正確的類型。
運行程序單擊右邊的按鈕,效果如下:

上述為元素添加路由時間在XAML代碼里面也可以完成,只需要把XAML代碼改成這樣即可:
<Grid x:Name="gridRoot" Background="Lime" Button.Click="Button_Click">
To Be Continue
