将路由事件标记为已处理,并进行类处理

尽管何时将路由事件标记为已处理没有绝对规则,但如果代码以重要方式响应事件,请考虑将事件标记为已处理。 标记为已处理的路由事件会沿着其路由继续传播,但只有配置为响应这些已处理事件的处理器才会被调用。 基本上,将一个路由事件标记为已处理后,其可见性会被限制于事件路由上的侦听器。

路由事件处理程序可以是实例处理程序或类处理程序。 实例处理程序处理对象或 XAML 元素上的路由事件。 类级处理程序在类级别处理路由事件,并且在任何类实例的实例级处理程序响应同一事件之前被调用。 当路由事件被标记为已处理时,它们通常会在类处理程序中标记为此类事件。 本文讨论了将路由事件标记为已处理的优缺点、不同类型的路由事件和路由事件处理程序,以及复合控件中的事件抑制。

先决条件

本文假设您具有有关路由事件的基本知识,并且已经阅读了 路由事件概述。 若要遵循本文中的示例,如果你熟悉可扩展应用程序标记语言(XAML),并且知道如何编写 Windows Presentation Foundation (WPF) 应用程序,则它很有帮助。

何时将路由事件标记为已处理

通常,只有一个处理程序应为每个路由事件提供重要的响应。 避免使用路由事件系统跨多个处理程序提供重要的响应。 构成重大响应的定义是主观的,取决于应用程序。 作为一般指南:

  • 重大响应包括设置焦点、修改公共状态、设置影响视觉表示的属性、引发新事件以及完全处理事件。
  • 微不足道的响应包括修改私有状态,而不影响视觉或编程影响、事件日志记录和检查事件数据,而无需响应事件。

某些 WPF 控件通过将组件级事件标记为已处理来抑制不需要进一步处理的组件级事件。 如果想处理由控件标记为已处理的事件,请参阅绕过控件事件抑制

若要将事件标记为 已处理,请将 Handled 事件数据中的属性值设置为 true。 尽管可以将该值还原到 false,但需要这样做的情况应该是罕见的。

预览和冒泡路由事件对

预览 和冒泡路由事件对是 输入事件 的专用对。 多个输入事件实现 隧道冒泡 路由事件对,例如 PreviewKeyDownKeyDown。 前缀 Preview 表示在预览事件完成后,浮泡事件将启动。 每对预览和冒泡事件都使用相同的事件数据实例。

路由事件处理程序按对应于事件的路由策略的顺序调用:

  1. 预览事件从应用程序根元素向下移动到引发路由事件的元素。 首先调用附加到应用程序根元素的预览事件处理程序,后跟附加到连续嵌套元素的处理程序。
  2. 预览事件完成后,与之配对的冒泡事件会从引发路由事件的元素传输到应用程序的根元素。 附加到引发路由事件的同一元素的冒泡事件处理程序会首先被调用,然后依次调用附加到上层父元素的处理程序。

配对预览事件和冒泡事件是多个 WPF 类内部实现的组成部分,这些类声明并激发它们自己的路由事件。 如果没有该类级内部实现,无论事件命名如何,预览和冒泡路由事件都是完全独立的,也不会共享事件数据。 有关如何在自定义类中实现浮泡或隧道输入路由事件的信息,请参阅 创建自定义路由事件

当预览和冒泡事件对共享同一个事件数据实例时,如果预览路由事件被标记为已处理,则其相对应的冒泡事件也会被处理。 如果冒泡路由事件标记为已处理,则不会影响配对预览事件,因为预览事件已完成。 在将预览和冒泡输入事件对标记为已处理时要小心。 已处理过的预览输入事件不会在隧道路由的其余部分调用任何通常注册的事件处理程序,并且配对的冒泡事件不会被触发。 已处理的冒泡输入事件不会为浮泡路由的其余部分调用任何正常注册的事件处理程序。

实例和类路由事件处理程序

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

  • 静态类事件处理程序,通过调用 RegisterClassHandler 静态类构造函数中的方法进行注册。
  • 重写通过覆盖基类虚拟事件方法注册的事件处理程序。 基类虚拟事件方法主要存在于输入事件中,并且具有以 On<事件名称和>OnPreview<事件名称>开头的名称。

实例事件处理程序

可以通过直接调用 AddHandler 方法将实例处理程序附加到对象或 XAML 元素。 WPF 路由事件实现了公共语言运行时(CLR)事件包装器,该包装器使用 AddHandler 方法来附加事件处理程序。 由于用于附加事件处理程序的 XAML 属性语法会导致对 CLR 事件包装器的调用,即使是在 XAML 中附加处理程序也会解析为 AddHandler 调用。 对于已处理的事件:

  • 未调用使用 XAML 属性语法或通用签名附加的 AddHandler 处理程序。
  • 通过使用AddHandler(RoutedEvent, Delegate, Boolean)重载并将handledEventsToo参数设置为true附加的处理程序将被调用。 当需要响应已处理的事件时,此重载适用于极少数情况。 例如,元素树中的某些元素已将事件标记为已处理,但事件路由中的其他元素需要响应已处理的事件。

下面的 XAML 示例将添加一个名为 componentWrapper 的自定义控件,该控件包含一个名为 componentTextBoxTextBox,并且该控件位于一个名为 outerStackPanelStackPanel 中。 为PreviewKeyDown事件的实例事件处理程序使用XAML属性语法附加到componentWrapper。 因此,实例处理程序只会响应PreviewKeyDowncomponentTextBox引发的未经处理的隧道事件。

<StackPanel Name="outerStackPanel" VerticalAlignment="Center">
    <custom:ComponentWrapper
        x:Name="componentWrapper"
        TextBox.PreviewKeyDown="HandlerInstanceEventInfo"
        HorizontalAlignment="Center">
        <TextBox Name="componentTextBox" Width="200" />
    </custom:ComponentWrapper>
</StackPanel>

构造MainWindow函数将浮升事件的componentWrapper实例处理程序KeyDown附加到使用UIElement.AddHandler(RoutedEvent, Delegate, Boolean)重载,并将handledEventsToo参数设置为 true。 因此,实例事件处理程序将响应未经处理的和已处理的事件。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        // Attach an instance handler on componentWrapper that will be invoked by handled KeyDown events.
        componentWrapper.AddHandler(KeyDownEvent, new RoutedEventHandler(Handler.InstanceEventInfo),
            handledEventsToo: true);
    }

    // The handler attached to componentWrapper in XAML.
    public void HandlerInstanceEventInfo(object sender, KeyEventArgs e) => 
        Handler.InstanceEventInfo(sender, e);
}
Partial Public Class MainWindow
    Inherits Window

    Public Sub New()
        InitializeComponent()

        ' Attach an instance handler on componentWrapper that will be invoked by handled KeyDown events.
        componentWrapper.[AddHandler](KeyDownEvent, New RoutedEventHandler(AddressOf InstanceEventInfo),
                                      handledEventsToo:=True)
    End Sub

    ' The handler attached to componentWrapper in XAML.
    Public Sub HandlerInstanceEventInfo(sender As Object, e As KeyEventArgs)
        InstanceEventInfo(sender, e)
    End Sub

End Class

在下一部分中展示了 ComponentWrapper 的后台代码实现。

静态类事件处理程序

可以通过在类的静态构造函数中调用 RegisterClassHandler 方法来附加静态类事件处理程序。 类层次结构中的每个类都可以为每个路由事件注册其自己的静态类处理程序。 因此,对于事件路由中任何给定节点上的同一事件,可以调用多个静态类处理程序。 构造事件的事件路由时,每个节点的所有静态类处理程序都会添加到事件路由中。 节点上静态类处理程序的调用顺序,首先是最派生的静态类处理程序,然后是每个后续基类的静态类处理程序。

通过使用参数设置为truehandledEventsToo重载来注册静态类事件处理程序可以响应未经处理和已处理的路由事件。

静态类处理程序通常注册以仅响应未经处理的事件。 在这种情况下,如果节点上的派生类处理程序将事件标记为已处理,则不会调用该事件的基类处理程序。 在这种情况下,基类处理程序实际上被派生类处理程序替换。 基类处理程序通常有助于控制设计,例如视觉外观、状态逻辑、输入处理和命令处理,因此请谨慎替换它们。 不将事件标记为已处理的派生类处理程序最终会补充基类处理程序,而不是替换基类处理程序。

下面的代码示例显示了在前面的 XAML 中引用的自定义控件的类层次结构 ComponentWrapper 。 该 ComponentWrapper 类派生自 ComponentWrapperBase 类,后者又派生自类 StackPanel 。 该RegisterClassHandler方法在ComponentWrapperComponentWrapperBase类的静态构造函数中使用,为每个类注册一个静态类事件处理程序。 WPF 事件系统在 ComponentWrapper 静态类处理程序之前 ComponentWrapperBase 调用静态类处理程序。

public class ComponentWrapper : ComponentWrapperBase
{
    static ComponentWrapper()
    {
        // Class event handler implemented in the static constructor.
        EventManager.RegisterClassHandler(typeof(ComponentWrapper), KeyDownEvent, 
            new RoutedEventHandler(Handler.ClassEventInfo_Static));
    }

    // Class event handler that overrides a base class virtual method.
    protected override void OnKeyDown(KeyEventArgs e)
    {
        Handler.ClassEventInfo_Override(this, e);

        // Call the base OnKeyDown implementation on ComponentWrapperBase.
        base.OnKeyDown(e);
    }
}

public class ComponentWrapperBase : StackPanel
{
    // Class event handler implemented in the static constructor.
    static ComponentWrapperBase()
    {
        EventManager.RegisterClassHandler(typeof(ComponentWrapperBase), KeyDownEvent, 
            new RoutedEventHandler(Handler.ClassEventInfoBase_Static));
    }

    // Class event handler that overrides a base class virtual method.
    protected override void OnKeyDown(KeyEventArgs e)
    {
        Handler.ClassEventInfoBase_Override(this, e);

        e.Handled = true;
        Debug.WriteLine("The KeyDown routed event is marked as handled.");

        // Call the base OnKeyDown implementation on StackPanel.
        base.OnKeyDown(e);
    }
}
Public Class ComponentWrapper
    Inherits ComponentWrapperBase

    Shared Sub New()
        ' Class event handler implemented in the static constructor.
        EventManager.RegisterClassHandler(GetType(ComponentWrapper), KeyDownEvent,
                                          New RoutedEventHandler(AddressOf ClassEventInfo_Static))
    End Sub

    ' Class event handler that overrides a base class virtual method.
    Protected Overrides Sub OnKeyDown(e As KeyEventArgs)
        ClassEventInfo_Override(Me, e)

        ' Call the base OnKeyDown implementation on ComponentWrapperBase.
        MyBase.OnKeyDown(e)
    End Sub

End Class

Public Class ComponentWrapperBase
    Inherits StackPanel

    Shared Sub New()
        ' Class event handler implemented in the static constructor.
        EventManager.RegisterClassHandler(GetType(ComponentWrapperBase), KeyDownEvent,
                                          New RoutedEventHandler(AddressOf ClassEventInfoBase_Static))
    End Sub

    ' Class event handler that overrides a base class virtual method.
    Protected Overrides Sub OnKeyDown(e As KeyEventArgs)
        ClassEventInfoBase_Override(Me, e)

        e.Handled = True
        Debug.WriteLine("The KeyDown event is marked as handled.")

        ' Call the base OnKeyDown implementation on StackPanel.
        MyBase.OnKeyDown(e)
    End Sub

End Class

下一节将讨论此代码示例中后台代码实现的重写类事件处理程序。

重写类事件处理程序

某些可视元素的基类为其每个公共路由输入事件公开了空的On<事件名称和>OnPreview<事件名称>虚拟方法。 例如, UIElement 实现 OnKeyDownOnPreviewKeyDown 虚拟事件处理程序,以及许多其他事件处理程序。 可以重写基类虚拟事件处理程序,以实现派生类的重写类事件处理程序。 例如,您可以通过重写OnDragEnter虚拟方法,在任何UIElement派生类中为DragEnter事件添加一个重写类处理程序。 重写基类虚拟方法比在静态构造函数中注册类处理程序更简单。 在重写中,可以引发事件、启动特定于类的逻辑以更改实例上的元素属性、将事件标记为已处理或执行其他事件处理逻辑。

与静态类事件处理程序不同,WPF 事件系统仅调用类层次结构中大多数派生类的重写类事件处理程序。 然后,类层次结构中最派生的类可以使用 关键字调用虚拟方法的基实现。 在大多数情况下,无论是否将事件标记为已处理,都应调用基本实现。 仅当类要求替换基本实现逻辑(如果有)时,才应省略调用基实现。 无论是在重写代码之前还是之后调用基本实现,都取决于实现的性质。

在前面的代码示例中,基类OnKeyDown的虚拟方法在ComponentWrapperComponentWrapperBase类中都被重写。 由于 WPF 事件系统仅调用 ComponentWrapper.OnKeyDown 重写类事件处理程序,该处理程序使用 base.OnKeyDown(e) 调用 ComponentWrapperBase.OnKeyDown 重写类事件处理程序,后者又使用 base.OnKeyDown(e) 调用 StackPanel.OnKeyDown 虚拟方法。 前面的代码示例中的事件顺序为:

  1. 附加到 componentWrapper 的实例处理程序是由 PreviewKeyDown 路由事件触发的。
  2. 附加到 componentWrapper 的静态类处理程序由 KeyDown 路由事件触发。
  3. 附加到 componentWrapperBaseKeyDown 静态类处理程序由路由事件触发。
  4. 附加到 componentWrapper 的重写类处理程序由KeyDown路由事件触发。
  5. 附加到 componentWrapperBase 的重写类处理程序被 KeyDown 路由事件触发。
  6. 路由事件 KeyDown 已被标记为已处理。
  7. 附加到 componentWrapper 的实例处理程序由 KeyDown 路由事件触发。 处理程序已注册,并将 handledEventsToo 参数设置为 true

复合控件中的输入事件抑制

某些复合控件禁止组件级别的 输入事件 ,以便将其替换为包含更多信息或暗示更具体行为的自定义高级事件。 复合控件由多个实际控件或控件基类组成。 经典示例是 Button 控件,它将各种鼠标事件转换为 Click 路由事件。 基类 ButtonButtonBase,它间接派生自 UIElement。 在UIElement级别上提供了控制输入处理所需的大部分事件基础设施。 UIElement 公开多个 Mouse 事件,例如 MouseLeftButtonDownMouseRightButtonDownUIElement 还实现空的虚拟方法 OnMouseLeftButtonDown ,并 OnMouseRightButtonDown 作为预注册的类处理程序。 ButtonBase 重写这些类处理程序,在重写处理程序中将 Handled 属性设置为 true 并引发事件 Click 。 大多数侦听器的最终结果是 MouseLeftButtonDownMouseRightButtonDown 事件被隐藏,而高级 Click 事件是可见的。

绕过输入事件抑制

有时,单个控件中的事件抑制可能会干扰应用程序中的事件处理逻辑。 例如,如果应用程序使用 XAML 属性语法为 XAML 根元素上的事件附加处理程序 MouseLeftButtonDown ,则不会调用该处理程序,因为 Button 控件将事件标记为 MouseLeftButtonDown 已处理。 如果希望为已处理的路由事件调用应用程序的根目录的元素,则可以:

  • 通过调用方法UIElement.AddHandler(RoutedEvent, Delegate, Boolean),并将参数handledEventsToo设置为true,来附加处理程序。 在获取要附加到的元素的对象引用后,此方法需要在代码隐藏中附加事件处理程序。

  • 如果标记为已处理的事件是冒泡输入事件,则附加配对预览事件的处理程序(如果可用)。 例如,如果控件抑制了 MouseLeftButtonDown 事件,您可以改为附加 PreviewMouseLeftButtonDown 事件处理程序。 此方法仅适用于共享事件数据的预览和冒泡输入事件对。 请注意不要将 PreviewMouseLeftButtonDown 标记为已处理,因为这会完全抑制 Click 事件。

有关如何解决输入事件抑制的示例,请参阅 通过控件解决事件抑制

另请参阅