Windows Presentation Foundation(WPF)应用程序开发人员和组件作者可以使用路由事件通过元素树传播事件,并在树中的多个侦听器上调用事件处理程序。 在公共语言运行时 (CLR) 事件中找不到这些功能。 多个 WPF 事件是路由事件,例如 ButtonBase.Click。 本文讨论基本的路由事件概念,并提供有关何时以及如何响应路由事件的指导。
先决条件
本文假定基本了解公共语言运行时(CLR)、面向对象的编程,以及如何将 WPF 元素布局 概念化为树。 若要遵循本文中的示例,如果熟悉可扩展应用程序标记语言(XAML),并且知道如何编写 WPF 应用程序,则很有帮助。
什么是路由事件?
可以从功能或实现角度考虑路由事件:
从 功能 的角度来看,路由事件是一种事件类型,可以在元素树中的多个侦听器上调用处理程序,而不仅仅是在事件源上调用处理程序。 事件侦听器是附加和调用事件处理程序的元素。 事件源是最初引发事件的元素或对象。
从 实现 的角度来看,路由事件是向 WPF 事件系统注册的事件,由类的 RoutedEvent 实例提供支持,并由 WPF 事件系统处理。 通常,路由事件是使用 CLR 事件“包装器”实现的,以便在 XAML 和代码隐藏中启用附加处理程序,就像 CLR 事件一样。
WPF 应用程序通常包含许多元素,这些元素是在 XAML 中声明的,要么在代码中实例化。 应用程序元素存在于其元素树中。 根据路由事件的定义方式,在源元素上引发该事件时,:
- 从源元素向根元素(通常是页面或窗口)在元素树中逐层上升。
- 从根元素下行经过元素树到达源元素。
- 不会遍历元素树,并且仅在源元素上发生。
请考虑以下部分元素树:
<Border Height="30" Width="200" BorderBrush="Gray" BorderThickness="1">
<StackPanel Background="LightBlue" Orientation="Horizontal" Button.Click="YesNoCancelButton_Click">
<Button Name="YesButton">Yes</Button>
<Button Name="NoButton">No</Button>
<Button Name="CancelButton">Cancel</Button>
</StackPanel>
</Border>
元素树按如下所示呈现:
这三个按钮中的每一个都是潜在的 Click 事件源。 当单击其中一个按钮时,它会引发一个事件,该事件从按钮向上冒泡到根元素。
Button和Border元素没有事件处理程序附加, 但StackPanel有事件处理程序。 在树的更高层中,其他未显示的元素也可能附加了 Click
事件处理程序。 当Click
事件到达StackPanel
元素时,WPF 事件系统将调用附加到它的YesNoCancelButton_Click
处理程序。 在示例中,Click
事件的路由是:Button
->StackPanel
->Border
-> 依次向上追溯的父元素。
注释
最初引发路由事件的元素在事件处理程序参数中被标识为 RoutedEventArgs.Source。 事件侦听器是附加和调用事件处理程序的元素,标识为事件处理程序参数中的 发送方 。
路由事件的主要场景
以下是一些促使路由事件概念产生的情境,并将其与典型 CLR 事件区分开的原因:
控件组合和封装:WPF 中的各种控件具有丰富的内容模型。 例如,您可以将图像放置在 Button 内,从而有效地扩展按钮的视觉树。 但是,添加的图像不得中断按钮的命中测试行为,当用户单击图像像素时需要做出响应。
单一处理程序附件点:可以为每个按钮
Click
的事件注册处理程序,但对于路由事件,可以附加单个处理程序,如前面的 XAML 示例所示。 这样,便可以更改单一处理程序下的元素树,例如添加或删除更多按钮,而无需注册每个按钮Click
的事件。Click
引发事件时,处理程序逻辑可以确定事件来自何处。 在前面显示的 XAML 元素树中指定的以下处理程序包含该逻辑:private void YesNoCancelButton_Click(object sender, RoutedEventArgs e) { FrameworkElement sourceFrameworkElement = e.Source as FrameworkElement; switch (sourceFrameworkElement.Name) { case "YesButton": // YesButton logic. break; case "NoButton": // NoButton logic. break; case "CancelButton": // CancelButton logic. break; } e.Handled = true; }
Private Sub YesNoCancelButton_Click(sender As Object, e As RoutedEventArgs) Dim frameworkElementSource As FrameworkElement = TryCast(e.Source, FrameworkElement) Select Case frameworkElementSource.Name Case "YesButton" ' YesButton logic. Case "NoButton" ' NoButton logic. Case "CancelButton" ' CancelButton logic. End Select e.Handled = True End Sub
类处理:路由事件支持在类中定义的 类事件处理程序 。 类处理程序优先于类的任何实例上的实例处理程序处理同一事件。
引用没有反射的事件:每个路由事件都会创建一个 RoutedEvent 字段标识符,以提供可靠的事件识别技术,不需要静态或运行时反射来标识事件。
如何实现路由事件
路由事件是向 WPF 事件系统注册的事件,由类的 RoutedEvent 实例提供支持,并由 WPF 事件系统处理。 从RoutedEvent
注册获取的实例通常存储为注册该实例的类的public static readonly
成员。 该类被称为事件“拥有者”类。 通常,一个路由事件会实现一个具有相同名称的 CLR 事件“封装器”。 CLR 事件包装器包含 add
和 remove
访问器,用于通过特定于语言的事件语法在 XAML 和后台代码中启用附加处理程序。
add
和remove
访问器会重写其 CLR 实现并调用路由事件AddHandler和RemoveHandler方法。 路由事件支持和连接机制在概念上类似于依赖属性是一个 CLR 属性,该属性由 DependencyProperty 该类提供支持,并在 WPF 属性系统中注册。
以下示例注册 Tap
路由事件、存储返回 RoutedEvent
的实例并实现 CLR 事件包装。
// Register a custom routed event using the Bubble routing strategy.
public static readonly RoutedEvent TapEvent = EventManager.RegisterRoutedEvent(
name: "Tap",
routingStrategy: RoutingStrategy.Bubble,
handlerType: typeof(RoutedEventHandler),
ownerType: typeof(CustomButton));
// Provide CLR accessors for adding and removing an event handler.
public event RoutedEventHandler Tap
{
add { AddHandler(TapEvent, value); }
remove { RemoveHandler(TapEvent, value); }
}
' Register a custom routed event using the Bubble routing strategy.
Public Shared ReadOnly TapEvent As RoutedEvent = EventManager.RegisterRoutedEvent(
name:="Tap",
routingStrategy:=RoutingStrategy.Bubble,
handlerType:=GetType(RoutedEventHandler),
ownerType:=GetType(CustomButton))
' Provide CLR accessors for adding and removing an event handler.
Public Custom Event Tap As RoutedEventHandler
AddHandler(value As RoutedEventHandler)
[AddHandler](TapEvent, value)
End AddHandler
RemoveHandler(value As RoutedEventHandler)
[RemoveHandler](TapEvent, value)
End RemoveHandler
RaiseEvent(sender As Object, e As RoutedEventArgs)
[RaiseEvent](e)
End RaiseEvent
End Event
路由策略
路由事件使用三种路由策略之一:
事件冒泡:最初,调用事件源上的事件处理程序。 然后,路由事件将路由到连续的父元素,并依次调用其事件处理程序,直到到达元素树根。 大多数路由事件都使用冒泡路由策略。 冒泡路由事件通常用于报告复合控件或其他 UI 元素的输入或状态更改。
隧道:最初,将调用元素树根处的事件处理程序。 然后,路由事件将路由到连续的子元素,并依次调用其事件处理程序,直到到达事件源。 遵循隧道路由的事件也称为 预览 事件。 WPF 输入事件通常以 预览和冒泡配对的形式实现。
直接:仅调用事件源上的事件处理程序。 此非路由策略类似于 Windows 窗体 UI 框架事件,即标准 CLR 事件。 与 CLR 事件不同,直接路由事件支持 类处理 ,并且可由 EventSetters 和 EventTriggers 使用。
为何使用路由事件?
作为应用程序开发人员,你并不总是需要知道或关心要处理的事件是作为路由事件实现的。 路由事件具有特殊行为,但如果在触发事件的元素上处理,那么这种行为在大部分情况下是看不见的。 但是,当您想要将事件处理程序附加到父元素,以处理由子元素(例如在组合控件中)引发的事件时,路由事件就显得尤为重要。
路由事件侦听器不需要它们处理的路由事件成为其类的成员。
UIElement 或 ContentElement 可以作为任何路由事件的事件侦听器。 由于视觉元素派生自 UIElement
或 ContentElement
派生,因此可以将路由事件用作支持在应用程序中不同元素之间交换事件信息的概念性“接口”。 路由事件的“接口”概念特别适用于 输入事件。
路由事件支持在事件路由中的元素之间交换事件信息,因为每个侦听器都有权访问相同的事件数据实例。 如果一个元素更改事件数据中的内容,该更改对事件路由中的后续元素可见。
除了路由方面的考虑,出于以下这些原因,可以选择实现路由事件而不是标准的CLR事件:
某些 WPF 样式设置和模板功能(如 EventSetters 和 EventTriggers)要求引用的事件是路由事件。
路由事件支持 类事件处理程序 ,这些处理程序会在侦听器类的任何实例处理同一事件的实例处理程序之前处理事件。 此功能在控件设计中很有用,因为类处理程序可以强制实施实例处理程序无法意外禁止的事件驱动类行为。
添加并实现路由事件处理器
在 XAML 中,通过将事件名称声明为事件侦听器元素的属性,将事件处理程序附加到元素。 属性值是处理程序方法名称。 处理程序方法必须在 XAML 页面的后台代码分部类中实现。 事件侦听器是附加和调用事件处理程序的元素。
对于侦听器类的事件(无论是继承还是其他方式获得的成员事件),可以按如下所示附加事件处理器:
<Button Name="Button1" Click="Button_Click">Click me</Button>
如果事件不是侦听器类的成员,则必须以以下形式 <owner type>.<event name>
使用限定的事件名称。 例如,由于StackPanel类未实现Click事件,所以如果要将事件处理程序附加到传递到Click
元素的StackPanel
事件上,需要使用限定的事件名称语法。
<StackPanel Name="StackPanel1" Button.Click="Button_Click">
<Button>Click me</Button>
</StackPanel>
代码隐藏中的事件处理程序方法的签名必须与路由事件的委托类型匹配。
Click事件的RoutedEventHandler委托的sender
参数指定事件处理程序附加到的元素。 委托 args
的参数 RoutedEventHandler
包含事件数据。 对于Button_Click
事件处理程序,一个可能的兼容代码隐藏实现是:
private void Button_Click(object sender, RoutedEventArgs e)
{
// Click event logic.
}
Private Sub Button_Click(sender As Object, e As RoutedEventArgs)
' Click event logic.
End Sub
虽然 RoutedEventHandler 是基本的路由事件处理程序委托,但某些控件或实现方案可能需要使用支持更特定事件数据的不同委托。 例如,对于DragEnter路由事件,您的处理程序应实现DragEventHandler委托。 这样做后,处理程序代码可以访问事件数据中的DragEventArgs.Data属性,该属性包含拖拽操作中的剪贴板数据。
添加路由事件处理程序的 XAML 语法与标准 CLR 事件处理程序的语法相同。 有关在 XAML 中添加事件处理程序的详细信息,请参阅 WPF 中的 XAML。 有关如何使用 XAML 将事件处理程序附加到元素的完整示例,请参阅 如何处理路由事件
若要使用代码将路由事件的事件处理程序附加到元素,通常有两个选项:
直接调用 AddHandler 该方法。 始终可以这样附加路由事件处理程序。 此示例使用
AddHandler
此方法将Click
事件处理程序附加到按钮:Button1.AddHandler(ButtonBase.ClickEvent, new RoutedEventHandler(Button_Click));
Button1.[AddHandler](ButtonBase.ClickEvent, New RoutedEventHandler(AddressOf Button_Click))
若要将按钮的
Click
事件处理程序附加到事件路由中的其他元素,例如名为StackPanel的StackPanel1
内:StackPanel1.AddHandler(ButtonBase.ClickEvent, new RoutedEventHandler(Button_Click));
StackPanel1.[AddHandler](ButtonBase.ClickEvent, New RoutedEventHandler(AddressOf Button_Click))
如果路由事件实现 CLR 事件包装器,请使用特定于语言的事件语法添加事件处理程序,就像对标准 CLR 事件一样。 大多数现有的 WPF 路由事件实现 CLR 包装器,从而启用特定于语言的事件语法。 此示例使用特定于语言的语法将事件处理程序附加到
Click
按钮:Button1.Click += Button_Click;
AddHandler Button1.Click, AddressOf Button_Click
有关如何在代码中附加事件处理程序的示例,请参阅 如何使用代码添加事件处理程序。 如果要在 Visual Basic 中编码,还可以使用 Handles
关键字添加处理程序作为处理程序声明的一部分。 有关详细信息,请参阅 Visual Basic 和 WPF 事件处理。
已处理的概念
所有路由事件共享事件数据的通用基类,即类 RoutedEventArgs 。 该 RoutedEventArgs
类定义布尔 Handled 属性。 该属性的目的是 Handled
让任何事件处理程序沿事件路由将路由事件标记为 已处理。 若要将事件标记为已处理,请在事件处理程序代码中将Handled
的值设置为true
。
Handled
的值会影响路由事件在沿事件路由传输时的处理方式。 如果 Handled
位于 true
路由事件的共享事件数据中,则通常不会为该特定事件实例调用附加到其他元素的处理程序。 对于最常见的处理程序方案,将事件标记为已处理实际上会阻止事件路由上的后续处理程序(无论是实例还是类处理程序)响应该特定事件实例。 在极少情况下,如果需要事件处理程序响应已标记为已处理的路由事件,您可以:
使用 AddHandler(RoutedEvent, Delegate, Boolean) 重载在代码隐藏中附加处理程序,并将
handledEventsToo
参数设置为true
.将 HandledEventsToo 属性
EventSetter
设置为true
.
这一概念 Handled
可能会影响如何设计应用程序和编写事件处理程序的代码。 可以概念化 Handled
为用于处理路由事件的简单协议。 使用此协议的方式由你决定,但参数的预期用途 Handled
是:
如果路由事件标记为已处理,则不需要通过路由的其他元素再次处理该事件。
如果路由事件没有被标记为已处理,那么事件路由中的前面的监听器可能没有事件处理程序,或者已注册的处理程序都没有以一种足以将事件标记为已处理的方式响应事件。 当前侦听器上的处理程序有三种可能的行动方案:
根本不采取任何作。 事件保持不变,并路由到树中的下一个侦听器。
执行代码来响应事件,但不要达到足以使事件被标记为已处理的程度。 事件保持不变,并路由到树中的下一个侦听器。
运行代码以响应事件,直到足以将该事件标记为已处理。 在事件数据中将事件标记为已处理。 该事件仍会路由到树中的下一个侦听器,但大多数侦听器不会调用进一步的处理程序。 异常是具有专门注册到
handledEventsToo
的true
处理程序的侦听器。
有关处理路由事件的详细信息,请参阅将 路由事件标记为已处理,以及类处理。
尽管只处理引发该事件的对象的冒泡路由事件的开发人员可能不关心其他侦听器,但最好还是将事件标记为已处理。 如果沿事件路由的元素具有同一路由事件的处理程序,则这样做可以防止意外的副作用。
类处理程序
路由事件处理程序可以是 实例 处理程序或 类 处理程序。 在响应该类的任何实例上的同一事件的实例处理程序之前,将调用给定类的类处理程序。 由于此行为,当路由事件被标记为已处理时,它们通常会在类处理程序中标记为此类事件。 有两种类型的类处理程序:
- 静态类事件处理程序,通过调用 RegisterClassHandler 静态类构造函数中的方法进行注册。
- 重写通过覆盖基类虚拟事件方法注册的事件处理程序。 基类虚拟事件方法主要存在于输入事件中,并且具有以 On<事件名称和>OnPreview<事件名称>开头的名称。
某些 WPF 控件具有某些路由事件的固有类处理。 类处理可能会让人感觉路由事件从未被引发,但实际上,它已被类处理器标记为已处理。 如果需要事件处理程序响应已处理的事件,您可以将处理程序注册,并将handledEventsToo
设置为true
。 关于实现自己的类处理程序或解决不期望的类处理问题的详细信息,请参阅 将路由事件标记为已处理,以及类处理。
WPF 中的附加事件
XAML 语言还定义了一种称为 附加事件的特殊类型的事件。 附加事件可用于在非元素类中定义新的 路由事件 ,并在树中的任何元素上引发该事件。 为此,必须将附加事件注册为路由事件,并提供支持附加事件功能的特定 后盾代码 。 由于附加事件注册为路由事件,当在某个元素上触发时,它们通过元素树传播。
在 XAML 语法中,附加事件以 事件名称和所有者 类型的形式 <owner type>.<event name>
指定。 由于事件名称使用其所有者类型的名称 进行限定 ,因此语法允许将事件附加到任何可以实例化的元素。 此语法也适用于附加到事件路由上的任意元素的常规路由事件的处理程序。 您还可以通过在后端代码中调用处理程序应附加到的对象上的 AddHandler 方法,来附加附加事件的处理程序。
WPF 输入系统广泛使用附加事件。 但是,几乎所有附加事件都通过基元素显示为等效的非附加路由事件。 你很少会直接使用或操作绑定事件。 例如,在UIElement上,通过等效UIElement.MouseDown路由事件处理基础附加Mouse.MouseDown事件比在XAML或后台代码中使用附加事件语法更容易。
有关 WPF 中的附加事件的详细信息,请参阅 附加事件概述。
XAML 中的限定事件名称
语法 <owner type>.<event name>
使用其所有者类型的名称限定事件名称。 此语法允许将事件附加到任何元素,而不仅仅是实现该事件的元素作为其类的成员。 在 XAML 中,当为附加事件或在事件路由中任意元素上的路由事件附加处理程序时,可以使用该语法。 假设你想要将事件处理程序附加到父元素,用来处理子元素触发的路由事件。 如果父元素没有路由事件作为成员,则需要使用限定的事件名称语法。 例如:
<StackPanel Name="StackPanel1" Button.Click="Button_Click">
<Button>Click me</Button>
</StackPanel>
在此示例中,添加事件处理程序的父元素侦听器是一个 StackPanel。 但是,Click 路由事件在 ButtonBase 类上实现和引发,并通过继承可用于 Button 类。 尽管Button类“拥有”Click
事件,但路由事件系统允许将任何路由事件的处理程序附加到任何可以为 CLR 事件附加处理程序的UIElement或ContentElement实例侦听器。 这些限定事件属性名称的默认 xmlns
命名空间通常是默认的 WPF xmlns
命名空间,但也可以为自定义路由事件指定前缀命名空间。 有关 xmlns
的详细信息,请参阅 WPF XAML 的 XAML 命名空间和命名空间映射
WPF 输入事件
在 WPF 平台中,路由事件的一个常见应用是处理 输入事件
实现成对的 WPF 输入事件,以便输入设备(如鼠标按钮按下)中的单个用户作将按顺序引发预览和冒泡路由事件。 首先,预览事件被触发并完成其流程。 完成预览事件后,将引发浮泡事件并完成其传播路径。 RaiseEvent实现类中引发冒泡事件的方法调用将重复使用预览事件中的事件数据用于冒泡事件。
标记为已处理的预览输入事件不会为预览路由的其余部分调用任何正常注册的事件处理程序,并且不会引发配对冒泡事件。 对于设计组合控件的设计人员来说,此处理行为非常有用,他们希望基于命中测试的输入事件或基于焦点的输入事件在控件的顶层得到报告。 控件的顶级元素有机会从控件子组件对预览事件进行类处理,以便将其“替换”为顶级控件特定的事件。
若要说明输入事件处理的工作原理,请考虑以下输入事件示例。 在以下树形图中, leaf element #2
是配对 PreviewMouseDown
事件和 MouseDown
配对事件的源:
在叶元素 #2 上执行鼠标向下作后的事件处理顺序为:
-
PreviewMouseDown
根元素上的隧道事件。 -
PreviewMouseDown
中间元素 #1 上的隧道事件。 -
PreviewMouseDown
叶元素 #2 上的 tunneling 事件,它是源元素。 -
MouseDown
叶元素 #2 上的浮点事件,它是源元素。 -
MouseDown
中间元素 #1 上的浮点事件。 -
MouseDown
根元素上的冒泡事件。
路由事件处理程序委托提供了对引发事件的对象和调用处理程序的对象的引用。 最初引发事件的对象由 Source 事件数据中的属性报告。
发送方参数报告调用处理程序的对象。 对于任何给定的路由事件实例,引发事件的对象在事件通过元素树传递时不会改变,但sender
会改变。 在上图的步骤 3 和步骤 4 中,Source
和 sender
是相同的对象。
如果输入事件处理程序完成处理事件所需的特定于应用程序的逻辑,则应将输入事件标记为已处理。 通常,在标记 Handled输入事件后,不会调用沿事件路由进一步的处理程序。 即使事件已标记为已处理,参数设置为 true
的 handledEventsToo
输入事件处理程序仍然会被调用。 有关详细信息,请参阅 预览事件 和 将路由事件标记为已处理,以及类处理。
预览和冒泡事件对的概念,以及共享事件数据和预览事件之后顺序触发冒泡事件的方式,仅适用于某些特定的 WPF 输入事件,并不适用于所有路由事件。 如果您为了解决高级场景而实现自己的输入事件,请考虑遵循 WPF 输入事件对的方法。
如果您正在实现自己的复合控件以响应输入事件,可以考虑使用预览事件来拦截和替换在子组件上产生的输入事件,并用一个完整控件的顶级事件进行替换。 有关详细信息,请参阅将 路由事件标记为已处理,以及类处理。
有关 WPF 输入系统以及如何在典型应用程序方案中的输入和事件交互的详细信息,请参阅 输入概述
事件设置器 和 事件触发器
在标记样式中,可以通过使用 EventSetter 来包含预先声明的 XAML 事件处理语法。 处理 XAML 时,被引用的处理程序会被添加到样式化的对象实例中。 只能为路由事件声明一个 EventSetter
。 在以下示例中,引用的 ApplyButtonStyle
事件处理程序方法在后台代码中实现。
<StackPanel>
<StackPanel.Resources>
<Style TargetType="{x:Type Button}">
<EventSetter Event="Click" Handler="ApplyButtonStyle"/>
</Style>
</StackPanel.Resources>
<Button>Click me</Button>
<Button Click="Button_Click">Click me</Button>
</StackPanel>
节点 Style
可能已经包含了与指定类型控件相关的其他样式信息,将 EventSetter 作为这些样式的一部分加入,甚至在标记层次上都促进了代码重用。 此外,EventSetter
将方法名称抽象化,使其与常规应用程序和页面标记的处理程序区分开来。
WPF 中结合路由事件和动画功能的另一种专门语法是 EventTrigger。 与上述 EventSetter
一样,只能为路由事件声明一个 EventTrigger
。 通常,EventTrigger
声明为样式的一部分,但 EventTrigger
可以在页面级元素上声明为 Triggers 集合的一部分,或者在 ControlTemplate 中声明。 每当一个路由事件到达其路径中声明该事件的EventTrigger
的元素时,EventTrigger
便可以使你指定运行的Storyboard。 通过 EventTrigger
而不是仅仅处理事件并启动现有情节提要的好处在于 EventTrigger
,这可以更好地控制情节提要及其运行时行为。 有关详细信息,请参阅“使用事件触发器在启动后管理情节提要”
有关路由事件的详细信息
在你自己的类中创建自定义路由事件时,可以使用本文中的概念和指南作为起点。 还可以使用专用事件数据类和委托支持自定义事件。 路由事件所有者可以是任何类,但路由事件必须由派生类引发和处理UIElementContentElement,才能有用。 有关自定义事件的详细信息,请参阅 创建自定义路由事件。