Windows Presentation Foundation (WPF) 应用程序开发人员和组件作者可以使用路由事件通过元素树传播事件,并在树中的多个侦听器上调用事件处理程序。 在公共语言运行时 (CLR) 事件中找不到这些功能。 多个 WPF 事件是路由事件,例如 ButtonBase.Click 。 本文讨论路由事件的基本概念,并提供有关何时以及如何响应路由事件的指导。

面向 .NET 7 和 .NET 6 的桌面指南文档正在撰写中。

本文假定基本了解公共语言运行时 (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 事件源。 单击其中一个按钮时,它会引发 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 注册它的类的成员。 该类称为事件“owner”类。 通常,路由事件实现同名 CLR 事件“包装器”。 CLR 事件包装器包含 add remove 访问器,用于通过特定于语言的事件语法在 XAML 和代码隐藏中启用附加处理程序。 add remove 访问器重写其 CLR 实现并调用路由事件 AddHandler RemoveHandler 方法。 路由事件支持和连接机制在概念上类似于依赖属性是由 DependencyProperty 类支持并在 WPF 属性系统中注册的 CLR 属性。

    以下示例注册 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 EventTrigger 使用。

    为何使用路由事件?

    作为应用程序开发人员,你并不总是需要知道或关心你正在处理的事件是作为路由事件实现的。 路由事件具有特殊行为,但如果在引发路由事件的元素上处理事件,该行为基本上不可见。 但是,如果要将事件处理程序附加到父元素以处理子元素引发的事件(例如在复合控件中),路由事件是相关的。

    路由事件侦听器不需要它们处理的路由事件成为其类的成员。 任何 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>

    代码隐藏中事件处理程序方法的签名必须与路由事件的委托类型匹配。 sender 事件的委托 Click 参数 RoutedEventHandler 指定事件处理程序附加到的元素。 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 关键字 (keyword) 添加处理程序作为处理程序声明的一部分。 有关详细信息,请参阅 Visual Basic 和 WPF 事件处理

    已处理的概念

    所有路由事件共享事件数据的通用基类,即 类 RoutedEventArgs 。 类 RoutedEventArgs 定义布尔 Handled 属性。 属性的目的是 Handled 让事件路由上的任何事件处理程序将路由事件标记为 已处理 。 若要将事件标记为已处理,请在事件处理程序代码中将 的值 Handled true 设置为 。

    的值 Handled 影响路由事件在沿事件路由传输时处理的方式。 如果 Handled 位于 true 路由事件的共享事件数据中,则通常不会为该特定事件实例调用附加到事件路由中其他元素的处理程序。 对于最常见的处理程序方案,将事件标记为已处理可有效地阻止事件路由上的后续处理程序(无论是实例处理程序还是类处理程序)响应该特定事件实例。 但是,在极少数情况下,需要事件处理程序来响应已标记为已处理的路由事件,可以:

  • 使用 AddHandler(RoutedEvent, Delegate, Boolean) 重载在代码隐藏中附加处理程序,并将 handledEventsToo 参数设置为 true

  • HandledEventsToo 中的 EventSetter 属性设置为 true

    的概念 Handled 可能会影响你设计应用程序和编写事件处理程序的方式。 可以概念化 Handled 为处理路由事件的简单协议。 此协议的使用方式由你决定,但参数的预期用法 Handled 是:

  • 如果路由事件标记为已处理,则无需由路由中的其他元素再次处理该事件。

  • 如果路由事件未标记为已处理,则事件路由中较早的侦听器没有事件的处理程序,或者注册的处理程序都没有响应事件的方式将事件标记为已处理。 当前侦听器上的处理程序有三种可能的操作过程:

  • 完全不执行任何操作。 该事件保持未处理状态,并路由到树中的下一个侦听器。

  • 运行代码以响应事件,但不能在将事件标记为“已处理”的合理范围内运行代码。 该事件保持未处理状态,并路由到树中的下一个侦听器。

  • 运行代码以响应事件,以合理地将事件标记为已处理。 在事件数据中将事件标记为已处理。 事件仍路由到树中的下一个侦听器,但大多数侦听器不会调用进一步的处理程序。 例外是具有专门注册的处理程序的侦听器, true 这些 handledEventsToo 处理程序设置为 。

    有关处理路由事件的详细信息,请参阅 将路由事件标记为已处理和类处理

    尽管仅处理引发该事件的对象上的浮升路由事件的开发人员可能并不关心其他侦听器,但最好还是将事件标记为已处理。 如果事件路由中进一步的元素具有同一路由事件的处理程序,则这样做可以防止意外的副作用。

    类处理程序

    路由事件处理程序可以是实例处理程序或类处理程序。 给定类的类处理程序会在任何实例处理程序对该类的任何实例响应相同事件之前进行调用。 由于此行为,当路由事件标记为已处理时,它们通常会在类处理程序中标记为这样。 有两种类型的类处理程序:

  • 静态类事件处理程序 ,通过在静态类构造函数中调用 RegisterClassHandler 方法进行注册。
  • 重写类事件处理程序 ,通过重写基类虚拟事件方法进行注册。 基类虚拟事件方法的存在主要是用于输入事件,名称以 On<事件名称> 和 OnPreview<事件名称> 开头。
  • 有些 WPF 控件对某些路由事件具有固有的类处理。 类处理可能会提供路由事件从未引发过的外观,但实际上它被标记为由类处理程序处理。 如果需要事件处理程序来响应已处理的事件,可以将处理程序 handledEventsToo 注册为 true 设置为 。 有关详细信息,请参阅将 路由事件标记为已处理和类处理

    WPF 中的附加事件

    XAML 语言还定义了一个名为附加事件的特殊类型的事件。 附加事件可用于在非元素类中定义新的 路由事件 ,并在树中的任何元素上引发该事件。 为此,必须将附加事件注册为路由事件,并提供支持附加事件功能的特定 支持代码 。 由于附加事件注册为路由事件,因此在元素上引发时,它们会通过元素树传播。

    在 XAML 语法中,附加事件按其事件 名称和 所有者类型指定,格式为 <owner type>.<event name> 。 由于事件名称使用其所有者类型的名称 进行限定 ,因此语法允许将事件附加到可以实例化的任何元素。 此语法也适用于附加到沿事件路由的任意元素的常规路由事件的处理程序。 还可以通过在处理程序应附加到的对象上调用 AddHandler 方法,在代码隐藏中附加事件的处理程序。

    WPF 输入系统广泛使用附加事件。 但是,几乎所有附加事件都通过基本元素显示为等效的非附加路由事件。 你很少会直接使用或处理附加事件。 例如,与在 XAML 或代码隐藏中使用附加事件语法相比,通过等效 UIElement.MouseDown 路由事件处理 UIElement 上的基础附加 Mouse.MouseDown 事件更为容易。

    有关 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 ,但路由事件系统允许将任何路由事件的处理程序附加到任何 UIElement ContentElement 实例侦听器,这些侦听器本来可以具有 CLR 事件的处理程序。 这些限定的事件属性名称的默认 xmlns 命名空间通常是默认的 WPF xmlns 命名空间,但也可以为自定义路由事件指定带前缀的命名空间。 有关 的详细信息 xmlns ,请参阅 WPF XAML 的 XAML 命名空间和命名空间映射

    WPF 输入事件

    WPF 平台中路由事件的一个频繁应用是 输入事件 。 按照约定,遵循隧道路由的 WPF 路由事件的名称以“预览”为前缀。 预览前缀表示预览事件在配对的浮升事件开始之前完成。 输入事件通常成对出现,一个是预览事件,另一个是冒泡路由事件。 例如, PreviewKeyDown KeyDown 。 事件对共享事件数据的同一实例,对于 PreviewKeyDown KeyDown 的类型为 KeyEventArgs 。 有时,输入事件只有浮升版本,或者只有直接路由版本。 在 API 文档中,路由事件主题交叉引用路由事件对,并阐明每个路由事件的路由策略。

    实现成对的 WPF 输入事件,以便来自输入设备的单个用户操作(如按鼠标按钮)将按顺序引发预览和冒泡路由事件。 首先,引发预览事件并完成其路由。 完成预览事件后,将引发浮升事件并完成其路由。 RaiseEvent 在引发浮升事件的实现类中调用的方法将重复用于冒泡事件的预览事件中的事件数据。

    标记为已处理的预览输入事件不会为预览路由的其余部分调用任何正常注册的事件处理程序,并且不会引发配对的浮升事件。 此处理行为对于希望在其控件的顶层报告基于命中测试的输入事件或基于焦点的输入事件的组合控件设计器非常有用。 控件的顶级元素有机会对控件子组件中的预览事件进行类处理,以便用特定于控件的顶级事件“替换”它们。

    为了说明输入事件处理的工作原理,请考虑以下输入事件示例。 在以下树图中, leaf element #2 是 和 MouseDown 配对事件的源 PreviewMouseDown

    在叶元素 #2 上执行鼠标按下操作后的事件处理顺序为:

  • PreviewMouseDown 根元素上的隧道事件。
  • PreviewMouseDown 中间元素 #1 上的隧道事件。
  • PreviewMouseDown 叶元素 #2(源元素)上的隧道事件。
  • MouseDown 叶元素 #2(源元素)上的浮升事件。
  • MouseDown 中间元素 #1 上的浮升事件。
  • MouseDown 根元素上的浮升事件。
  • 路由事件处理程序委托提供对引发事件的对象和调用处理程序的对象的引用。 最初引发事件的对象由 Source 事件数据中的 属性报告。 调用处理程序的对象由 发送方 参数报告。 对于任何给定的路由事件实例,引发该事件的对象不会随着事件穿过元素树而更改,但 sender 会更改。 在上图的步骤 3 和 4 中, Source sender 是同一对象。

    如果输入事件处理程序完成了处理事件所需的特定于应用程序的逻辑,则应将输入事件标记为已处理。 通常,一旦输入事件被标记为 Handled ,就不会调用沿事件路由进一步的处理程序。 但是,即使事件标记为已处理,也会调用向 设置为 true 的参数注册 handledEventsToo 的输入事件处理程序。 有关详细信息,请参阅 预览事件 将路由事件标记为已处理和类处理

    预览和冒泡事件对的概念(共享事件数据以及依次引发预览事件和冒泡事件)仅适用于某些 WPF 输入事件,不适用于所有路由事件。 如果实现自己的输入事件来解决高级方案,请考虑遵循 WPF 输入事件对方法。

    如果要实现自己的响应输入事件的复合控件,请考虑使用预览事件来抑制子组件上引发的输入事件,并将其替换为表示完整控件的顶级事件。 有关详细信息,请参阅 将路由事件标记为已处理和类处理

    有关 WPF 输入系统以及典型应用程序方案中输入和事件如何交互的详细信息,请参阅 输入概述

    EventSetter 和 EventTrigger

    在标记样式中,可以使用 来包括预先声明的 XAML 事件处理语法 EventSetter 。 处理 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 可对情节提要及其运行时行为提供更好的控制。 有关详细信息,请参阅 启动情节提要后使用事件触发器控制情节提要

    有关路由事件的详细信息

    在你自己的类中创建自定义路由事件时,可以使用本文中的概念和指南作为起点。 还可以使用专用事件数据类和委托来支持自定义事件。 路由事件所有者可以是任何类,但路由事件必须由或 ContentElement 派生类引发和处理 UIElement 才能发挥作用。 有关自定义事件的详细信息,请参阅 创建自定义路由事件

  • EventManager
  • RoutedEvent
  • RoutedEventArgs
  • 将路由事件标记为“已处理”和“类处理”
  • 自定义依赖属性
  • WPF 中的树
  • 弱事件模式
  •