我們都知道路由事件,而附加事件用的比較少。
但如果是通用的場景,類似附加屬性,附加事件就很有必要的。
舉個例子,輸入裝置有很多種,WPF中輸入事件主要分為滑鼠、觸控、觸筆:WPF 螢幕點選的裝置型別 - 唐宋元明清2188 - 部落格園 (cnblogs.com)
有這麼多輸入事件Mouse、Touch、Stylus,另外按鈕Click還處理了冒泡事件,事件型別就更多了。
但絕大部分業務其實並不關心事件型別,只需要一個觸發事件就行了。
所以我們有這樣的需求:設計並提供一個通用的輸入事件,大家只需要拿到事件進行業務操作。另外一些小場景,如果需要具體事件如觸控點集,可以從事件源引數內部去獲取。
具體的通用輸入事件,我們另外討論,這裡主要描述如何自定義附加事件
附加事件
WPF官方對附加事件的描述 - 附加事件概述 - WPF .NET Framework | Microsoft Learn
所以我們先定義一個附加事件類:
1 public class DeviceEvents 2 { 3 /// <summary> 4 /// 按壓事件 5 /// </summary> 6 public static readonly RoutedEvent PreviewDeviceDownEvent = EventManager.RegisterRoutedEvent("PreviewDeviceDown", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(DeviceEvents)); 7 public static void AddPreviewDeviceDownHandler(DependencyObject d, RoutedEventHandler handler) 8 { 9 (d as UIElement)?.AddHandler(DeviceEvents.PreviewDeviceDownEvent, handler); 10 } 11 public static void RemovePreviewDeviceDownHandler(DependencyObject d, RoutedEventHandler handler) 12 { 13 (d as UIElement)?.RemoveHandler(DeviceEvents.PreviewDeviceDownEvent, handler); 14 } 15 }
然後,我們看下如何封裝原有事件PreviewMouseDown、PreviewStylusDown,並轉換成此PreviewDeviceDown事件
在介面上新增下事件:
1 <Button x:Name="TestButton" 2 Content="測試" Height="30" Width="120" 3 local:DeviceEvents.PreviewDeviceDown="TestButton_OnPreviewDeviceDown"/>
我們會發現介面載入時,AddPreviewDeviceDownHandler不會觸發呼叫。附加事件,與附加屬性原理不一樣,附加事件相當於在編譯時注入委託(事件處理程式)AddPreviewDeviceDownHandler,介面載入時不會執行這個委託註冊。
所以,我們可以透過另一個附加屬性來開關附加事件,並且內部整合事件的觸發。具體設計如下:
1 public class DeviceEvents 2 { 3 private static readonly List<DeviceEventTransformer> EventTransformers = new List<DeviceEventTransformer>(); 4 5 /// <summary> 6 /// 是否開啟附加事件 7 /// </summary> 8 public static readonly DependencyProperty IsOpenProperty = DependencyProperty.RegisterAttached( 9 "IsOpen", typeof(bool), typeof(DeviceEvents), new PropertyMetadata(default(bool), OnIsOpenPropertyChanged)); 10 private static void OnIsOpenPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 11 { 12 if (d is UIElement element && e.NewValue is bool isOpen) 13 { 14 var eventTransformer = EventTransformers.FirstOrDefault(i => i.Target == element); 15 if (isOpen) 16 { 17 if (eventTransformer == null) 18 { 19 eventTransformer = new DeviceEventTransformer(element); 20 EventTransformers.Add(eventTransformer); 21 } 22 eventTransformer.Register(); 23 } 24 else 25 { 26 eventTransformer?.UnRegister(); 27 } 28 } 29 } 30 31 public static void SetIsOpen(DependencyObject element, bool value) 32 { 33 element.SetValue(IsOpenProperty, value); 34 } 35 public static bool GetIsOpen(DependencyObject element) 36 { 37 return (bool)element.GetValue(IsOpenProperty); 38 } 39 40 /// <summary> 41 /// 按壓事件 42 /// </summary> 43 public static readonly RoutedEvent PreviewDeviceDownEvent = EventManager.RegisterRoutedEvent("PreviewDeviceDown", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(DeviceEvents)); 44 public static void AddPreviewDeviceDownHandler(DependencyObject d, RoutedEventHandler handler) 45 { 46 (d as UIElement)?.AddHandler(DeviceEvents.PreviewDeviceDownEvent, handler); 47 } 48 public static void RemovePreviewDeviceDownHandler(DependencyObject d, RoutedEventHandler handler) 49 { 50 (d as UIElement)?.RemoveHandler(DeviceEvents.PreviewDeviceDownEvent, handler); 51 } 52 } 53 /// <summary> 54 /// 事件轉換器 55 /// </summary> 56 public class DeviceEventTransformer 57 { 58 private readonly UIElement _uiElement; 59 60 public DeviceEventTransformer(UIElement uiElement) 61 { 62 _uiElement = uiElement; 63 } 64 public UIElement Target => _uiElement; 65 66 public void Register() 67 { 68 _uiElement.PreviewMouseDown += UiElement_PreviewMouseDown; 69 _uiElement.PreviewStylusDown += UiElement_PreviewStylusDown; 70 } 71 72 public void UnRegister() 73 { 74 _uiElement.PreviewMouseDown -= UiElement_PreviewMouseDown; 75 _uiElement.PreviewStylusDown += UiElement_PreviewStylusDown; 76 } 77 78 private void UiElement_PreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) 79 { 80 OnPreviewDeviceDown(e); 81 } 82 private void UiElement_PreviewStylusDown(object sender, StylusDownEventArgs e) 83 { 84 OnPreviewDeviceDown(e); 85 } 86 87 private void OnPreviewDeviceDown(InputEventArgs e) 88 { 89 RoutedEventArgs reArgs = new RoutedEventArgs(DeviceEvents.PreviewDeviceDownEvent, e); 90 _uiElement.RaiseEvent(reArgs); 91 } 92 }
DeviceEventTransformer是一個事件封裝器,如果只是後臺程式碼,也可以直接使用DeviceEventTransformer訂閱相關事件。
上面事件實現是一個案例,這裡的程式碼比較粗糙,沒有對滑鼠、觸控做互斥處理。另外其它的事件型別如冒泡、隧道、Down、Up等,後續可以將DeviceEvents封裝完整。
附加事件使用
在介面上,我們可以直接在任意控制元件上新增此事件:
先新增DeviceEvents.IsOpen="True"開啟裝置附加事件,
然後訂閱相應的附加事件DeviceEvents.PreviewDeviceDown="TestGrid_OnPreviewDeviceDown"
Demo:DeviceEventsDemo: WPF附加事件DEMO (gitee.com)