将路由事件标记为“已处理”和“类级处理”
处理程序可以在事件数据中将路由事件标记为已处理。 处理事件将有效地缩短路线。 类处理是路由事件支持的编程概念。 类处理程序有机会在类级别处理特定的路由事件,并在类的任何实例处理程序被调用之前,先调用一个处理程序。
先决条件
本主题详细说明了 路由事件概述中介绍的概念。
何时将事件标记为已处理
将 Handled 属性的值设置为在路由事件的事件数据中 true
时,这称为“标记已处理的事件”。 在应该何时将路由事件标记为已处理方面,无论是作为应用程序作者,还是作为响应现有路由事件或实现新路由事件的控件作者,都没有绝对的规则。 通常情况下,“已处理”概念在路由事件的事件数据中携带,可作为一种有限的协议,用于你自己的应用程序对 WPF API 中公开的各种路由事件及任何自定义路由事件的响应。 另一种考虑“已处理”问题的方法是,当代码以显著且相对完整的方式响应路由事件时,通常应标记该事件为已处理。 通常,对于任何单个路由事件事件,不应有多个重要响应需要单独的处理程序实现。 如果需要更多响应,则应通过在单个处理程序中链接的应用程序逻辑(而不是使用路由事件系统进行转发)来实现必要的代码。 什么是“重要”的概念也是主观的,取决于应用程序或代码。 作为一般指导,一些“重大响应”示例包括:设置焦点、修改公共状态、设置影响视觉表示形式的属性,以及引发其他新事件。 非重要响应的示例包括:修改私有状态(无视觉效果或编程表示)、事件日志记录或查看事件参数,然后选择不响应事件。
路由事件系统的行为强化了这一“重大响应”模型,以使用路由事件的处理状态。因为在 XAML 中添加的处理程序或使用常见的 AddHandler 签名的处理程序不会在事件数据已经标记为已处理的路由事件中被调用。 必须额外付出努力,使用 handledEventsToo
参数版本(AddHandler(RoutedEvent, Delegate, Boolean))添加处理程序,以处理由事件路由中较早参与者标记为已处理的路由事件。
在某些情况下,控件本身将某些路由事件标记为已处理。 已处理的路由事件表示 WPF 控件作者的决策,即控件响应路由事件的操作在控件实现过程中非常重要或完整,并且事件无需进一步处理。 通常,这是通过为事件添加类处理程序来完成的,或通过重写基类上存在的类处理程序虚拟之一来完成。 如有必要,仍可绕过此事件处理,请参阅本主题后面的章节 控件如何绕过事件抑制。
“预览”(隧道)事件与冒泡事件和事件处理
预览路由事件是遵循通过元素树的隧道路由的事件。 命名约定中的“预览”指的是对于输入事件的一般原则,即预览(隧道)路由事件会在等效的浮泡路由事件之前引发。 此外,具有隧道和冒泡对的输入路由事件具有不同的处理逻辑。 如果隧道/预览路由事件被事件侦听器标记为已处理,那么即使在冒泡路由事件的任何侦听器接收到之前,冒泡路由事件也将被标记为已处理。 隧道式和冒泡式路由事件在技术上是独立的事件,但它们故意共享相同的事件数据实例以实现此行为。
隧道路由事件与冒泡路由事件之间的连接是通过内部实现的,即任何一个 WPF 类如何引发其自身声明的路由事件,这同样适用于配对的输入路由事件。 但是,除非存在此类级实现,否则隧道路由事件与共享命名方案的浮泡路由事件之间没有连接:如果没有此类实现,它们将是两个完全独立的路由事件,也不会按顺序引发或共享事件数据。
有关如何在自定义类中实现隧道/气泡输入路由事件对的详细信息,请参阅 创建自定义路由事件。
类处理程序和实例处理程序
路由事件考虑到事件的两种不同类型的侦听器:类侦听器和实例侦听器。 类侦听器之所以存在,是因为类型在其静态构造函数中调用了特定的 EventManager API 和RegisterClassHandler,或者重写了来自元素基类的类处理程序虚方法。 实例侦听器是特定的类实例/元素,其中一个或多个处理程序已通过调用 AddHandler为该路由事件附加。 作为公共语言运行时(CLR)事件包装器的一部分,现有 WPF 路由事件调用 AddHandler 添加{} 并删除事件的{} 实现,这也是通过属性语法附加事件处理程序的简单 XAML 机制的方式。 因此,即使是简单的 XAML 用法最终也等同于 AddHandler 调用。
检查可视化树中的元素是否具有已注册的处理程序实现。 处理程序可能会在整个路由中调用,其顺序是该路由事件的路由策略类型固有的。 例如,冒泡路由事件将首先调用那些附加在引发该事件的同一元素上的处理程序。 然后,路由事件“冒泡”到下一个父元素,依此类推,直到到达应用程序的根元素。
从冒泡路由中的根元素的角度来看,如果类的处理或任何更接近路由事件源的元素调用了处理程序,并将事件参数标记为已处理,则根元素上的处理程序不会被调用,事件路由将在到达该根元素之前被有效缩短。 但是,路由不会完全停止,因为处理程序可以使用一个特殊条件来添加,即使类处理程序或实例处理程序已将路由事件标记为已处理也是如此。 本主题后面的 “添加实例处理程序,即使事件被标记为已处理”中对此进行了说明。
在比事件路由更深的级别上,也可能有多个类处理程序对类的任何给定实例执行操作。 这是因为路由事件的类处理模型允许类层次结构中的所有可能类为每个路由事件注册其自己的类处理程序。 每个类处理程序都会添加到内部存储,当构造应用程序的事件路由时,类处理程序都将添加到事件路由中。 类处理程序将添加到路由中,以便先调用最派生的类处理程序,然后调用每个连续基类中的类处理程序。 通常,不会注册类处理程序,以便它们也响应已标记为已处理路由的事件。 因此,这种类处理机制提供以下两种选择之一:
派生类可以通过添加一个不标记路由事件已处理的处理程序,来增强从基类继承过来的类处理,因为基类的处理程序会在派生类处理程序之后被调用。
派生类可以通过添加一个类处理程序来替换基类的类处理,该类处理程序标记已处理的路由事件。 应谨慎使用此方法,因为它可能会更改视觉外观、状态逻辑、输入处理和命令处理等领域的预期基本控件设计。
控制基类对路由事件的类处理方式
在事件路由中的每个给定元素节点上,类侦听器可以在元素上任何实例侦听器响应之前处理路由事件。 因此,类处理器有时用于抑制某个控件类实现不希望进一步传播的路由事件,或者为该类特有的路由事件提供特殊处理。 例如,类可能会引发其自己的特定于类的事件,该事件包含有关特定类上下文中某些用户输入条件的含义的更具体内容。 然后,类实现可能会将更常规的路由事件标记为已处理。 通常情况下,添加类处理程序的目的是避免它们在共享事件数据已标记为处理的路由事件中被调用。但在非常规情况下,也存在一个 RegisterClassHandler(Type, RoutedEvent, Delegate, Boolean) 标识,这可以使类处理程序在路由事件被标记为已处理时仍然被调用。
类处理程序虚拟函数
某些元素,特别是基元素(如 UIElement),会公开空的“On*Event”和“OnPreview*Event”虚拟方法,这些方法对应于它们的公共路由事件列表。 可以重写这些虚拟方法,以实现该路由事件的类处理程序。 基元素类使用前面所述的 RegisterClassHandler(Type, RoutedEvent, Delegate, Boolean) 将这些虚拟方法注册为每个此类路由事件的类处理程序。 On*Event 虚拟方法使得为相关路由事件实现类处理要简单得多,而无需对每种类型在静态构造函数中进行特殊初始化。 例如,可以在任何 UIElement 派生类中,通过重写 OnDragEnter 虚拟方法,为 DragEnter 事件添加类处理。 在重写中,您可以处理路由事件、触发其他事件、启动类特定的逻辑(这些逻辑可能会更改实例上的元素属性),或执行这些操作的任意组合。 通常,即使标记事件为已处理,也应在此类重写中调用基类实现。 强烈建议调用基实现,因为虚拟方法位于基类上。 调用每个虚拟基实现的标准受保护虚拟模式,本质上取代并平行于一种类似的机制,该机制原生于路由事件类的处理,其中,类层次结构中所有类的类处理程序都会在给定实例上被调用,从最派生的类的处理程序开始,依次调用至基类处理程序。 仅当类有更改基类处理逻辑的故意要求时,才应省略基实现调用。 无论是在重写代码之前还是之后调用基本实现都将取决于实现的性质。
输入事件类处理
类处理程序的虚拟方法是这样注册的:仅在共享事件数据未被标记为已处理的情况下,才会调用它们。 此外,特定于输入事件,隧道版本和冒泡版本通常按顺序触发,并共享事件数据。 这意味着,对于输入事件的类处理程序对,其中一个处理程序是隧道版本,另一个是冒泡版本,你可能不希望事件被立即标记为已处理。 如果实现隧道类处理虚拟方法来标记所处理的事件,这将阻止调用冒泡类处理程序(以及防止调用隧道或冒泡事件的任何正常注册的实例处理程序)。
节点上的类别处理完成后,实例侦听器会被考虑。
添加实例处理程序,即使事件已被标记为处理,它们仍然会被触发
AddHandler 方法提供了一个特定的重载,允许你添加处理程序,这些处理程序在事件到达路由中的处理元素时会被事件系统调用,即使其他处理程序已修改事件数据将其标记为已处理。 这通常不会完成。 通常,可以编写处理程序来调整所有可能受事件影响的应用程序代码区域,而不论其在元素树中处理的位置,甚至可以实现多个最终结果。 此外,通常只有一个元素需要响应该事件,并且已经发生了相应的应用程序逻辑。 然而,对于某些特殊情况,“handledEventsToo
”重载是可用的:元素树或控件组合中的某些其他元素已将事件标记为已处理,但元素树中其他层级的元素(根据路由),仍希望调用它们自己的处理程序。
何时将处理的事件标记为未处理
通常,被标记为已处理的路由事件即使在处理 handledEventsToo
的处理程序中,也不应将其标记为未处理(Handled 设置为 false
)。 某些输入事件具有高层和低层的事件表示形式,当高层事件和低层事件在树中的不同位置同时出现时,这些表示形式可能会重叠。 例如,假设子元素侦听高级键事件(例如 TextInput),而父元素侦听低级别事件(如 KeyDown)。 如果父元素处理了低级别事件,即使在按理应当首先有机会处理事件的子元素中,也可以阻止高级别事件的发生。
在这些情况下,可能需要将事件处理器添加到低级别事件的父级元素和子级元素。 子元素处理程序实现可以将低级别事件标记为已处理,但父元素处理程序实现将再次将其设置为未经处理,以便树上进一步的元素(以及高级事件)可以有机会进行响应。 这种情况应该相当罕见。
故意抑制用于控制合成的输入事件
使用路由事件的类处理的主要方案是用于输入事件和复合控件。 复合控件的定义由多个实际控件或控件基类组成。 控件的作者通常希望将每个子组件可能引发的所有可能输入事件合并,以便将整个控件报告为单一事件源。 在某些情况下,控件作者可能希望完全禁止组件中的事件,或者替换包含更多信息或暗示更具体行为的组件定义事件。 对于任何组件作者来说,一个经典的示例是,Windows Presentation Foundation(WPF)Button如何处理鼠标事件,这些事件最终解析为所有按钮共有的直观事件:Click事件。
Button 基类(ButtonBase)派生自 Control,后者又派生自 FrameworkElement 和 UIElement,控制输入处理所需的大部分事件基础结构在 UIElement 级别可用。 具体而言,UIElement 处理一般 Mouse 事件,这些事件处理其边界内鼠标光标的命中测试,并为最常见的按钮操作(如 MouseLeftButtonDown)提供不同的事件。 UIElement 还提供了一个空的虚拟 OnMouseLeftButtonDown 作为预注册的 MouseLeftButtonDown类处理程序,并 ButtonBase 重写它。 同样,ButtonBase 对 MouseLeftButtonUp使用类处理程序。 在覆盖方法中传递事件数据时,实现会通过将 Handled 设置为 true
来标记 RoutedEventArgs 实例为已处理,并且相同的事件数据在路由的其余部分继续传递到其他类处理程序以及实例处理程序或事件设置器。 此外,OnMouseLeftButtonUp 替代将接下来引发 Click 事件。 对于大多数侦听器来说,最终结果是 MouseLeftButtonDown 和 MouseLeftButtonUp 事件“消失”,而被 Click替代,这是一个更具意义的事件,因为已知该事件源自一个真实的按钮,而不是按钮的某些复合部分或者完全来自其他元素。
通过控件绕过事件抑制
有时,单个控件中的此事件抑制行为可能会干扰应用程序事件处理逻辑的一些更常规的意图。 例如,如果出于某种原因,应用程序具有位于应用程序根元素 MouseLeftButtonDown 的处理程序,你会注意到,任何鼠标单击按钮都不会在根级别调用 MouseLeftButtonDown 或 MouseLeftButtonUp 处理程序。 事件本身实际上确实出现了气泡(同样,事件路由不会真正结束,但路由的事件系统会在被标记处理后更改其处理程序调用行为)。 当路由事件到达按钮时,ButtonBase 类处理将 MouseLeftButtonDown 标记为已处理,因为它希望用更有意义的 Click 事件替换该事件。 因此,不会调用路由上游的任何标准 MouseLeftButtonDown 处理程序。 有两种方法,可以确保在这种情况下调用你的事件处理器。
第一个技巧是故意通过使用 AddHandler(RoutedEvent, Delegate, Boolean)的 handledEventsToo
签名来添加处理程序。 此方法的一个限制是,用于附加事件处理程序的技术只能从代码而不是标记中获取。 通过可扩展应用程序标记语言(XAML)将事件处理程序名称指定为事件属性值的简单语法不会启用该行为。
第二种技术仅适用于输入事件,其中已配对路由事件的隧道和冒泡版本。 对于这些路由事件,可以改为将处理程序添加到预览/隧道等效路由事件。 该路由事件将通过从根开始的路由进行隧道,因此按钮类处理代码不会截获它,假定你在应用程序的元素树中的某个上级元素级别附加了预览处理程序。 如果使用此方法,请谨慎将任何预览事件标记为已处理。 对于在根元素处处理 PreviewMouseLeftButtonDown 的示例,如果在处理程序实现中将事件标记为 Handled,那么实际上会压制 Click 事件。 这通常是不理想的行为。