尽管对于何时将路由事件标记为已处理没有绝对规则,但如果代码以重要方式响应事件,请考虑将事件标记为已处理。 标记为已处理的路由事件会继续进行其路由,但只会调用配置为响应已处理事件的处理程序。 基本上,将路由事件标记为已处理会限制其在事件路由上对侦听器的可见性。
路由事件处理程序可以是实例处理程序或类处理程序。 实例处理程序处理对象或 XAML 元素上的路由事件。 类处理程序在类级别处理路由事件,会在任何实例处理程序对类的任何实例响应相同事件之前进行调用。 当路由事件标记为已处理时,它们通常会在类处理程序中标记为这样。 本文讨论了将路由事件标记为已处理的好处和潜在缺陷、不同类型的路由事件和路由事件处理程序以及复合控件中的事件禁止。
本文假定你对路由事件有基本的了解,并且已阅读路由事件概述
。 若要遵循本文中的示例,如果熟悉 Extensible Application Markup Language (XAML) 并知道如何编写 Windows Presentation Foundation (WPF) 应用程序,将会很有帮助。
通常,只应有一个处理程序为每个路由事件提供重要响应。 避免使用路由事件系统跨多个处理程序提供重要响应。 构成重要响应的定义是主观的,取决于应用程序。 一般准则是:
某些 WPF 控件通过将不需要进一步处理的组件级别事件标记为已处理来禁止这些事件。?
若要将事件标记为已处理,请在其事件数据中将?Handled?属性值设置为?true
。 尽管可以将该值还原到?false
,但很少需要这样做。
预览和浮升路由事件对特定于输入事件
。 多个输入事件实现隧道和浮升路由事件对,例如?PreviewKeyDown?和?KeyDown。?Preview
?前缀表示一旦预览事件完成,浮升事件便会启动。 每个预览和浮升事件对会共享事件数据的相同实例。
路由事件处理程序按对应于事件路由策略的顺序进行调用:
配对的预览和浮升事件是声明并引发自身路由事件的多个 WPF 类的内部实现的一部分。 如果没有该类级别内部实现,预览和浮升路由事件会完全独立,不会共享事件数据(无论事件命名如何)。?
由于每个预览和浮升事件对共享事件数据的相同实例,因此如果预览路由事件标记为已处理,则其配对的浮升事件也会进行处理。 如果浮升路由事件标记为已处理,则不会影响配对的预览事件,因为预览事件已完成。 将预览和浮升输入事件对标记为已处理时要小心。 已处理的预览输入事件不会为隧道路由的其余部分调用任何正常注册的事件处理程序,并且不会引发配对的浮升事件。 已处理的浮升输入事件不会为浮升路由的其余部分调用任何正常注册的事件处理程序。
路由事件处理程序可以是实例处理程序或类处理程序。 给定类的类处理程序会在任何实例处理程序对该类的任何实例响应相同事件之前进行调用。 由于此行为,当路由事件标记为已处理时,它们通常会在类处理程序中标记为这样。 有两种类型的类处理程序:
可以通过直接调用?AddHandler?方法,将实例处理程序附加到对象或 XAML 元素。 WPF 路由事件实现使用?AddHandler
?方法附加事件处理程序的公共语言运行时 (CLR) 事件包装器。 由于用于附加事件处理程序的 XAML 特性语法会导致调用 CLR 事件包装器,因此即时是在 XAML 中附加处理程序也会解析为?AddHandler
?调用。 对于已处理的事件:
AddHandler
?的公共签名附加的处理程序。handledEventsToo
?参数设置为?true
?的情况下使用?AddHandler(RoutedEvent, Delegate, Boolean)?重载附加的处理程序。 此重载适用于需要响应已处理的事件的极少数情况。 例如,元素树中的某个元素已将事件标记为已处理,但事件路由中的其他元素需要响应已处理的事件。下面的 XAML 示例将名为?componentWrapper
?的自定义控件(包装名为?componentTextBox
?的?TextBox)添加到名为?outerStackPanel
?的?StackPanel。?PreviewKeyDown?事件的实例事件处理程序使用 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);
}
下一部分显示了?ComponentWrapper
?的代码隐藏实现。
可以通过在类的静态构造函数中调用?RegisterClassHandler?方法来附加静态类事件处理程序。 类层次结构中的每个类都可以为每个路由事件注册其自己的静态类处理程序。 因此,可以在事件路由中的任何给定节点上为相同事件调用多个静态类处理程序。 构造事件的事件路由时,每个节点的所有静态类处理程序都会添加到事件路由中。 节点上静态类处理程序的调用顺序从派生程度最高的静态类处理程序开始,接下来是来自每个后续基类的静态类处理程序。
使用?RegisterClassHandler(Type, RoutedEvent, Delegate, Boolean)?重载(handledEventsToo
?参数设置为?true
)注册的静态类事件处理程序会响应未经处理和已处理的路由事件。
静态类处理程序通常注册为仅响应未经处理的事件。 在这种情况下,如果节点上的派生类处理程序将事件标记为已处理,则不会调用该事件的基类处理程序。 在这种情况下,基类处理程序实际上会被派生类处理程序所替换。 基类处理程序通常在视觉外观、状态逻辑、输入处理和命令处理等领域中帮助控制设计,因此在替换它们时要谨慎。 不将事件标记为已处理的派生类处理程序最终会补充基类处理程序,而不是替换它们。
下面的代码示例演示在前面 XAML 中引用的?ComponentWrapper
?自定义控件的类层次结构。?ComponentWrapper
?类从?ComponentWrapperBase
?派生类,而后者又从?StackPanel?类派生。 在?ComponentWrapper
?和?ComponentWrapperBase
?类的静态构造函数中使用的?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);
}
}
下一部分会讨论此代码示例中的重写类事件处理程序的代码隐藏实现。
某些视觉元素基类会为其每个公共路由输入事件公开空的 <On>事件名称?和 <OnPreview>事件名称?虚拟方法。 例如,UIElement?会实现?OnKeyDown?和?OnPreviewKeyDown?虚拟事件处理程序以及许多其他事件处理程序。 可以重写基类虚拟事件处理程序,以便为派生类实现重写类事件处理程序。 例如,可以通过重写?OnDragEnter?虚拟方法,为任何?UIElement
?派生类中的?DragEnter?事件添加重写类处理程序。 重写基类虚拟方法是比在静态构造函数中注册类处理程序更简单的一种实现类处理程序的方法。 在重写中,可以引发事件、启动特定于类的逻辑以更改实例中的元素属性、将事件标记为已处理或执行其他事件处理逻辑。
与静态类事件处理程序不同,WPF 事件系统仅为类层次结构中派生程度最高的类调用重写类事件处理程序。 类层次结构中派生程度最高的类随后可以使用?base?关键字调用虚拟方法的基实现。 在大多数情况下,无论是否将事件标记为已处理,都应调用基本实现。 如果类要求替换基实现(如果有),则应仅省略调用基实现。 在重写代码之前还是之后调用基实现取决于实现的性质。
在前面的代码示例中,基类?OnKeyDown
?虚拟方法在?ComponentWrapper
?和?ComponentWrapperBase
?类中进行重写。 由于 WPF 事件系统仅调用?ComponentWrapper.OnKeyDown
?重写类事件处理程序,因此该处理程序使用?base.OnKeyDown(e)
?调用?ComponentWrapperBase.OnKeyDown
?重写类事件处理程序,后者进而使用?base.OnKeyDown(e)
?调用?StackPanel.OnKeyDown
?虚拟方法。 前面的代码示例中的事件顺序为:
componentWrapper
?的实例处理程序由?PreviewKeyDown
?路由事件触发。componentWrapper
?的静态类处理程序由?KeyDown
?路由事件触发。componentWrapperBase
?的静态类处理程序由?KeyDown
?路由事件触发。componentWrapper
?的重写类处理程序由?KeyDown
?路由事件触发。componentWrapperBase
?的重写类处理程序由?KeyDown
?路由事件触发。KeyDown
?路由事件标记为已处理。componentWrapper
?的实例处理程序由?KeyDown
?路由事件触发。 处理程序已注册(handledEventsToo
?参数设置为?true
)。某些复合控件会在组件级别禁止输入事件
,以便将它们替换为包含更多信息或暗示更特定行为的自定义高级事件。 按照定义,复合控件是由多个实际控件或控件基类组成的。 经典示例是将各种鼠标事件转换为?Click?路由事件的?Button?控件。?Button
?基类是间接派生自?UIElement?的?ButtonBase?类。 控制输入处理所需的大部分事件基础结构在?UIElement
?级别提供。?UIElement
?会公开多个?Mouse?事件,例如?MouseLeftButtonDown?和?MouseRightButtonDown。?UIElement
?还实现空虚拟方法?OnMouseLeftButtonDown?和?OnMouseRightButtonDown?作为预注册类处理程序。?ButtonBase
?会重写这些类处理程序,在重写处理程序中将?Handled?属性设置?true
?为并引发?Click
?事件。 大多数侦听器的最终结果是?MouseLeftButtonDown
?和?MouseRightButtonDown
?事件会隐藏,而高级?Click
?事件可见。
有时,各个控件内的事件禁止可能会干扰应用程序中的事件处理逻辑。 例如,如果应用程序使用 XAML 特性语法在 XAML 根元素上附加?MouseLeftButtonDown?事件的处理程序,则不会调用该处理程序,因为?Button?控件将?MouseLeftButtonDown
?事件标记为已处理。 如果希望对已处理的路由事件调用应用程序的根的元素,则可以:
通过调用?UIElement.AddHandler(RoutedEvent, Delegate, Boolean)?方法(handledEventsToo
?参数设置为?true
)来附加处理程序。 此方法需要在获取要附加到的元素的对象引用后,在代码隐藏中附加事件处理程序。
如果标记为已处理的事件是浮升输入事件,则附加配对的预览事件的处理程序(如果可用)。 例如,如果控件禁止了?MouseLeftButtonDown
?事件,则可以改为附加?PreviewMouseLeftButtonDown?事件的处理程序。 此方法仅适用于共享事件数据的预览和浮升输入事件对。 请注意不要将?PreviewMouseLeftButtonDown
?标记为已处理,因为这会完全禁止?Click?事件。