WPF除了創(chuàng )建了一個(gè)新的依賴(lài)屬性系統之外,還用更高級的路由事件功能替換了普通的.NET事件。
路由事件是具有更強傳播能力的事件——它可以在元素樹(shù)上向上冒泡和向下隧道傳播,并且沿著(zhù)傳播路徑被事件處理程序處理。與依賴(lài)屬性一樣,可以使用傳統的事件方式使用路由事件。盡管路由事件的使用方式與傳統的事件一樣,但是理解其工作原理還是相當重要的。
對于.NET中的事件,大家應該在熟悉不過(guò)了。事件指的在某個(gè)事情發(fā)生時(shí),由對象發(fā)送用于通知代碼的消息。WPF中的路由事件允許事件可以被傳遞。例如,路由事件允許一個(gè)來(lái)自工具欄按鈕的單擊事件,在被處理之前可以傳遞到工具欄,然后再傳遞到包含工具欄的窗口。那么現在問(wèn)題來(lái)了,我怎樣在WPF中去定義一個(gè)路由事件呢?
既然有了問(wèn)題,自然就要去解決了。在自己定義一個(gè)依賴(lài)屬性之前,首先,我們得學(xué)習下WPF框架中是怎么去定義的,然后按照WPF框架中定義的方式去試著(zhù)自己定義一個(gè)依賴(lài)屬性。下面通過(guò)Reflector工具來(lái)查看下WPF中 Button 按鈕的Click事件的定義方式。
由于Button按鈕的Click事件是繼承于 ButtonBase 基類(lèi)的,所以我們直接來(lái)查看ButtonBase中Click事件的定義。具體的定義代碼如下所示:
[Localizability(LocalizationCategory.Button), DefaultEvent("Click")]public abstract class ButtonBase : ContentControl, ICommandSource{ // 事件定義 public static readonly RoutedEvent ClickEvent; // 事件注冊 static ButtonBase() { ClickEvent = EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ButtonBase)); CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(ButtonBase), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(ButtonBase.OnCommandChanged))); ....... } // 傳統事件包裝 public event RoutedEventHandler Click { add { base.AddHandler(ClickEvent, value); } remove { base.RemoveHandler(ClickEvent, value); } } .......}
從上面代碼可知,路由事件的定義與依賴(lài)屬性的定義類(lèi)似,路由事件由只讀的靜態(tài)字段表示,在一個(gè)靜態(tài)構造函數通過(guò) EventManager.RegisterRoutedEvent 函數注冊,并且通過(guò)一個(gè).NET事件定義進(jìn)行包裝。
現在已經(jīng)知道了路由事件是如何在WPF框架中定義和實(shí)現的了,那要想自己定義一個(gè)路由事件也自然不在話(huà)下了。
與依賴(lài)屬性一樣,可以在類(lèi)之間共享路由事件的定義。即實(shí)現路由事件的繼承。例如 UIElement 類(lèi)和ContentElement類(lèi)都使用了MouseUp事件,但MouseUp事件是由 System.Windows.Input.Mouse 類(lèi)定義的。UIElement類(lèi)和ContentElement類(lèi)只是通過(guò) RouteEvent.AddOwner 方法重用了MouseUp事件。你可以在UIElement類(lèi)的靜態(tài)構造函數找到下面的代碼:
static UIElement(){ _typeofThis = typeof(UIElement); PreviewMouseUpEvent = Mouse.PreviewMouseUpEvent.AddOwner(_typeofThis); MouseUpEvent = Mouse.MouseUpEvent.AddOwner(_typeofThis);}
盡管路由事件通過(guò)傳統的.NET事件進(jìn)行包裝,但路由事件并不是通過(guò).NET事件觸發(fā)的,而是使用RaiseEvent方法觸發(fā)事件,所有元素都從UIElement類(lèi)繼承了該方法。下面代碼是具體ButtonBase類(lèi)中觸發(fā)路由事件的代碼:
1 protected virtual void OnClick()2 {3 RoutedEventArgs e = new RoutedEventArgs(ClickEvent, this);4 base.RaiseEvent(e);// 通過(guò)RaiseEvent方法觸發(fā)路由事件5 CommandHelpers.ExecuteCommandSource(this);6 }
而在WinForm中, Button 的Click事件是通過(guò)調用委托進(jìn)行觸發(fā)的,具體的實(shí)現代碼如下所示:
1 protected virtual void OnClick(EventArgs e)2 {3 EventHandler handler = (EventHandler)base.Events[EventClick];4 if (handler != null)5 {6 handler(this, e); // 直接調用委托進(jìn)行觸發(fā)事件7 }8 }
對于路由事件的處理,與原來(lái)WinForm方式一樣,你可以在XAML中直接連接一個(gè)事件處理程序,具體實(shí)現代碼如下所示:
<TextBlock Margin="3" MouseUp="SomethingClick" Name="tbxTest"> text label</TextBlock>// 后臺cs代碼private void SomethingClick(object sender, MouseButtonEventArgs e){}
同時(shí)還可以通過(guò)后臺代碼的方式連接事件處理程序,具體的實(shí)現代碼如下所示:
tbxTest.MouseUp += new MouseButtonEventHandler(SomethingClick); // 或者省略委托類(lèi)型 tbxTest.MouseUp += SomethingClick;
路由事件的特殊性在于其傳遞性,WPF中的路由事件分為三種。
既然,路由事件有三種表現形式,那我們怎么去區別具體的路由事件是屬于哪種呢?辨別的方法在于路由事件的注冊方法上,當使用 EventManager.RegisterEvent 方法注冊一個(gè)路由事件時(shí),需要傳遞一個(gè) RoutingStrategy 枚舉值來(lái)標識希望應用于事件的事件行為。
下面代碼演示了事件冒泡過(guò)程:
<Window x:Class="BubbleLabelClick.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525" MouseUp="SomethingClick"> <Grid Margin="3" MouseUp="SomethingClick"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Label Margin="5" Grid.Row="0" HorizontalAlignment="Left" Background="AliceBlue" BorderBrush="Black" BorderThickness="2" MouseUp="SomethingClick"> <StackPanel MouseUp="SomethingClick"> <TextBlock Margin="3" MouseUp="SomethingClick" Name="tbxTest"> Image and text label </TextBlock> <Image Source="pack://application:,,,/BubbleLabelClick;component/face.png" Stretch="None" MouseUp="SomethingClick"/> <TextBlock Margin="3" MouseUp="SomethingClick"> Courtest for the StackPanel </TextBlock> </StackPanel> </Label> <ListBox Grid.Row="1" Margin="3" Name="lstMessage"> </ListBox> <CheckBox Margin="5" Grid.Row="2" Name="chkHandle">Handle first event</CheckBox> <Button Click="cmdClear_Click" Grid.Row="3" HorizontalAlignment="Right" Margin="5" Padding="3">Clear List</Button> </Grid></Window>
其后臺代碼為:
1 public partial class MainWindow : Window 2 { 3 public MainWindow() 4 { 5 InitializeComponent(); 6 7 } 8 9 private int eventCounter = 0;10 11 private void SomethingClick(object sender, RoutedEventArgs e)12 {13 eventCounter++;14 string message = "#" + eventCounter.ToString() + ":\r\n" + "Sender: " + sender.ToString() + "\r\n" +15 "Source: " + e.Source + "\r\n" +16 "Original Source: " + e.OriginalSource;17 lstMessage.Items.Add(message);18 e.Handled = (bool)chkHandle.IsChecked;19 }20 21 private void cmdClear_Click(object sender, RoutedEventArgs e)22 {23 eventCounter = 0;24 lstMessage.Items.Clear();25 }26 }
運行之后的效果圖如下所示:

單擊窗口中的笑臉圖像之后,程序的運行結果如下圖所示。

從上圖結果可以發(fā)現,MouseUp事件由下向上傳遞了5級,直到窗口級別結束。另外,如果選擇了Handle first event復選框的話(huà),SomethingClicked方法會(huì )將RoutedEventArgs.Handled屬性設置為true,表示事件已被處理,且該事件將終止向上冒泡。因此,此時(shí)列表中只能看到Image的事件,具體運行結果如下圖所示:

并且在列表框或窗口空白處進(jìn)行單擊,此時(shí)也一樣只會(huì )出現一次MouseUp事件。但單擊一個(gè)地方例外。當單擊Clear List按鈕,此時(shí)不會(huì )引發(fā)MouseUp事件。這是因為按鈕包含一些特殊的處理代碼,這些代碼會(huì )掛起MouseUp事件(即不會(huì )觸發(fā)MouseUp事件,則相應的事件處理程序也不會(huì )被調用),并引發(fā)一個(gè)更高級的Click事件,同時(shí),Handled標記被設置為true(這里指的在觸發(fā)Click事件時(shí)會(huì )把Handled設置為true),從而阻止MouseUp事件繼續向上傳遞。
隧道路由事件與冒泡路由事件的工作方式一樣,只是方向相反。 即如果上面的例子中,觸發(fā)的是一個(gè)隧道路由事件的話(huà),如果在圖像上單擊,則首先窗口觸發(fā)該隧道路由事件,然后才是Grid控件,接下來(lái)是StackPanel面板,以此類(lèi)推,直到到達實(shí)際源頭,即標簽中的圖像為止。
看了上面的介紹。隧道路由事件想必是相當好理解吧。它與冒泡路由事件的傳遞方式相反。但是我們怎樣去區別隧道路由事件呢?隧道路由事件的識別相當容易,因為 隧道路由事件都是以單詞Preview開(kāi)頭。 并且,WPF一般都成對地定義冒泡路由事件和隧道路由事件。這意味著(zhù)如果發(fā)現一個(gè)冒泡的MouseUp事件,則對應的PreviewMouseUp就是一個(gè)隧道路由事件。另外, 隧道路由事件總是在冒泡路由事件之前被觸發(fā) 。
另外需要注意的一點(diǎn)是:如果將隧道路由事件標記為已處理的,那么冒泡路由事件就不會(huì )發(fā)生。這是因為這兩個(gè)事件共享同一個(gè)RoutedEventArgs類(lèi)的實(shí)例。隧道路由事件對于來(lái)執行一些預處理操作非常有用,例如,根據鍵盤(pán)上特定的鍵執行特定操作,或過(guò)濾掉特定的鼠標操作等這樣的場(chǎng)景都可以在隧道路由事件處理程序中進(jìn)行處理。下面的示例演示了PreviewKeyDown事件的隧道過(guò)程。XAML代碼如下所示。
<Window x:Class="TunneleEvent.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525" PreviewKeyDown="SomeKeyPressed"> <Grid Margin="3" PreviewKeyDown="SomeKeyPressed"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Label Margin="5" Grid.Row="0" HorizontalAlignment="Left" Background="AliceBlue" BorderBrush="Black" BorderThickness="2" PreviewKeyDown="SomeKeyPressed"> <StackPanel> <TextBlock Margin="3" PreviewKeyDown="SomeKeyPressed"> Image and text label </TextBlock> <Image Source="face.png" Stretch="None" PreviewMouseUp="SomeKeyPressed"/> <DockPanel Margin="0,5,0,0" PreviewKeyDown="SomeKeyPressed"> <TextBlock Margin="3" PreviewKeyDown="SomeKeyPressed"> Type here: </TextBlock> <TextBox PreviewKeyDown="SomeKeyPressed" KeyDown="SomeKeyPressed"></TextBox> </DockPanel> </StackPanel> </Label> <ListBox Grid.Row="1" Margin="3" Name="lstMessage"> </ListBox> <CheckBox Margin="5" Grid.Row="2" Name="chkHandle">Handle first event</CheckBox> <Button Click="cmdClear_Click" Grid.Row="3" HorizontalAlignment="Right" Margin="5" Padding="3">Clear List</Button> </Grid></Window>
其對應的后臺cs代碼實(shí)現如下所示:
1 public partial class MainWindow : Window 2 { 3 public MainWindow() 4 { 5 InitializeComponent(); 6 } 7 8 private int eventCounter = 0; 9 10 private void SomeKeyPressed(object sender, RoutedEventArgs e)11 {12 eventCounter++;13 string message = "#" + eventCounter.ToString() + ":\r\n" +14 " Sender: " + sender.ToString() + "\r\n" +15 " Source: " + e.Source + "\r\n" +16 " Original Source: " + e.OriginalSource + "\r\n" +17 " Event: " + e.RoutedEvent;18 lstMessage.Items.Add(message);19 e.Handled = (bool)chkHandle.IsChecked;20 }21 22 private void cmdClear_Click(object sender, RoutedEventArgs e)23 {24 eventCounter = 0;25 lstMessage.Items.Clear();26 }27 }
程序運行后的效果圖如下所示:

在文本框中按下一個(gè)鍵時(shí),事件首先在窗口觸發(fā),然后在整個(gè)層次結構中向下傳遞。具體的運行結果如下圖所示:

如果在任何位置將PreviewKeyDown事件標記為已處理,則冒泡的KeyDown事件也就不會(huì )觸發(fā)。 當勾選了Handle first event 復選框時(shí),當在輸入框中按下一個(gè)鍵時(shí),listbox中顯示的記錄只有1條記錄,因為窗口觸發(fā)的PrevieKeyDown事件處理已經(jīng)把隧道路由事件標識為已處理,所以PreviewKeyDown事件將不會(huì )向下傳遞,所以此時(shí)只會(huì )顯示一條MainWindow觸發(fā)的記錄。并且,此時(shí),你可以注意到, 我們按下的鍵上對應的字符并沒(méi)有在輸入框中顯示,因為此時(shí)并沒(méi)有觸發(fā)Textbox中的KeyDown事件,因為改變文本框內容的處理是在KeyDown事件中處理的。 具體的運行結果如下圖所示:

在上面例子中,因為所有元素都支持MouseUp和PreviewKeyDown事件。然而,許多控件都有它們自己特殊的事件。例如按鈕的的Click事件,其他任何類(lèi)都有定義該事件。假設有這樣一個(gè)場(chǎng)景,StackPanel面板中包含了一堆按鈕,并且希望在一個(gè)事件處理程序中處理所有這些按鈕的單擊事件。首先想到的辦法就是將每個(gè)按鈕的Click事件關(guān)聯(lián)到同一個(gè)事件處理程序。但是Click事件支持事件冒泡,從而有一種更好的解決辦法??梢栽诟邔哟卧貋?lái)關(guān)聯(lián)Click事件來(lái)處理所有按鈕的單擊事件,具體的XAML代碼實(shí)現如下所示:
<Window x:Class="AttachClickEvent.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <StackPanel Margin="3" Button.Click="DoSomething"> <Button Name="btn1">Button 1</Button> <Button Name="btn2">Button 2</Button> <Button Name="btn3">Button 3</Button> </StackPanel></Window>
也可以在代碼中關(guān)聯(lián)附加事件,但是需要使用UIElement.AddHandle方法,而不能使用+=運算符的方式。具體實(shí)現代碼如下所示:
// StackPanel面板命名為ButtonsPanel ButtonsPanel.AddHandler(Button.ClickEvent, new RoutedEventHandler(DoSomething));
WPF事件生命周期起始和WinForm中類(lèi)似。下面詳細解釋下WPF中事件的生命周期。
FrameworkElement類(lèi)實(shí)現了 ISupportInitialize 接口,該接口提供了兩個(gè)用于控制初始化過(guò)程的方法。第一個(gè)是 BeginInit 方法,在實(shí)例化元素后立即調用該方法。BeginInit方法被調用之后,XAML解析器設置所有元素的屬性并添加內容。第二個(gè)是EndInit方法,當初始化完成后,該方法被調用。此時(shí)引發(fā) Initialized 事件。更準確地說(shuō),XAML解析器負責調用BeginInit方法和EndInit方法。
當創(chuàng )建窗口時(shí),每個(gè)元素分支都以自下而上的方式被初始化。這意味著(zhù)位于深層的嵌套元素在它們容器之前先被初始化。當引發(fā)初始化事件時(shí),可以確保元素樹(shù)中當前元素以下的元素已經(jīng)全部完成了初始化。但是,包含當前元素的容器還沒(méi)有初始化,而且也不能假設窗口的其他部分也已經(jīng)完成初始化了。在每個(gè)元素都完成初始化之后,還需要在它們的容器中進(jìn)行布局、應用樣式,如果需要的話(huà)還會(huì )進(jìn)行數據綁定。
一旦初始化過(guò)程完成后,就會(huì )引發(fā)Loaded事件。Loaded事件和Initialized事件的發(fā)生過(guò)程相反。意思就是說(shuō),包含所有元素的窗口首先引發(fā)Loaded事件,然后才是更深層次的嵌套元素。當所有元素都引發(fā)了Loaded事件之后,窗口就變得可見(jiàn)了,并且元素都已被呈現。下圖列出了部分生命周期事件。

到這里,WPF路由事件的內容就介紹結束了,本文首先介紹了路由事件的定義,接著(zhù)介紹了三種路由事件,WPF包括直接路由事件、冒泡路由事件和隧道路由事件,最后介紹了WPF事件的生命周期。在后面一篇文章將介紹WPF中的元素綁定。
聯(lián)系客服