Редактиране

Споделяне чрез


Marking routed events as handled, and class handling (WPF .NET)

Although there's no absolute rule for when to mark a routed event as handled, consider marking an event as handled if your code responds to the event in a significant way. A routed event that's marked as handled will continue along its route, but only handlers that are configured to respond to handled events are invoked. Basically, marking a routed event as handled limits its visibility to listeners along the event route.

Routed event handlers can be either instance handlers or class handlers. Instance handlers handle routed events on objects or XAML elements. Class handlers handle a routed event at a class level, and are invoked before any instance handler responding to the same event on any instance of the class. When routed events are marked as handled, they're often marked as such within class handlers. This article discusses the benefits and potential pitfalls of marking routed events as handled, the different types of routed events and routed event handlers, and event suppression in composite controls.

Prerequisites

The article assumes a basic knowledge of routed events, and that you've read Routed events overview. To follow the examples in this article, it helps if you're familiar with Extensible Application Markup Language (XAML) and know how to write Windows Presentation Foundation (WPF) applications.

When to mark routed events as handled

Typically, only one handler should provide a significant response for each routed event. Avoid using the routed event system to provide a significant response across multiple handlers. The definition of what constitutes a significant response is subjective and depends on your application. As general guidance:

  • Significant responses include setting focus, modifying public state, setting properties that affect visual representation, raising new events, and completely handling an event.
  • Insignificant responses include modifying private state without visual or programmatic impact, event logging, and examining event data without responding to the event.

Some WPF controls suppress component-level events that don't need further handling by marking them as handled. If you want to handle an event that was marked as handled by a control, see Working around event suppression by controls.

To mark an event as handled, set the Handled property value in its event data to true. Although it's possible to revert that value to false, the need to do so should be rare.

Preview and bubbling routed event pairs

Preview and bubbling routed event pairs are specific to input events. Several input events implement a tunneling and bubbling routed event pair, such as PreviewKeyDown and KeyDown. The Preview prefix signifies that the bubbling event starts once the preview event completes. Each preview and bubbling event pair shares the same instance of event data.

Routed event handlers are invoked in an order that corresponds to an event's routing strategy:

  1. The preview event travels from the application root element down to the element that raised the routed event. Preview event handlers attached to the application root element get invoked first, followed by handlers attached to successive nested elements.
  2. After the preview event completes, the paired bubbling event travels from the element that raised the routed event to the application root element. Bubbling event handlers attached to the same element that raised the routed event get invoked first, followed by handlers attached to successive parent elements.

Paired preview and bubbling events are part of the internal implementation of several WPF classes that declare and raise their own routed events. Without that class-level internal implementation, preview and bubbling routed events are entirely separate and won't share event data—regardless of event naming. For information about how to implement bubbling or tunneling input routed events in a custom class, see Create a custom routed event.

Because each preview and bubbling event pair shares the same instance of event data, if a preview routed event is marked as handled then its paired bubbling event will also be handled. If a bubbling routed event is marked as handled, it won't affect the paired preview event because the preview event completed. Be careful when marking preview and bubbling input event pairs as handled. A handled preview input event won't invoke any normally registered event handlers for the remainder of the tunneling route, and the paired bubbling event won't be raised. A handled bubbling input event won't invoke any normally registered event handlers for the remainder of the bubbling route.

Instance and class routed event handlers

Routed event handlers can be either instance handlers or class handlers. Class handlers for a given class are invoked before any instance handler responding to the same event on any instance of that class. Due to this behavior, when routed events are marked as handled, they're often marked as such within class handlers. There are two types of class handlers:

Instance event handlers

You can attach instance handlers to objects or XAML elements by directly calling the AddHandler method. WPF routed events implement a common language runtime (CLR) event wrapper that uses the AddHandler method to attach event handlers. Since XAML attribute syntax for attaching event handlers results in a call to the CLR event wrapper, even attaching handlers in XAML resolves to an AddHandler call. For handled events:

  • Handlers that are attached using XAML attribute syntax or the common signature of AddHandler aren't invoked.
  • Handlers that are attached using the AddHandler(RoutedEvent, Delegate, Boolean) overload with the handledEventsToo parameter set to true are invoked. This overload is available for the rare cases when it's necessary to respond to handled events. For example, some element in an element tree has marked an event as handled, but other elements further along the event route need to respond to the handled event.

The following XAML sample adds a custom control named componentWrapper, which wraps a TextBox named componentTextBox, to a StackPanel named outerStackPanel. An instance event handler for the PreviewKeyDown event is attached to the componentWrapper using XAML attribute syntax. As a result, the instance handler will only respond to unhandled PreviewKeyDown tunneling events raised by the componentTextBox.

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

The MainWindow constructor attaches an instance handler for the KeyDown bubbling event to the componentWrapper using the UIElement.AddHandler(RoutedEvent, Delegate, Boolean) overload, with the handledEventsToo parameter set to true. As a result, the instance event handler will respond to both unhandled and handled events.

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

The code-behind implementation of ComponentWrapper is shown in the next section.

Static class event handlers

You can attach static class event handlers by calling the RegisterClassHandler method in the static constructor of a class. Each class in a class hierarchy can register its own static class handler for each routed event. As a result, there can be multiple static class handlers invoked for the same event on any given node in the event route. When the event route for the event is constructed, all static class handlers for each node are added to the event route. The order of invocation of static class handlers on a node starts with the most-derived static class handler, followed by static class handlers from each successive base class.

Static class event handlers registered using the RegisterClassHandler(Type, RoutedEvent, Delegate, Boolean) overload with the handledEventsToo parameter set to true will respond to both unhandled and handled routed events.

Static class handlers are typically registered to respond to unhandled events only. In which case, if a derived class handler on a node marks an event as handled, base class handlers for that event won't be invoked. In that scenario, the base class handler is effectively replaced by the derived class handler. Base class handlers often contribute to control design in areas such as visual appearance, state logic, input handling, and command handling, so be cautious about replacing them. Derived class handlers that don't mark an event as handled end up supplementing the base class handlers instead of replacing them.

The following code sample shows the class hierarchy for the ComponentWrapper custom control that was referenced in the preceding XAML. The ComponentWrapper class derives from the ComponentWrapperBase class, which in turn derives from the StackPanel class. The RegisterClassHandler method, used in the static constructor of the ComponentWrapper and ComponentWrapperBase classes, registers a static class event handler for each of those classes. The WPF event system invokes the ComponentWrapper static class handler ahead of the ComponentWrapperBase static class handler.

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

The code-behind implementation of the override class event handlers in this code sample is discussed in the next section.

Override class event handlers

Some visual element base classes expose empty On<event name> and OnPreview<event name> virtual methods for each of their public routed input events. For example, UIElement implements the OnKeyDown and OnPreviewKeyDown virtual event handlers, and many others. You can override base class virtual event handlers to implement override class event handlers for your derived classes. For example, you can add an override class handler for the DragEnter event in any UIElement derived class by overriding the OnDragEnter virtual method. Overriding base class virtual methods is a simpler way to implement class handlers than registering class handlers in a static constructor. Within the override, you can raise events, initiate class-specific logic to change element properties on instances, mark the event as handled, or perform other event handling logic.

Unlike static class event handlers, the WPF event system only invokes override class event handlers for the most derived class in a class hierarchy. The most derived class in a class hierarchy, can then use the base keyword to call the base implementation of the virtual method. In most cases, you should call the base implementation, regardless of whether you mark an event as handled. You should only omit calling the base implementation if your class has a requirement to replace the base implementation logic, if any. Whether you call the base implementation before or after your overriding code depends on the nature of your implementation.

In the preceding code sample, the base class OnKeyDown virtual method is overridden in both the ComponentWrapper and ComponentWrapperBase classes. Since the WPF event system only invokes the ComponentWrapper.OnKeyDown override class event handler, that handler uses base.OnKeyDown(e) to call the ComponentWrapperBase.OnKeyDown override class event handler, which in turn uses base.OnKeyDown(e) to call the StackPanel.OnKeyDown virtual method. The order of events in the preceding code sample is:

  1. The instance handler attached to componentWrapper is triggered by the PreviewKeyDown routed event.
  2. The static class handler attached to componentWrapper is triggered by the KeyDown routed event.
  3. The static class handler attached to componentWrapperBase is triggered by the KeyDown routed event.
  4. The override class handler attached to componentWrapper is triggered by the KeyDown routed event.
  5. The override class handler attached to componentWrapperBase is triggered by the KeyDown routed event.
  6. The KeyDown routed event is marked as handled.
  7. The instance handler attached to componentWrapper is triggered by the KeyDown routed event. The handler was registered with the handledEventsToo parameter set to true.

Input event suppression in composite controls

Some composite controls suppress input events at the component-level in order to replace them with a customized high-level event that carries more information or implies a more specific behavior. A composite control is by definition composed of multiple practical controls or control base classes. A classic example is the Button control, which transforms various mouse events into a Click routed event. The Button base class is ButtonBase, which indirectly derives from UIElement. Much of the event infrastructure needed for control input processing is available at the UIElement level. UIElement exposes several Mouse events such as MouseLeftButtonDown and MouseRightButtonDown. UIElement also implements the empty virtual methods OnMouseLeftButtonDown and OnMouseRightButtonDown as preregistered class handlers. ButtonBase overrides these class handlers, and within the override handler sets the Handled property to true and raises a Click event. The end result for most listeners is that the MouseLeftButtonDown and MouseRightButtonDown events are hidden, and the high-level Click event is visible.

Working around input event suppression

Sometimes event suppression within individual controls can interfere with the event handling logic in your application. For example, if your application used XAML attribute syntax to attach a handler for the MouseLeftButtonDown event on the XAML root element, that handler won't be invoked because the Button control marks the MouseLeftButtonDown event as handled. If you want elements toward the root of your application to be invoked for a handled routed event, you can either:

  • Attach handlers by calling the UIElement.AddHandler(RoutedEvent, Delegate, Boolean) method with the handledEventsToo parameter set to true. This approach requires attaching the event handler in code-behind, after obtaining an object reference for the element that it will attach to.

  • If the event marked as handled is a bubbling input event, attach handlers for the paired preview event, if available. For example, if a control suppresses the MouseLeftButtonDown event, you can attach a handler for the PreviewMouseLeftButtonDown event instead. This approach only works for preview and bubbling input event pairs, which share event data. Be careful not to mark the PreviewMouseLeftButtonDown as handled since that would completely suppress the Click event.

For an example of how to work around input event suppression, see Working around event suppression by controls.

See also