WPF - 善用路由事件


  在原來的公司中,編寫自定義控件是常常遇到的任務。但這些控件常常擁有一個不怎么好的特點:無論是內部還是外部都沒有使用路由事件。那我們應該怎樣宰自定義控件開發中使用路由事件?我們將在這篇短文中對該問題進行討論。

 

路由事件簡介

  談到路由事件,我想首先我們就需要問自己一個問題。在.net已經支持事件的情況下,為什么WPF還額外提供了對路由事件的支持?這是因為在WPF開發模型下,原始的CLR事件已經不能滿足開發的要求,從而導致對事件的處理異常繁瑣:

  首先就是控件的封裝。WPF中,我們可以將一個控件作為另一個控件的子控件,從而呈現豐富的效果。例如我們可以在一個Button中包含一個圖像。在這種情況下,對圖像的點擊實際上應該是對按鈕的點擊。正因為如此,我們期望真正觸發被點擊事件的控件是Button,而不是嵌在其中的圖像。這正好要求WPF將點擊事件沿視覺樹依次傳遞,即路由事件的路由功能。可以說,這是WPF添加路由事件的最直觀理由。

  同樣由於WPF提供了豐富的組合模型,一小塊程序界面組成中就可能包含了多個相同的界面元素。為了能在一處執行對特定事件的偵聽,而不是為這些界面組成依次添加事件處理函數。路由事件為這種情況提供了一種較為簡單的處理方式:在它們的公共父元素中添加事件處理函數。在該路由事件路由到該元素時,事件處理函數才會被調用。例如在為TreeView中為DragDrop功能提供支持的時候,您不可能在各個條目中依次標明對鼠標操作的響應,而應在TreeView元素中偵聽鼠標操作事件。

  除了這些較為明顯的優點之外,路由事件還提供了更為豐富的功能。首先,路由事件允許軟件開發人員通過EventManager.RegisterClassHandler()函數使用由類定義的靜態處理程序。這個類定義的靜態處理程序與類型的靜態構造函數有些類似:在路由事件到達路由中的元素實例時,WPF都會首先調用該類處理程序,然后再執行該實例所注冊的偵聽函數。這種控件編寫方式在WPF的內部實現中經常使用。另外,通過對路由事件進行管理的類型EventManager,我們可以通過函數調用GetRoutedEvents()得到相應的路由事件,而不再需要運用反射等較為耗時的方法。

  路由事件一般使用以下三種路由策略:1) 冒泡:由事件源向上傳遞一直到根元素。2) 直接:只有事件源才有機會響應事件。3) 隧道:從元素樹的根部調用事件處理程序並依次向下深入直到事件源。一般情況下,WPF提供的輸入事件都是以隧道/冒泡對實現的。隧道事件常常被稱為Preview事件。

  您可能會想,路由事件的直接路由方式與普通CLR事件的處理方式有什么不同呢?實際上並沒有什么不同。但是路由事件為WPF提供了更好的支持。例如觸發器等功能需要事件是路由事件。同時路由事件還提供了類處理機制,從而為WPF提供了更靈活的執行方式。在后面的章節中,您將看到WPF是如何通過類處理函數完成一些WPF常見功能的。

 

路由事件編程

  與依賴項屬性類似,WPF也為路由事件提供了WPF事件系統這一組成。為一個類型添加一個路由事件的方式與為類型添加依賴項屬性的方法類似:軟件開發人員需要通過EventManager的RegisterRoutedEvent()函數向事件系統注冊路由事件。該函數的簽名如下所示:

1 public static RoutedEvent RegisterRoutedEvent(string name, RoutingStrategy routingStrategy, 
2     Type handlerType, Type ownerType);

  該函數帶有四個參數:第一個參數name表示事件在WPF事件系統中的名稱,而第二個參數routingStrategy則標明了路由事件的路由原則。第三個參數handlerType用來標明事件處理函數的類型,而最后一個參數ownerType則用來標明擁有該路由事件的類型。例如,下面就是Control類注冊MouseDoubleClick事件的代碼:

1 public static readonly RoutedEvent MouseDoubleClickEvent = 
2     EventManager.RegisterRoutedEvent("MouseDoubleClick", RoutingStrategy.Direct, 
3         typeof(MouseButtonEventHandler), typeof(Control));

  該函數返回一個RoutedEvent類型的實例。一般情況下,該實例將由一個public static readonly字段所保存,並可以通過add和remove訪問符模擬為CLR事件。仍讓我們以MouseDoubleClick事件為例。Control類中的MouseDoubleClick事件的實現如下所示:

 1 public event MouseButtonEventHandler MouseDoubleClick
 2 {
 3     add
 4     {
 5         base.AddHandler(MouseDoubleClickEvent, value);
 6     }
 7     remove
 8     {
 9         base.RemoveHandler(MouseDoubleClickEvent, value);
10     }
11 }

  在前面的講解中我們已經提到過,EventManager類還提供了一個RegisterClassHandler()函數,以為特定路由事件注冊類處理程序。該函數的原型如下所示:

1 public static void RegisterClassHandler(Type classType, RoutedEvent routedEvent, 
2     Delegate handler, bool handledEventsToo);

  該函數的第一個參數用來指定注冊類處理函數的類型,而第二個參數則用來指定類處理函數所需要偵聽的事件。第三個參數則指明了類處理函數,而將最后一個參數設置為true則允許類處理函數能夠處理被標記為已處理的路由事件。

  由RegisterClassHandler()函數所注冊的類處理程序可以在各個實例的事件處理程序運行之前運行。在該類處理程序中,軟件開發人員可以選擇將事件標記為已處理,或將當前事件轉化為另一個事件。就仍以Control類的DoubleClick事件為例。Control類的靜態構造函數通過RegisterClassHandler()函數首先注冊了一個類處理程序:

EventManager.RegisterClassHandler(typeof(Control), UIElement.MouseLeftButtonDownEvent, 
    new MouseButtonEventHandler(Control.HandleDoubleClick), true);

  接下來,在類處理程序HandleDoubleClick()中,其將會在用戶雙擊時將MouseLeftButtonDown事件轉化為雙擊事件:

 1 private static void HandleDoubleClick(object sender, MouseButtonEventArgs e)
 2 {
 3     if (e.ClickCount == 2) // 對雙擊進行處理
 4     {
 5         Control control = (Control)sender;
 6         MouseButtonEventArgs args = new MouseButtonEventArgs(e.MouseDevice, 
 7             e.Timestamp, e.ChangedButton, e.StylusDevice);
 8         if ((e.RoutedEvent == UIElement.PreviewMouseLeftButtonDownEvent) 
 9             || (e.RoutedEvent == UIElement.PreviewMouseRightButtonDownEvent))
10         {
11             args.RoutedEvent = PreviewMouseDoubleClickEvent;
12             args.Source = e.OriginalSource;
13             args.OverrideSource(e.Source); // 注意這里對Source的處理
14             control.OnPreviewMouseDoubleClick(args); // 發出雙擊的Preview消息
15         }
16         else
17         {
18             args.RoutedEvent = MouseDoubleClickEvent;
19             args.Source = e.OriginalSource;
20             args.OverrideSource(e.Source);
21             control.OnMouseDoubleClick(args); // 發出雙擊消息
22         }
23         if (args.Handled)
24             e.Handled = true; // 將Handled設置為true,從而使該消息被隱藏
25     }
26 }

  需要注意的是,RegisterClassHandler()函數所注冊的類處理函數需要是靜態成員函數,因此您需要從參數中得到發出路由事件的類型實例。例如在上面的函數中,類處理函數就是通過參數sender得到實際發出路由事件的類型實例的。

  同時,上面的代碼還向您展示了在組件編程過程中隱藏消息的方法及實現自定義輸入事件的方法。在上面的代碼中,路由事件的響應函數會手動發出雙擊的消息,從而使控件的PreviewDoubleClick以及DoubleC         lick事件被觸發。接下來,路由事件的響應函數會將原事件的Handled屬性設置為true,進而使原本的低級輸入事件被隱藏。這種將低級事件隱藏並轉化為高級事件的方法在WPF中非常常見。就以我們常用的Button類為例。在鼠標按下的時候,我們會接收到PreviewMouseDown事件,卻不能接收到MouseDown事件。您一方面需要理解並掌握該方法,另一方面,您在遇到該情況時應能估計到產生該情況的原因,更能使用相應的解決方案:Preview-事件。

  除了通過RegisterRoutedEvent()函數之外,軟件開發人員還可以通過RoutedEvent的AddOwner()函數將其它類型所定義的路由事件作為自身的路由事件。RoutedEvent的成員函數AddOwner()函數的原型如下:

1 public RoutedEvent AddOwner(Type ownerType)

  該函數同樣返回一個RoutedEvent實例並可以在CLR包裝中使用。就以UIElement類所提供的MouseMove事件為例。WPF首先通過AddOwner()函數添加了對事件的引用:

1 public static readonly RoutedEvent MouseMoveEvent = 
2     Mouse.MouseMoveEvent.AddOwner(typeof(UIElement));

  接下來,您仍需要按照通常的方式為該附加事件添加一個CLR事件包裝:

 1 public event MouseEventHandler MouseMove
 2 {
 3     add
 4     {
 5         this.AddHandler(Mouse.MouseMoveEvent, value, false);
 6     }
 7     remove
 8     {
 9         this.RemoveHandler(Mouse.MouseMoveEvent, value);
10     }
11 }

  最后要說的則是如何處理Handled已經被設置為true的路由事件。在需要處理這種類型事件的時候,您首先需要考慮的是,您當前的解決方案是否有略欠妥當的地方。如果您有足夠強的理由證明自己對Handled屬性已經被設置為true的路由事件的處理是有必要的,您需要通過AddHandler函數添加對路由事件的偵聽,並在該函數調用中設置屬性handledEventsToo參數的值為true。除此之外,軟件開發人員還可以通過EventSetter中的HandledEventsToo屬性實現相同的功能。

 

附加事件

  和附加屬性與依賴項屬性之間的關系相對應,WPF的事件系統也支持普通的路由事件以及附加事件。與附加屬性具有完全不同的語法實現不同,附加事件所使用的語法與普通的路由事件沒有什么不同。例如,下面的代碼中,對Image.MouseDown事件的使用就是對普通路由事件的使用:

1 <StackPanel Image.MouseDown=…>

  而對Mouse.MouseDown事件的使用就是對附加路由事件的使用:

1 <StackPanel Mouse.MouseDown=…>

  在上面的兩段代碼中,我們都使用了“類型名稱.事件名稱”的限定事件語法。但是它們一個屬於普通的路由事件,一個是附加事件。那到底怎樣辨別哪些是路由事件,哪些是附加事件呢。實際上,附加事件更主要的是其所具有的語義特征。與附加屬性所擁有的服務特性類似,附加事件也常常對應着一個全局服務,例如Mouse類所對應的鼠標輸入服務。這些服務可能並不會在XAML中作為當前元素的子元素存在。

  我們前面已經看到了,WPF的眾多類型都通過AddOwner()函數調用等一系列方法將一些服務所提供的路由事件整合進類型定義中,例如UIElement類對Mouse類所提供的各個路由事件的集成。這常常是控件編寫者所采用的一種控件編寫策略。畢竟系統服務常常是較為低級的API。將其集成到類型中一方面可以直接使用該事件,而不是路由事件的限定形式,另一方面也令XAML對事件的表示更為直觀。

  當然,您不要以為僅僅是輸入等底層組成需要創建服務,其實在控件開發過程中,對這種服務的使用也是常常出現的。就以Selector為例。在您查看TreeViewItem,ListBoxItem等組成的實現時,您就會發現它們通過AddOwner()函數添加了對Selector.Selected事件的使用,而Selector自身則添加了對該事件的偵聽,並最終轉化為路由事件SelectionChanged。這是一個非常明顯的對附加路由事件的使用。之所以將選中事件實現為一個服務則是因為對各個項目的選中操作常常發生在各個條目的內部,如鼠標的點擊,因此在這些條目中處理選中事件並將該事件路由至Selector是一種較好的解決方法。

  那如何創建一個路由事件呢?答案就是為控件添加AddYourEventHandler()以及RemoveYourEventHandler()兩個函數。這兩個函數的第一個參數都需要標明需要操作的事件,事件的名稱需要與YourEvent所代表的名稱相匹配。而第二個參數則是需要為該附加事件指定的處理程序。就以Mouse類所提供的函數AddMouseDownHandler()以及RemoveMouseDownHandler()為例:

 1 public static void AddMouseDownHandler(DependencyObject element, 
 2     MouseButtonEventHandler handler)
 3 {
 4     UIElement.AddHandler(element, MouseDownEvent, handler);
 5 }
 6 
 7 public static void RemoveMouseDownHandler(DependencyObject element, 
 8     MouseButtonEventHandler handler)
 9 {
10     UIElement.RemoveHandler(element, MouseDownEvent, handler);
11 }

  這樣,您就可以在XAML中通過Mouse.MouseDown引用Mouse類所提供的MouseDown附加事件了。

 

轉載請注明原文地址:http://www.cnblogs.com/loveis715/archive/2012/04/09/2439803.html

商業轉載請事先與我聯系:silverfox715@sina.com

更多精彩文章,請查看博客主頁:http://www.cnblogs.com/loveis715/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM