将路由事件标记为“已处理”和“类处理”
路由事件的处理程序可以在事件数据内将事件标记为已处理。 处理事件将有效地缩短路由。 类处理是一个编程概念,受路由事件支持。 类处理程序有机会在类级别使用处理程序处理特定路由事件,在类的任何实例上存在任何实例处理程序之前调用该处理程序。
先决条件
本主题将详细介绍路由事件概述中引入的概念。
何时将事件标记为“已处理”
当在路由事件的事件数据中将 Handled 属性的值设置为 true
时,这就称为“将事件标记为已处理”。 对于应用程序创作者或者响应现有路由事件或实现新路由事件的控件创建者而言,何时应将路由事件标记为已处理,没有绝对规则。 大多数情形下,路由事件的事件数据中携带的“已处理”概念应当用作一种限定协议,用于自己的应用程序响应 WPF API 中公开的各种路由事件时以及用于任意自定义路由事件。 考虑“已处理”问题的另一种方式为:如果代码以重要且相对完整的方式响应路由事件,则通常应将路由事件标记为已处理。 通常,重要响应不应该超过一个。所谓重要响应,是指对于单个任意路由事件,都需要单独的处理程序实现。 如果需要多个响应,则应通过在单个处理程序内链接的应用程序逻辑实现必要的代码,而不是使用路由事件系统进行转发。 是否“重要”这一概念比较主观,视应用程序或代码而定。 作为一般性指导原则,一些“重要响应”示例包括:设置焦点、修改公共状态、设置影响视觉表示形式的属性以及引发其他新事件。 非重要响应的示例包括:修改私有状态(无视觉影响,或编程表示形式)、记录事件或查看事件参数并选择不响应该事件。
路由事件系统行为在使用路由事件的已处理状态方面增强了此“重要响应”模型,因为不会调用 XAML 中添加的处理程序或 AddHandler 的公用签名来响应事件数据已标记为已处理的路由事件。 必须额外添加参数版本为 handledEventsToo
的处理程序 (AddHandler(RoutedEvent, Delegate, Boolean)) 才能处理由事件路由中的早期参与者标记为已处理的路由事件。
在某些情况下,控件本身会将某些路由事件标记为已处理。 已处理的路由事件表示 WPF 控件作者做出的以下决定:响应路由事件的控件操作是重要的,或者作为控件实现的一部分已完成,并且事件无需进一步处理。 通常,通过为事件添加一个类处理程序,或重写存在于基类上的其中一个类处理程序虚方法,可以完成此操作。 必要时仍然可以应对此事件处理;请参阅本主题后面部分的通过控件解决事件禁止问题。
“预览”(隧道)事件与浮升事件和事件处理
预览路由事件是指遵循元素树的隧道路由的事件。 命名约定中的“Preview”是指输入事件的一般原则,即预览(隧道)路由事件是在对等的冒泡路由事件之前引发的。 另外,具有隧道和冒泡对的输入路由事件具有截然不同的处理逻辑。 如果隧道/预览路由事件标记为已由事件侦听器处理,则冒泡路由事件将标记为已处理,然而此时冒泡路由事件的任何侦听器尚未接收到该事件。 隧道路由事件和冒泡路由事件在技术层面上是单独的事件,但是它们有意共享相同的事件数据实例以实现此行为。
任意给定的 WPF 类引发其声明的路由事件的方式的内部实现可在隧道路由事件与浮升路由事件之间建立连接,对于成对的输入路由事件也是如此。 但是除非该类级实现存在,否则共享命名方案的隧道路由事件与冒泡路由事件之间将没有连接:没有上述实现,它们将是两个完全独立的路由事件,不会顺次引发,也不会共享事件数据。
有关如何在自定义类中实现隧道/冒泡输入路由事件对的详细信息,请参阅创建自定义路由事件。
类处理程序和实例处理程序
路由事件会考虑事件的两种不同类型的侦听器:类侦听器和实例侦听器。 类侦听器已存在,因为类型已在其静态构造函数中调用特定的 EventManager,即 RegisterClassHandler,或者已从元素基类重写类处理程序虚方法。 实例侦听器是特定的类实例/元素,其中该路由事件已通过调用 AddHandler 附加了一个或多个处理程序。 现有 WPF 路由事件调用 AddHandler 作为事件的公共语言运行时 (CLR) 事件包装器 add{} 和 remove{} 实现的一部分,这也是通过属性语法附加事件处理程序的简单 XAML 机制的启用方式。 因此,即使是简单的 XAML 用法最终也等同于 AddHandler 调用。
将检查可视化树内的元素是否有注册的处理程序实现。 可能会在整个路由中以该路由事件的路由策略类型所固有的顺序调用处理程序。 例如,冒泡路由事件将首先调用那些附加到引发该路由事件的同一元素的处理程序。 然后,路由事件“冒泡”到下一父元素,以此类推,直到到达应用程序根元素。
从冒泡路由中的根元素的角度看,如果类处理或者更靠近路由事件源的任意元素调用那些将事件参数标记为正在处理的处理程序,那么就不会调用根元素上的处理程序,这样在到达该根元素之前的事件路由会大大缩短。 不过,路由并未完全停止,因为可以使用仍应调用处理程序的特殊条件来添加处理程序,即使类处理程序或实例处理程序已将路由事件标记为已处理也是如此。 本主题后面部分的即使在事件标记为已处理时也要添加引发的实例处理程序对此进行了说明。
在比事件路由更深的级别上,还可能有多个类处理程序处理任意给定的类实例。 这是因为,路由事件的类处理模型使得类层次结构中的所有可能的类都可以针对每个路由事件注册自己的类处理程序。 每个类处理程序都会添加到一个内部存储,当构造应用程序的事件路由时,所有类处理程序都会添加到该事件路由中。 类处理程序按以下顺序添加到路由:最先调用派生程度最高的类处理程序,然后调用每个后续基类中的类处理程序。 通常,不会注册类处理程序以便它们也响应标记为已处理的路由事件。 因此,此类处理机制可提供以下两个选项之一:
派生类可以通过添加一个不将路由事件标记为已处理的处理程序,来补充从基类继承的类处理,因为有时会在派生类处理程序之后调用基类处理程序。
派生类可以通过添加一个将路由事件标记为已处理的类处理程序,来替换基类中的类处理。 应慎用此方法,因为它可能会更改视觉外观、状态逻辑、输入处理和命令处理等方面原本的基控件设计。
按控件基类进行的路由事件类处理
在事件路由中的每个给定元素节点上,类侦听器都有机会在元素的任意实例侦听器之前响应路由事件。 为此,类处理程序有时用于禁止某个特定控件类实现不希望继续传播的路由事件,或者用于提供类的一项功能,即对该路由事件进行特殊处理。 例如,类可能引发特定于其自身的事件,其中包含有关某个用户输入条件在该特定类的上下文中所代表的意义的更多具体信息。 然后,此类实现可能会将较为常规的路由事件标记为已处理。 一般会添加类处理程序,这样在共享事件数据标记为已处理后,就不会对路由事件调用该处理程序,但是对于非典型情况,还存在一个 RegisterClassHandler(Type, RoutedEvent, Delegate, Boolean) 签名,即使在将路由事件标记为已处理的情况下,该签名也会注册类处理程序。
类处理程序虚方法
某些元素(特别是 UIElement 等基元素)会公开与其公共路由事件列表对应的空的“On*Event”和“OnPreview*Event”虚方法。 可以重写这些虚方法以便为该路由事件实现类处理程序。 如前所述,基元素类使用 RegisterClassHandler(Type, RoutedEvent, Delegate, Boolean) 将这些虚方法注册为每个此类路由事件的类处理程序。 因为无需在静态构造函数中针对每个类型进行特殊初始化,所以 On*Event 虚方法使得为相关路由事件实现类处理变得简单得多。 例如,可以通过替代 OnDragEnter 虚方法来在任何 UIElement 派生类中添加对 DragEnter 事件的类处理。 在重写内,可以处理路由事件、引发其他事件、启动可能更改实例的元素属性的特定于类的逻辑或上述操作的任意组合。 通常情况下,在此类重写中,即使将事件标记为已处理,也应调用基实现。 强烈建议调用基实现,因为虚方法是在基类上。 从每个虚方法调用基实现的标准受保护虚拟模式实质上会替换和比较路由事件类处理所固有的类似机制,因此可以在任意给定实例上调用类层次结构中所有类的类处理程序,并从派生程度最高的类的处理程序开始,一直继续到基类处理程序。 如果类明确要求更改基类处理逻辑,则应仅忽略基实现调用。 在重写代码之前还是之后调用基实现取决于实现的性质。
输入事件类处理
类处理程序虚方法均已注册,只有当存在任何共享事件数据尚未标记为已处理的情况时才调用这些方法。 另外,仅仅对于输入事件而言,隧道和冒泡版本通常是顺次引发的,并且共享事件数据。 对于一对给定的输入事件的类处理程序(其中一个是隧道版本,另一个是冒泡版本),当你可能不希望立即将事件标记为已处理时也需要此项。 如果实现隧道类处理虚方法以便将事件标记为已处理,这将会阻止调用冒泡类处理程序(并阻止调用隧道或冒泡事件的任意正常注册的实例处理程序)。
节点上的类处理一经完成,就会考虑实例侦听器。
即使在事件标记为已处理时也要添加引发的实例处理程序
AddHandler 方法提供一个允许添加处理程序的特定重载,只要事件到达路由中的处理元素,事件系统就会调用该处理程序,即使其他处理程序通过调整事件数据将该事件标记为已处理也是如此。 但一般不这样操作。 通常情况下,不论事件是在元素树中的什么位置处理,即使需要多个最终结果,都可以通过编写处理程序来调整可能受事件影响的应用程序代码的各个方面。 另外,通常情况下,需要响应该事件的实际只有一个元素,并且已发生了适当的应用程序逻辑。 但是 handledEventsToo
重载适用于某些例外情况,例如元素数或控件复合中的某些其他元素已将事件标记为已处理,但是元素数中较高或较低的其他元素(视路由而定)仍希望调用自己的处理程序。
何时将已处理事件标记为未处理
通常,标记为已处理的路由事件不应再标记为未处理(将 Handled 重设回 false
),即便处理 handledEventsToo
的处理程序也不应这样做。 不过,某些输入事件具有高级别和低级别两种事件表示形式,当在树中的一个位置看到高级别事件,在另一个位置看到低级别事件时,这两种表示形式可以重叠。 例如,假设这样一种情况:一个子元素侦听高级别键事件(如 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
签名有意添加处理程序。 这种方法的局限性在于用于附加事件处理程序的这种技术只可能来自代码,而不能来自标记。 通过 Extensible Application Markup Language (XAML) 将事件处理程序名称指定为事件属性值的简单语法不会启用该行为。
第二种技术仅适用于输入事件,其中路由事件的隧道版本和冒泡版本是配对的。 对于这些路由事件,可以改为将处理程序添加到预览/隧道对等路由事件。 假如在应用程序的元素树中的某个上级元素级别附加了 Preview 处理程序,该路由事件将从根开始在路由中传递,所以按钮类处理代码不会截获到该事件。 如果使用此方法,则将任意 Preview 事件标记为已处理时一定要谨慎。 对于在根元素处对 PreviewMouseLeftButtonDown 进行处理这一示例,如果在处理程序实现中将事件标记为 Handled,实际上会抑制 Click 事件。 这通常不是希望的行为。