WPF,Silverlight与XAML读书笔记第八 - WPF新概念之三路由事件
说明:本系列基本上是《WPF揭秘》的读书笔记。在结构安排与文章内容上参照《WPF揭秘》的编排,对内容进行了总结并加入一些个人理解。
路由事件是专门设计用于在元素树中使用的事件。当路由事件触发后,其可以向上或向下遍历可视树和逻辑树,并且持续在每个元素上触发。
我们以按钮为例,说明路由事件的作用。在WPF之前的Windows客户端技术WinForm中按钮仅仅就是一个System.Windows.Forms命名空间下的Button类的对象。对其的点击等操作可以直接捕获,但是WPF中展现给你的按钮的组成可能会很复杂。其可能包括ButtonChrome或者TextBlock等可视子元素或者包含Rectangle等逻辑子元素,这时候对一个按钮的点击(MouseLeftButtonDown事件或KeyDown事件)可能实际发生在其可视子元素或逻辑子元素上,这时使用路由事件,由于其遍历可视树/逻辑树,Button的Click(路由)事件最终会被触发。
路由事件的实现
同依赖属性,路由事件也只有XAML语言天生支持,在传统过程式代码中注册路由事件的代码如下(以Button的Click路由事件为例)。首先注意的是,路由事件的约定名称是在传统事件名称的后面加上Event后缀(类似依赖属性的Property后缀)。
示例代码:
1 2 | //声明路由事件 public static readonly RoutedEvent ClickEvent; |
首先在Button类中声明这个路由事件。接着,
1 2 3 4 5 6 7 | //注册路由事件 Button.ClickEvent = EventManager.RegisterRoutedEvent( "Click" , RoutingStrategy.Bubble, typeof (RoutedEventHandler), typeof (Button) ); |
在Button类的静态构造函数中初始化这个路由事件。
最后添加可选的.NET事件包装器
1 2 3 4 5 6 | //(可选).NET事件包装器 public event RoutedEventHandler Click { add { AddHandler(Button.ClickEvent, value); } remove { RemoveHandler(Button.ClickEvent, value); } } |
触发路由事件的代码(可以位于OnMouseLeftButtonDown处理程序中)
1 | RaiseEvent( new RoutedEventArgs(Button.ClickEvent, this )); |
分析:
RegisterRoutedEvent方法中传入的第一个参数定义了一个普通的.NET事件,事件包装器对这个事件进行包装。这样可以使过程式代码中事件使用更接近原有模式,并且XAML中也可以更方便的添加事件处理程序。
.NET事件包装器与内部实现
事件包装器内部调用的AddHandler与RemoveHandler方法位于继承自DependencyObject对象的UIElement对象。这两个方法分别可以向一个适当的路由事件添加一个委托对象及由路由事件移除一个委托对象。事件包装器中只能调用AddHandler和RemoveHandler,不能做它用,这点与属性包装器类似。
路由策略
在前文注册路由事件的代码中你已经见过RoutingStrategy这个枚举。与此相关的就是路由策略的概念,路由策略指的是事件触发遍历元素树的方式。有以下三种:
-
Tunneling(管道传递)
事件首先在根元素上被触发,然后向下沿着树中每一个元素传递,直到到达源元素为止(或者直到处理程序把事件标记为已处理为止)。
-
Bubbling(冒泡传递)
事件首先在源元素上被触发,然后向上沿着树中每一个元素传递,直到到达根元素为止(或者直到处理程序把事件标记为已处理为止)。
-
Direct(直接)
事件仅在源元素上触发。这与普通.NET事件的行为基本相同。不同之处在于此类事件仍会参与事件触发器等路由事件的特定机制。
路由事件处理程序
路由事件处理程序(RoutedEventHandler)与普通的.NET事件的处理程序的设计遵循相同的模式:第一个参数为名为Sender的System.Object对象表示流处理程序被添加到的元素。第二个元素是名为e(通常习惯)的派生自RoutedEventArgs或其子类(这些类都派生自System.EventArgs类)的对象。这个对象有4个属性,通过它们变向扩展了提供给路由事件的参数。
-
Source:逻辑树中第一个触发该事件的元素。
-
OriginalSource:可视树中第一个触发该事件的元素(如点击Button首先触发的可视树为ButtonChrome,这个及前者的属性多用于区分鼠标事件之类的物理事件)。
-
Handled:布尔值,设置为true来标记事件为已处理,用于停止Tunneling或Bubbling事件在树上的触发。
-
RoutedEvent:真正的路由事件对象(如Button.ClickEvent),当多个路由事件共享一个事件处理函数时,此属性可以用来区分这个事件。
路由事件的作用
路由事件定义于UIElement类中,主要用于键盘,鼠标,触控笔之类触发的物理事件。而且这些路由事件往往是成对定义的即对于一个物理事件存在一个管道事件与一个冒泡事件相对应。其中管道事件的惯例是以Preview前缀开头。这个管道事件会在对应的冒泡事件之前触发,WPF的内部元素也只会响应冒泡事件。所以你可以在管道事件发生的过程中预览事件或者采取停止事件等措施。(实例:对于TextBox可以处理其KeyDown的管道事件(PreviewKeyDown事件)来严格限制其中输入的内容)。如果内容不合要求则在PreviewKeyDown事件发生的过程中标记事件为已处理,这样KeyDown冒泡事件将不再触发,则对TextBox的操作不会有任何效果。
注意在冒泡事件由source逐级向声明其的对象触发的过程中,可以被其中的接受事件的对象处理,并终止此冒泡事件。
最佳控制事件的方式
在路由事件的Preview版本中添加事件处理程序。
路由事件(管道或冒泡)的终止都是表面的,其只是被标记为已处理,但事件传递会继续,只是事件处理程序只处理没有标记为已处理的事件。
路由事件既可以沿可视树传递,也可以沿逻辑树传递。
附加事件
当树中(逻辑树或可视树)的每个元素都有路由事件时,路由事件(管道类型与冒泡类型)自然可以在其中传递。有了附加事件,事件传递也可以在一个没有定义这些事件的元素上进行。由于定义附加事件的元素本身不支持此事件,所以要在此附加事件上定义处理此事件(路由事件)的方法的名称。
示例:如下XAML代码将ListBox的SelectionChanged路由事件与Button的Click路由事件作为附加事件提供给window元素。
<Window ... ListBox.SelectionChanged="ListBox_SelectionChanged" Button.Click="Button_Click">
与这些XAML对应的过程代码实现:
1 2 | this .AddHandler(ListBox.SelectionChangedEvent, new SelectionChangedEventHandler(ListBox_SelectionChanged)); this .AddHandler(Button.ClickEvent, new RoutedEventHandler(Button_Click)); |
需要注意的是这些过程代码应置于InitializeComponent()后。
本文完
参考:
《WPF揭秘》
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异