将路由事件标记为已处理和类处理 (WPF .NET)

尽管对于何时将路由事件标记为已处理没有绝对规则,但如果代码以重要方式响应事件,请考虑将事件标记为已处理。 标记为已处理的路由事件会继续进行其路由,但只会调用配置为响应已处理事件的处理程序。 基本上,将路由事件标记为已处理会限制其在事件路由上对侦听器的可见性。

路由事件处理程序可以是实例处理程序或类处理程序。 实例处理程序处理对象或 XAML 元素上的路由事件。 类处理程序在类级别处理路由事件,会在任何实例处理程序对类的任何实例响应相同事件之前进行调用。 当路由事件标记为已处理时,它们通常会在类处理程序中标记为这样。 本文讨论了将路由事件标记为已处理的好处和潜在缺陷、不同类型的路由事件和路由事件处理程序以及复合控件中的事件禁止。

重要

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

先决条件

本文假定你对路由事件有基本的了解,并且已阅读路由事件概述。 若要遵循本文中的示例,如果熟悉 Extensible Application Markup Language (XAML) 并知道如何编写 Windows Presentation Foundation (WPF) 应用程序,将会很有帮助。

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

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

  • 重要响应包括设置焦点、修改公共状态、设置影响视觉表示形式的属性、引发新事件以及完全处理事件。
  • 不重要响应包括修改私有状态(而没有视觉或编程影响)、事件日志记录以及检查事件数据而不响应事件。

某些 WPF 控件通过将不需要进一步处理的组件级别事件标记为已处理来禁止这些事件。 如果要处理已由控件标记为已处理的事件,请参阅通过控件解决事件禁止问题

若要将事件标记为已处理,请在其事件数据中将 Handled 属性值设置为 true。 尽管可以将该值还原到 false,但很少需要这样做。

预览和浮升路由事件对

预览和浮升路由事件对特定于输入事件。 多个输入事件实现隧道浮升路由事件对,例如 PreviewKeyDownKeyDownPreview 前缀表示一旦预览事件完成,浮升事件便会启动。 每个预览和浮升事件对会共享事件数据的相同实例。

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

  1. 预览事件从应用程序根元素向下传递到引发路由事件的元素。 附加到应用程序根元素的预览事件处理程序会首先进行调用,接下来是附加到后续嵌套元素的处理程序。
  2. 预览事件完成后,配对的浮升事件会从将路由事件引发的元素传递到应用程序根元素。 附加到引发路由事件的相同元素的浮升事件处理程序会首先进行调用,接下来是附加到后续父元素的处理程序。

配对的预览和浮升事件是声明并引发自身路由事件的多个 WPF 类的内部实现的一部分。 如果没有该类级别内部实现,预览和浮升路由事件会完全独立,不会共享事件数据(无论事件命名如何)。 有关如何在自定义类中实现浮升或隧道输入路由事件的信息,请参阅创建自定义路由事件

由于每个预览和浮升事件对共享事件数据的相同实例,因此如果预览路由事件标记为已处理,则其配对的浮升事件也会进行处理。 如果浮升路由事件标记为已处理,则不会影响配对的预览事件,因为预览事件已完成。 将预览和浮升输入事件对标记为已处理时要小心。 已处理的预览输入事件不会为隧道路由的其余部分调用任何正常注册的事件处理程序,并且不会引发配对的浮升事件。 已处理的浮升输入事件不会为浮升路由的其余部分调用任何正常注册的事件处理程序。

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

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

实例事件处理程序

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

  • 不会调用使用 XAML 特性语法或 AddHandler 的公共签名附加的处理程序。
  • 会调用在 handledEventsToo 参数设置为 true 的情况下使用 AddHandler(RoutedEvent, Delegate, Boolean) 重载附加的处理程序。 此重载适用于需要响应已处理的事件的极少数情况。 例如,元素树中的某个元素已将事件标记为已处理,但事件路由中的其他元素需要响应已处理的事件。

下面的 XAML 示例将名为 componentWrapper 的自定义控件(包装名为 componentTextBoxTextBox)添加到名为 outerStackPanelStackPanelPreviewKeyDown 事件的实例事件处理程序使用 XAML 特性语法附加到 componentWrapper。 因此,实例处理程序只会响应由 componentTextBox 引发的未经处理的 PreviewKeyDown 隧道事件。

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

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

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 方法来附加静态类事件处理程序。 类层次结构中的每个类都可以为每个路由事件注册其自己的静态类处理程序。 因此,可以在事件路由中的任何给定节点上为相同事件调用多个静态类处理程序。 构造事件的事件路由时,每个节点的所有静态类处理程序都会添加到事件路由中。 节点上静态类处理程序的调用顺序从派生程度最高的静态类处理程序开始,接下来是来自每个后续基类的静态类处理程序。

使用 RegisterClassHandler(Type, RoutedEvent, Delegate, Boolean) 重载(handledEventsToo 参数设置为 true)注册的静态类事件处理程序会响应未经处理和已处理的路由事件。

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

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

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 事件系统仅为类层次结构中派生程度最高的类调用重写类事件处理程序。 类层次结构中派生程度最高的类随后可以使用 base 关键字调用虚拟方法的基实现。 在大多数情况下,无论是否将事件标记为已处理,都应调用基本实现。 如果类要求替换基实现(如果有),则应仅省略调用基实现。 在重写代码之前还是之后调用基实现取决于实现的性质。

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

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

复合控件中的输入事件禁止

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

解决输入事件禁止问题

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

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

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

有关如何解决输入事件禁止的示例,请参阅通过控件解决事件禁止问题

另请参阅