3.3 事件也附加——深入淺出附加事件
WPF事件系統中還有一種事件被稱為附加事件(Attached Event),簡言之,它就是路由事件。“那為什么還要起個新名字呢?”你可能會問。
“身無彩鳳雙飛翼,心有靈犀一點通”,這就是對附加事件宿主的真實寫照。怎么解釋呢?讓我們看看都有哪些類擁有附加事件:
-
Binding類:SourceUpdated事件,TargetUpdated事件
-
Mouse類:MouseEnter事件、MouseLeave事件、MouseDown事件、MouseUp事件,等等
-
Keyboard類:KeyDown事件、KeyUp事件,等等
再對比一下那些擁有路由事件的類,諸如Button、Slider、TextBox……發現什么問題了嗎?原來,路由事件的宿主都是些擁有可視化實體的界面元素,而附加事件則不具備顯示在用戶界面上的能力。也就是說,附加事件的宿主沒有界面渲染功能這雙“飛翼”,但一樣可以使用附加事件這個“靈犀”與其他對象進行溝通。
理解了附加事件的原理,讓我們動手寫一個例子。我想實現的邏輯是這樣的:設計一個名為Student的類,如果Student實例的Name屬性值發生了變化就激發一個路由事件,我會使用界面元素來捕捉這個事件。
這個類的代碼如下:
1 public class Student 2 { 3 // 聲明並定義路由事件 4 public static readonly RoutedEvent NameChangedEvent = EventManager.RegisterRoutedEvent 5 ("NameChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Student)); 6 7 public int Id { get; set; } 8 public string Name { get; set; } 9 }
設計一個簡單的界面:
1 <Window x:Class="WpfApplication1.Window1" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Attached Event" 4 Height="200" Width="200"> 5 <Grid x:Name="gridMain"> 6 <Button x:Name="button1" Content="OK" Width="80" Height="80" 7 Click="Button_Click" /> 8 </Grid> 9 </Window>
其后台代碼如下:
1 public partial class Window1 : Window 2 { 3 public Window1() 4 { 5 InitializeComponent(); 6 7 // 為外層Grid添加路由事件偵聽器 8 this.gridMain.AddHandler( 9 Student.NameChangedEvent, 10 new RoutedEventHandler(this.StudentNameChangedHandler)); 11 } 12 13 // Click事件處理器 14 private void Button_Click(object sender, RoutedEventArgs e) 15 { 16 Student stu = new Student() { Id = 101, Name = "Tim" }; 17 stu.Name = "Tom"; 18 // 准備事件消息並發送路由事件 19 RoutedEventArgs arg = new RoutedEventArgs(Student.NameChangedEvent, stu); 20 this.button1.RaiseEvent(arg); 21 } 22 23 // Grid捕捉到NameChangedEvent后的處理器 24 private void StudentNameChangedHandler(object sender, RoutedEventArgs e) 25 { 26 MessageBox.Show((e.OriginalSource as Student).Id.ToString()); 27 } 28 }
后台代碼中,當界面上唯一的Button被點擊后會觸發Button_Click這個方法。有一點必須注意的是:因為Student不是UIElement的派生類,所以它不具有RaiseEvent這個方法,為了發送路由事件就不得不“借用”一下Button的RaiseEvent方法了。在窗體的構造器中我為Grid元素添加了對Student.NameChangedEvnet的偵聽——與添加對路由事件的偵聽沒有任何區別。Grid在捕捉到路由事件后會顯示事件消息源(一個Student實例)的Id。
運行程序並點擊按鈕,效果如圖:
理論上現在的Student類已經算是具有一個附加事件了,但微軟的官方文檔約定要為這個附加事件添加一個CLR包裝以便XAML編輯器識別並進行智能提示。可惜的是,Student類並非派生自UIElement,因此亦不具備AddHandler和RemoveHandler兩個方法,所以不能使用CLR屬性作為包裝器(因為CLR屬性包裝器的add和remove分支分別調用當前對象的AddHandler和RemoveHandler)。微軟規定:
-
為目標UI元素添加附加事件偵聽器的包裝器是一個名為Add*Handler的public static方法,星號代表事件名稱(與注冊事件時的名稱一致)。此方法接收兩個參數,第一個參數是事件的偵聽者(類型為DependencyObject),第二個參數為事件的處理器(RoutedEventHandler委托類型)。
-
解除UI元素對附加事件偵聽的包裝器是名為Remove*Handler的public static方法,星號亦為事件名稱,參數與Add*Handler一致。
按照規范,Student類被升級為這樣:
1 public class Student 2 { 3 // 聲明並定義路由事件 4 public static readonly RoutedEvent NameChangedEvent = EventManager.RegisterRoutedEvent 5 ("NameChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Student)); 6 7 // 為界面元素添加路由事件偵聽 8 public static void AddNameChangedHandler(DependencyObject d, RoutedEventHandler h) 9 { 10 UIElement e = d as UIElement; 11 if (e != null) 12 { 13 e.AddHandler(Student.NameChangedEvent, h); 14 } 15 } 16 17 // 移除偵聽 18 public static void RemoveNameChangedHandler(DependencyObject d, RoutedEventHandler h) 19 { 20 UIElement e = d as UIElement; 21 if (e != null) 22 { 23 e.RemoveHandler(Student.NameChangedEvent, h); 24 } 25 } 26 27 public int Id { get; set; } 28 public string Name { get; set; } 29 }
原來的代碼也需要做出相應的改動(只有添加事件偵聽一處需要改動):
1 public Window1() 2 { 3 InitializeComponent(); 4 5 // 為外層Grid添加路由事件偵聽器 6 Student.AddNameChangedHandler( 7 this.gridMain, 8 new RoutedEventHandler(this.StudentNameChangedHandler)); 9 }
現在讓我們仔細理解一下附加事件的“附加”。確切地說,UIElement類是路由事件宿主與附加事件宿主的分水嶺,不單是因為從UIElement類開始才具備了在界面上顯示的能力,還因為RaiseEvent、AddHander和RemoveHandler這些方法也定義在UIElement類中。因此,如果在一個非UIElement派生類中注冊了路由事件,則這個類的實例即不能自己激發(Raise)此路由事件也無法自己偵聽此路由事件,只能把這個事件的激發“附着”在某個具有RaiseEvent方法的對象上、借助這個對象的RaiseEvent方法把事件發送出去;事件的偵聽任務也只能交給別的對象去做。總之,附加事件只能算是路由事件的一種用法而非一個新概念,說不定哪天微軟就把附加事件這個概念撤消了。
最后分享些實際工作中的經驗。
第一,像Button.Click這些路由事件,因為事件的宿主是界面元素、本身就是UI樹上是一個結點,所以路由事件路由時的第一站就是事件的激發者。附加事件宿主不是UIElement的派生類,所以不能出現在UI樹上的結點,而且附加事件的激發是借助UI元素實現的,因此,附加事件路由的第一站是激發它的元素。
第二,實際上很少會把附加事件定義在Student這種與業務邏輯相關的類中,一般都是定義在像Binding、Mouse、Keyboard這種全局的Helper類中。如果需要業務邏輯類的對象能發送出路由事件來怎么辦?我們不是有Binding嗎!如果程序架構設計的好(使用數據驅動UI),那么業務邏輯一定會使用Binding對象與UI元素關聯,一旦與業務邏輯相關的對象實現了INotifyPropertyChanged接口並且Binding對象的NotifyOnSourceUpdated屬性設為true,則Binding就會激發其SourceUpdated附加事件,此事件會在UI元素樹上路由並被偵聽者捕獲。
轉載:http://blog.csdn.net/FantasiaX/article/details/4575968
相關:http://blog.csdn.net/fwj380891124/article/details/8139260