如果對事件一點都不了解或者是模棱兩可的話,建議先去看張子陽的委托與事件的文章(比較長,或許看完了,也忘記看這一篇了,沒事,我會原諒你的)http://www.cnblogs.com/JimmyZhang/archive/2007/09/23/903360.html,廢話不多說,開始進入正題。本記錄不是記錄傳統的事件(CLR事件),而是記錄WPF中常用的事件——路由事件,由於路由事件“傳播”的時間是沿着可視樹傳播的,所以在記錄開始之前還是了解一下邏輯樹和可視樹。
一、邏輯樹和可視樹
在WPF中有兩種樹:邏輯樹(Logical Tree)和可視樹(Visual Tree),XAML是表達WPF的一棵樹。邏輯樹完全是由布局組件和控件構成。如果我們把邏輯樹延伸至Template組件級別,我們就得到了可視樹,所以可視樹把樹分的更細致。由於本記錄重在記錄事件,所以不做過多表述邏輯樹和可視樹的內容。關於邏輯樹和可視樹的區別可以參考http://www.cnblogs.com/Clingingboy/archive/2010/08/06/1793923.html。
二、路由事件
2.1、小記事件
如果看完了委托與事件的文章,相信會對事件有更進一步的認識了,但還是要把一些基礎的地方再記錄一下。一個事件要有下面幾個要素,才會變的有意義:
-
- 事件的擁有者(sender)——即消息的發送者。
- 事件發送的消息(EventAgs)
- 事件的響應者——消息的接收者(對象)。
- 響應者的處理器——消息的接受者要對消息作出處理的方法(方法名)。
- 響應者對發送者事件的訂閱
2.2 初試路由事件
我們建立一個winform項目,然后在窗體上添加一個按鈕,雙擊添加一個處理器,會發現private void btn_Click(object sender, EventArgs e)處理器要處理消息是EventArgs類型的,這里對應的是傳統的事件(我們叫它CLR事件)。同樣我們建立一個WPF項目,然后添加一個按鈕,雙擊添加一個處理器,會發現private void Button_Click(object sender, RoutedEventArgs e)處理器要處理的消息是RoutedEventArgs類型的,這里對應的是路由事件。兩者有什么區別呢。讓我們先看看CLR事件的弊端,就如(this.btn.Click += new System.EventHandler(this.btn_Click);)每一個消息都是從發送到響應的一個過程,當一個處理器要用多次,必須建立顯式的點對點訂閱關系(窗體對按鈕事件的訂閱,如果是再有一個按鈕的話,就要再來一次訂閱);還有一個弊端是:事件的宿主必須能夠直接訪問事件的響應者,不然無法建立訂閱關系(如有兩個組件,點擊組件一的按鈕,想讓組件二響應事件,那么就讓組件二向組件一的按鈕暴露一個可以訪問的事件,這樣如果再多幾個嵌套,會出現事件鏈,有暴露如果暴露不當就存在着威脅)。路由事件除了能很好的解決上面的問題,還有一個是路由事件在有路的情況下,能很好的按照規定的方式傳播事件,因為XAML的樹狀結構,構成了一條條的道路,所以在WPF中,引入了路由事件。舉個例子:如果窗體要以相同的方式處理兩個按鈕的事件,我們就可以用一句代碼就搞定了,this.AddHandler(Button.ClickEvent, new RoutedEventHandler(this.ButtonClicked));這樣就減少代碼量。下面通過一個例子初試一下路由事件。 給出XAML代碼:
<Window x:Class="Chapter_06.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"> <Grid x:Name="GridRoot" Background="Lime"> <Grid x:Name="gridA" Margin="10" Background="Blue"> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Canvas x:Name="canvasLeft" Grid.Column="0" Background="Red" Margin="10"> <Button x:Name="buttonLeft" Content="left" Width="40" Height="100" Margin="10"/> </Canvas> <Canvas x:Name="canvasRight" Grid.Column="1" Background="Yellow" Margin="10"> <Button x:Name="buttonRight" Content="right" Width="40" Height="100" Margin="10" /> </Canvas> </Grid> </Grid> </Window>
我們點擊按鈕時,無論是buttonLeft還是buttonRight單擊都能顯示按鈕的名稱。兩個按鈕到頂部的window有唯一條路,左邊的按鈕對應的路:buttonLeft->canvasLeft->gridA->GridRoot->Window,右邊按鈕對應的路:buttonRight->canvasRight->gridA->GridRoot->Window。如果GridRoot訂閱兩個處理器,那么處理器應該是相同的。后台代碼為:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace Chapter_06 { /// <summary> /// MainWindow.xaml 的交互邏輯 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this.GridRoot.AddHandler(Button.ClickEvent,new RoutedEventHandler(this.ButtonClicked)); } private void ButtonClicked(object sender, RoutedEventArgs e) { MessageBox.Show((e.OriginalSource as FrameworkElement).Name); } } }
下面先解釋一下路由事件是怎么沿着可視樹來傳播的,當Button被點擊,Button就開始發送消息了,可視樹上的元素如果訂閱了Button的點擊事件,那么才會根據消息來作出相應的反應,如果沒有訂閱的話,就無視它發出的消息,當然我們還可以控制它的消息的傳播方式,是從樹根到樹葉傳播,還是樹葉向樹根傳播以及是直接到達目的傳播,不僅如此,還能控制消息傳到某個元素時,停止傳播。具體的會在后面記錄到。其次是this.GridRoot.AddHandler(Button.ClickEvent,new RoutedEventHandler(this.ButtonClicked));訂閱事件時,第一個參數是路由事件類型,在這里用的是Button的ClickEvent,就像依賴屬性一樣,類名加上依賴屬性,這里是類名加上路由事件。另外一個是e.OriginalSource與e.Source的區別。由於消息每傳一站,都要把消息交個一個控件(此控件成為了消息的發送地點),e.Source為邏輯樹上的源頭,要想獲取原始發消息的控件(可視樹的源頭)要用e.OriginalSource。最后說明一下,怎么在XAML中添加訂閱事件。直接用<Grid x:Name="gridA" Margin="10" Background="Blue" Button.Click="ButtonClicked">在.net平台上不能智能提示Button,因為Click是繼承與ButtonBase的事件,XAML只認識含有Click事件的元素,但是要大膽的寫下去才能成功。運行上面代碼,點擊左邊按鈕,效果如圖1:
圖1
默認的路由消息里面屬性有四個,如圖2,可以自行轉到定義看一下其屬性代表的意義。
圖2
2.3自定義路由事件
通過上面的小試路由事件,應該對路由事件有些了解了,下面就記錄一下如何自定義一個路由事件。大致分為三個步驟:1.聲明並注冊路由事件,2.為路由事件添加CLR事件包裝,3.創建可以激發路由事件的方法。回憶一下依賴屬性,前兩個步驟應該和路由事件很相似吧。下面將三個步驟分開來說明:
第一步:聲明並注冊路由事件
//***Event為路由事件名,類型為路由事件類型 //注冊事件用的是EventManager.RegisterRoutedEvent //第一個參數為事件名(下面的***都為同一個單詞) //第二個參數為事件傳播的策略,有三種策略:Bubble(冒泡式),Tunnel(隧道式),Direct(直達式)分別對應上面的三種青色字體的三種方式 //第三個參數用於指定事件處理器的類型,該類型必須為委托類型,並且不能為 null。 //第四個參數為路由事件的宿主 public static readonly RoutedEvent ***Event = EventManager.RegisterRoutedEvent("***", RoutingStrategy.Bubble, typeof(***RouteEventHandler), typeof(ClassName));
第二步:為路由事件添加CLR事件包裝
/*包裝事件 *這里與傳統的數據差別是把+=和-=換成了AddHandler和RemovedHandler */ public event RoutedEventHandler *** { add { this.AddHandler(***Event, value); } remove { this.RemoveHandler(***Event, value); } }
第三步:創建可以激發路由事件的方法
/*對於控件的事件,一般是重寫宿主事件對應的方法(如Button的click事件和OnClick()方法相對應):新建消息,並把消息與路由事件相關聯, *通過調用元素的RaiseEvent方法把時間傳送出去(這里與包裝器的CRL事件毫不相干),在CLR事件是用Invoke方法,下面以按鈕為例 */ protected override void OnClick() { base.OnClick(); ***EventArgs args = new ***EventArgs(***Event, this); this.RaiseEvent(args); }
下面我們就來實現一個簡單的自定義路由功能,當路由飄過一個控件的時間,顯示通過該控件的時間。 上面介紹的差不多了,所以就直接上代碼,有需要解釋的話,再一個個解釋。
下面是XAML代碼:
<Window x:Class="DefineEvent.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:DefineEvent" Title="Routed Event" x:Name="window_1" Height="350" Width="525" local:TimeButton.ReportTime="ReportTimeHandler" > <Grid x:Name="grid_1" local:TimeButton.ReportTime="ReportTimeHandler" > <Grid x:Name="grid_2" local:TimeButton.ReportTime="ReportTimeHandler" > <Grid x:Name="grid_3" local:TimeButton.ReportTime="ReportTimeHandler" > <StackPanel x:Name="stackPanel_1" local:TimeButton.ReportTime="ReportTimeHandler" > <ListBox x:Name="listBox" /> <local:TimeButton x:Name="timeButton" Width="200" Height="80" Content="顯示到達某個位置的時間" ReportTime="ReportTimeHandler"/> </StackPanel> </Grid> </Grid> </Grid> </Window>
下面是CS代碼
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.ComponentModel; namespace DefineEvent { /// <summary> /// MainWindow.xaml 的交互邏輯 /// </summary> delegate void ReportTimeRouteEventHandler(object sender, ReportTimeEventArgs e); public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void ReportTimeHandler(object sender, ReportTimeEventArgs e) { FrameworkElement element = sender as FrameworkElement; string timeStr = e.ClickTime.ToString("yyyyMMddHHmmss"); string content = string.Format("{0}到達{1}", timeStr, element.Name); this.listBox.Items.Add(content); } } //創建消息類型,在此可以附加自己想要的信息 public class ReportTimeEventArgs : RoutedEventArgs { public ReportTimeEventArgs(RoutedEvent routedEvent, object source) : base(routedEvent, source) { } public DateTime ClickTime { get; set; } } public class TimeButton : Button { //1、為元素聲明並注冊事件 public static readonly RoutedEvent ReportTimeEvent = EventManager.RegisterRoutedEvent("ReportTime", RoutingStrategy.Bubble, typeof(ReportTimeRouteEventHandler), typeof(TimeButton)); //2、包裝事件 public event RoutedEventHandler ReportTime { add { this.AddHandler(ReportTimeEvent,value); } remove { this.RemoveHandler(ReportTimeEvent,value); } } //3、創建激發事件的方法 protected override void OnClick() { base.OnClick(); ReportTimeEventArgs args = new ReportTimeEventArgs(ReportTimeEvent,this); args.ClickTime = DateTime.Now; this.RaiseEvent(args); } } }
運行單擊按鈕的效果為圖3:
圖3
注意下面的一行代碼,在聲明和定義路由事件時,第三個參數,委托的類型和處理器方法的的參數一定要相同,不然會報錯,可以把下面一句中的ReportTimeRouteEventHandler換成RoutedEventHandler試試,會出現:無法從文本“ReportTimeHandler”創建“ReportTime”。”,行號為“5”,行位置為“30”,的問題,這里主要的原因就是委托的參數和RoutedEventHandler的參數不一致,雖然都是(sender, e);但是e的類型已經發生了變化,成為了ReportTimeEventArgs類型的。所以在使用之前,聲明一個委托就可以了。還有個方法是使用EventHandler<ReportTimeEventArgs>替換ReportTimeRouteEventHandler,其實二者的用法差不多,只是不同的寫法,但是是我感覺第一種寫法會更好理解。具體關於EventHandler<ReportTimeEventArgs>的含義請參考http://book.51cto.com/art/200811/98553.htm。我們同樣可以使用讓第二個參數改變成另外兩種類型的看看測試結果。
public static readonly RoutedEvent ReportTimeEvent = EventManager.RegisterRoutedEvent("ReportTime", RoutingStrategy.Tunnel, typeof(ReportTimeRouteEventHandler), typeof(TimeButton));
如果我們希望當事件傳遞到grid_2上面就停止了,我們可以這樣做:在ReportTimeHandler函數中添加代碼。
if (element.Name == "grid_2") e.Handled = true;
三、總結
本篇記錄的內容雖然不多,但是感覺記錄的時間特別費力,主要是因為對事件的幾個組成部分還不是非常熟練,而且在理解路由事件時,還要先理解邏輯樹與可視樹。最終還是把這一章看完了,但這個只是開始。
文章主要記錄了路由事件的在可視樹上的傳播以及自定義路由事件的實現。如果在文章有不同的見解或建議,歡迎交流! 下一篇:《深入淺出WPF》筆記——命令篇