Markieren von Routingereignissen als behandelt und Klassenbehandlung (WPF .NET)

Es gibt zwar keine absolute Regel dafür, wann ein Routingereignis als behandelt markiert werden sollte, aber Sie sollten ein Ereignis als behandelt markieren, wenn Ihr Code in signifikanter Weise auf das Ereignis reagiert. Ein als behandelt markiertes Routingereignis wird auf seiner Route fortgesetzt, aber nur Handler, die für die Reaktion auf behandelte Ereignisse konfiguriert sind, werden aufgerufen. Im Grunde schränkt das Markieren eines Routingereignisses als behandelt dessen Sichtbarkeit für Listener entlang der Ereignisroute ein.

Bei Routingereignishandlern kann es sich um Instanzhandler oder Klassenhandler handeln. Instanzhandler behandeln Routingereignisse für Objekte oder XAML-Elemente. Klassenhandler behandeln ein Routingereignis auf Klassenebene und werden vor jedem Instanzhandler aufgerufen, der auf dasselbe Ereignis in einer Instanz der Klasse reagiert. Wenn Routingereignisse als behandelt markiert werden, erfolgt diese Markierung häufig innerhalb von Klassenhandlern. In diesem Artikel werden die Vorteile und potenziellen Nachteile der Markierung von Routingereignissen als behandelt, die verschiedenen Typen von Routingereignissen und Routingereignishandlern sowie die Ereignisunterdrückung in zusammengesetzten Steuerelementen erläutert.

Wichtig

Der Desktopleitfaden zu .NET 7 und .NET 6 ist in Bearbeitung.

Voraussetzungen

Im Artikel wird davon ausgegangen, dass Sie grundlegende Kenntnisse über Routingereignisse besitzen und die Übersicht über Routingereignisse gelesen haben. Um den Beispielen in diesem Artikel zu folgen, ist es hilfreich, wenn Sie mit Extensible Application Markup Language (XAML) vertraut sind und wissen, wie Windows Presentation Foundation-Anwendungen (WPF-Anwendungen) geschrieben werden.

Grundlegendes zum Markieren von Routingereignissen als behandelt

In der Regel sollte nur ein Handler eine signifikante Reaktion für jedes Routingereignis abgeben. Vermeiden Sie die Verwendung des Routingereignissystems, um eine signifikante Reaktion über mehrere Handler zu erzeugen. Die Definition einer signifikanten Reaktion ist subjektiv und hängt von Ihrer Anwendung ab. Allgemeine Hinweise:

  • Signifikante Reaktionen umfassen das Festlegen des Fokus, das Ändern des öffentlichen Zustands, das Festlegen von Eigenschaften, die sich auf die visuelle Darstellung auswirken, das Auslösen neuer Ereignisse und die vollständige Behandlung eines Ereignisses.
  • Unbedeutende Reaktionen umfassen das Ändern des privaten Zustands ohne visuelle oder programmgesteuerte Auswirkungen, die Ereignisprotokollierung und das Untersuchen von Ereignisdaten, ohne auf das Ereignis zu reagieren.

Einige WPF-Steuerelemente unterdrücken Ereignisse auf Komponentenebene, die keine weitere Behandlung benötigen, indem sie als behandelt markiert werden. Wenn Sie ein Ereignis behandeln möchten, das von einem Steuerelement als behandelt markiert wurde, lesen Sie Umgehen der Ereignisunterdrückung durch Steuerelemente.

Um ein Ereignis als behandelt zu markieren, legen Sie den Wert der Eigenschaft Handled in den Ereignisdaten auf true fest. Obwohl es möglich ist, diesen Wert auf false zurückzusetzen, sollte dies nur selten erforderlich sein.

Vorschau- und Bubbling-Routingereignispaare

Vorschau- und Bubbling-Routingereignispaare sind für Eingabeereignisse spezifisch. Mehrere Eingabeereignisse implementieren ein Tunneling- und Bubbling-Routingereignispaar, z. B. PreviewKeyDown und KeyDown. Das Präfix Preview gibt an, dass das Bubbling-Ereignis gestartet wird, sobald das Vorschauereignis abgeschlossen ist. Jedes Vorschau- und Bubbling-Ereignispaar verwendet dieselbe Instanz von Ereignisdaten.

Routingereignishandler werden in einer Reihenfolge aufgerufen, die der Routingstrategie eines Ereignisses entspricht:

  1. Das Vorschauereignis wird vom Anwendungsstammelement bis zu dem Element weitergeleitet, das das Routingereignis ausgelöst hat. An das Anwendungsstammelement angefügte Vorschauereignishandler werden zuerst aufgerufen, gefolgt von Handlern, die an nachfolgende geschachtelte Elemente angefügt sind.
  2. Nach Abschluss des Vorschauereignisses wird das gepaarte Bubbling-Ereignis von dem Element, das das Routingereignis ausgelöst hat, zum Anwendungsstammelement weitergeleitet. Bubbling-Ereignishandler, die an dasselbe Element angefügt sind, das das Routingereignis ausgelöst hat, werden zuerst aufgerufen, gefolgt von Handlern, die an nachfolgende übergeordnete Elemente angefügt sind.

Gepaarte Vorschau- und Bubblingereignisse sind Teil der internen Implementierung mehrerer WPF-Klassen, die ihre eigenen Routingereignisse deklarieren und auslösen. Ohne diese interne Implementierung auf Klassenebene sind Vorschau- und Bubbling-Routingereignisse vollständig separate Ereignisse, und sie verwenden nicht dieselben Ereignisdaten – unabhängig von der Benennung des Ereignisses. Informationen zum Implementieren von Bubbling- oder Tunneling-Eingaberoutingereignissen in einer benutzerdefinierten Klasse finden Sie unter Erstellen eines benutzerdefinierten Routingereignisses.

Da jedes Vorschau- und Bubbling-Ereignispaar dieselbe Instanz von Ereignisdaten verwendet, wird auch das gepaarte Bubbling-Ereignis behandelt, wenn ein Vorschauroutingereignis als behandelt markiert wird. Wenn ein Bubbling-Routingereignis als behandelt markiert wird, hat dies keine Auswirkungen auf das gepaarte Vorschauereignis, da das Vorschauereignis abgeschlossen ist. Gehen Sie sorgfältig vor, wenn Sie Vorschau- und Bubbling-Eingabeereignispaare als behandelt markieren. Ein behandeltes Vorschaueingabeereignis ruft keine normal registrierten Ereignishandler für den Rest der Tunnelroute auf, und das gepaarte Bubbling-Ereignis wird nicht ausgelöst. Ein behandeltes Bubbling-Eingabeereignis ruft keine normal registrierten Ereignishandler für den Rest der Bubbling-Route auf.

Instanz- und Klassen-Routingereignishandler

Bei Routingereignishandlern kann es sich um Instanzhandler oder Klassenhandler handeln. Klassenhandler für eine bestimmte Klasse werden vor jedem Instanzhandler aufgerufen, der auf dasselbe Ereignis in einer Instanz dieser Klasse reagiert. Wenn Routingereignisse als behandelt markiert werden, erfolgt diese Markierung aufgrund dieses Verhaltens häufig innerhalb von Klassenhandlern. Es gibt zwei Typen von Klassenhandlern:

  • Statische Klassenereignishandler, die durch Aufrufen der RegisterClassHandler-Methode innerhalb eines statischen Klassenkonstruktors registriert werden.
  • Überschreibende Klassenereignishandler, die durch Überschreiben (Außerkraftsetzen) der virtuellen Ereignismethoden der Basisklasse registriert werden. Virtuelle Ereignismethoden der Basisklasse sind in erster Linie für Eingabeereignisse vorhanden und weisen Namen auf, die mit On<Ereignisname> und OnPreview<Ereignisname> beginnen.

Instanzereignishandler

Sie können Instanzhandler an Objekte oder XAML-Elemente anfügen, indem Sie die AddHandler-Methode direkt aufrufen. WPF-Routingereignisse implementieren einen Common Language Runtime-Ereigniswrapper (CLR-Ereigniswrapper), der die AddHandler-Methode zum Anfügen von Ereignishandlern verwendet. Da die XAML-Attributsyntax für das Anfügen von Ereignishandlern zu einem Aufruf des CLR-Ereigniswrappers führt, wird auch das Anfügen von Handlern in XAML in einen AddHandler-Aufruf aufgelöst. Für behandelte Ereignisse gilt Folgendes:

  • Mit der XAML-Attributsyntax oder der allgemeinen Signatur von AddHandler angefügte Handler werden nicht aufgerufen.
  • Mithilfe der Überladung AddHandler(RoutedEvent, Delegate, Boolean) mit dem auf true festgelegten Parameter handledEventsToo angefügte Handler werden aufgerufen. Diese Überladung ist für die seltenen Fälle verfügbar, in denen auf behandelte Ereignisse reagiert werden muss. Beispielsweise hat ein Element in einer Elementstruktur ein Ereignis als behandelt markiert, aber andere Elemente weiter entlang der Ereignisroute müssen auf das behandelte Ereignis reagieren.

Im folgenden XAML-Beispiel wird ein benutzerdefiniertes Steuerelement mit dem Namen componentWrapper, das ein TextBox mit dem Namen componentTextBox umschließt, zu einem StackPanel mit dem Namen outerStackPanel hinzugefügt. Ein Instanzereignishandler für das PreviewKeyDown-Ereignis wird mit der XAML-Attributsyntax an den componentWrapper angefügt. Daher reagiert der Instanzhandler nur auf nicht behandelte PreviewKeyDown-Tunnelereignisse, die vom componentTextBox ausgelöst werden.

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

Der MainWindow-Konstruktor fügt einen Instanzhandler für das KeyDown-Bubbling-Ereignis an den componentWrapper an, wobei die Überladung UIElement.AddHandler(RoutedEvent, Delegate, Boolean) mit dem auf true festgelegten Parameter handledEventsToo verwendet wird. Daher reagiert der Instanzereignishandler sowohl auf nicht behandelte als auch auf behandelte Ereignisse.

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

Die Codebehind-Implementierung von ComponentWrapper wird im nächsten Abschnitt veranschaulicht.

Statische Klassenereignishandler

Sie können statische Klassenereignishandler anfügen, indem Sie die RegisterClassHandler-Methode im statischen Konstruktor einer Klasse aufrufen. Jede Klasse in einer Klassenhierarchie kann ihren eigenen statischen Klassenhandler für jedes Routingereignis registrieren. Daher können mehrere statische Klassenhandler für dasselbe Ereignis auf einem bestimmten Knoten der Ereignisroute aufgerufen werden. Beim Erstellen der Ereignisroute für das Ereignis werden der Ereignisroute alle statischen Klassenhandler für jeden Knoten hinzugefügt. Die Reihenfolge der Aufrufe statischer Klassenhandler auf einem Knoten beginnt mit dem am stärksten abgeleiteten statischen Klassenhandler, gefolgt von statischen Klassenhandlern jeder nachfolgenden Basisklasse.

Statische Klassenereignishandler, die mithilfe der Überladung RegisterClassHandler(Type, RoutedEvent, Delegate, Boolean) mit dem auf true festgelegten Parameter handledEventsToo registriert wurden, reagieren sowohl auf nicht behandelte als auch auf behandelte Routingereignisse.

In der Regel werden statische Klassenhandler registriert, um nur auf nicht behandelte Ereignisse zu reagieren. Wenn in diesem Fall ein Handler einer abgeleiteten Klasse auf einem Knoten ein Ereignis als behandelt markiert, werden die Basisklassenhandler für dieses Ereignis nicht aufgerufen. In diesem Szenario wird der Basisklassenhandler effektiv durch den Handler der abgeleiteten Klasse ersetzt. Basisklassenhandler tragen häufig zum Steuerelementdesign in Bereichen wie visuelle Darstellung, Zustandslogik, Eingabeverarbeitung und Befehlsverarbeitung bei, sodass sie mit Bedacht ersetzt werden sollten. Handler abgeleiteter Klassen, die keine Ereignisse als behandelt markieren, ergänzen die Basisklassenhandler, anstatt sie zu ersetzen.

Das folgende Codebeispiel zeigt die Klassenhierarchie für das benutzerdefinierte Steuerelement ComponentWrapper, auf das im vorherigen XAML-Beispiel verwiesen wurde. Die ComponentWrapper-Klasse leitet von der ComponentWrapperBase-Klasse ab, die sich wiederum von der StackPanel-Klasse ableitet. Die RegisterClassHandler-Methode, die im statischen Konstruktor der Klassen ComponentWrapper und ComponentWrapperBase verwendet wird, registriert einen statischen Klassenereignishandler für jede dieser Klassen. Das WPF-Ereignissystem ruft den statischen Klassenhandler ComponentWrapper vor dem statischen Klassenhandler ComponentWrapperBase auf.

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

Die CodeBehind-Implementierung der überschreibenden Klassenereignishandler in diesem Codebeispiel wird im nächsten Abschnitt erläutert.

Überschreibende Klassenereignishandler

Einige Basisklassen visueller Elemente stellen leere virtuelle Methoden vom Typ On<Ereignisname> und OnPreview<Ereignisname> für jedes ihrer öffentlichen Eingaberoutingereignisse bereit. Beispielsweise implementiert UIElement die virtuellen Ereignishandler OnKeyDown und OnPreviewKeyDown und viele andere. Sie können virtuelle Ereignishandler der Basisklasse außer Kraft setzen, um überschreibende Klassenereignishandler für Ihre abgeleiteten Klassen zu implementieren. Sie können beispielsweise einen überschreibenden Klassenhandler für das DragEnter-Ereignis in einer abgeleiteten UIElement-Klasse hinzufügen, indem Sie die virtuelle Methode OnDragEnter überschreiben. Das Überschreiben von virtuellen Basisklassenmethoden ist eine einfachere Methode zum Implementieren von Klassenhandlern als das Registrieren von Klassenhandlern in einem statischen Konstruktor. Innerhalb des Überschreibungsvorgangs können Sie Ereignisse auslösen, klassenspezifische Logik initiieren, um Elementeigenschaften für Instanzen zu ändern, das Ereignis als behandelt markieren oder andere Ereignisbehandlungslogik ausführen.

Im Gegensatz zu statischen Klassenereignishandlern ruft das WPF-Ereignissystem nur überschreibende Klassenereignishandler für die am stärksten abgeleitete Klasse in einer Klassenhierarchie auf. Die am stärksten abgeleitete Klasse in einer Klassenhierarchie kann dann das Schlüsselwort base verwenden, um die Basisimplementierung der virtuellen Methode aufzurufen. In den meisten Fällen sollten Sie die Basisimplementierung aufrufen, unabhängig davon, ob Sie ein Ereignis als behandelt markieren. Sie sollten die Basisimplementierung nur dann nicht aufrufen, wenn es für Ihre Klasse erforderlich ist, die Basisimplementierungslogik zu ersetzen, falls vorhanden. Ob Sie die Basisimplementierung vor oder nach dem überschreibenden Code aufrufen, hängt von der Art Ihrer Implementierung ab.

Im vorherigen Codebeispiel wird die virtuelle Basisklassenmethode OnKeyDown sowohl in der ComponentWrapper-Klasse als auch in der ComponentWrapperBase-Klasse überschrieben. Da das WPF-Ereignissystem nur den überschreibenden Klassenereignishandler ComponentWrapper.OnKeyDown aufruft, verwendet dieser Handler base.OnKeyDown(e), um den überschreibenden Klassenereignishandler ComponentWrapperBase.OnKeyDown aufzurufen, der wiederum base.OnKeyDown(e) zum Aufrufen der virtuellen Methode StackPanel.OnKeyDown verwendet. Die Reihenfolge der Ereignisse im vorherigen Codebeispiel lautet wie folgt:

  1. Der an componentWrapper angefügte Instanzhandler wird durch das Routingereignis PreviewKeyDown ausgelöst.
  2. Der an componentWrapper angefügte statische Klassenhandler wird durch das Routingereignis KeyDown ausgelöst.
  3. Der an componentWrapperBase angefügte statische Klassenhandler wird durch das Routingereignis KeyDown ausgelöst.
  4. Der an componentWrapper angefügte überschreibende Klassenhandler wird durch das Routingereignis KeyDown ausgelöst.
  5. Der an componentWrapperBase angefügte überschreibende Klassenhandler wird durch das Routingereignis KeyDown ausgelöst.
  6. Das Routingereignis KeyDown wird als behandelt markiert.
  7. Der an componentWrapper angefügte Instanzhandler wird durch das Routingereignis KeyDown ausgelöst. Der Handler wurde mit dem auf true festgelegten Parameter handledEventsToo registriert.

Eingabeereignisunterdrückung in zusammengesetzten Steuerelementen

Einige zusammengesetzte Steuerelemente unterdrücken Eingabeereignisse auf Komponentenebene, um sie durch ein angepasstes übergeordnetes Ereignis zu ersetzen, das weitere Informationen enthält oder ein spezifischeres Verhalten impliziert. Ein zusammengesetztes Steuerelement besteht per Definition aus mehreren praktischen Steuerelementen oder Steuerelementbasisklassen. Ein klassisches Beispiel ist das Button-Steuerelement, das verschiedene Mausereignisse in ein Click-Routingereignis transformiert. Die Button-Basisklasse ist ButtonBase, die sich indirekt von UIElement ableitet. Ein Großteil der für die Verarbeitung der Steuerelementeingabe erforderlichen Ereignisinfrastruktur ist auf der UIElement-Ebene verfügbar. UIElement macht mehrere Mouse-Ereignisse wie z. B. MouseLeftButtonDown und MouseRightButtonDown verfügbar. UIElement implementiert auch die leeren virtuellen Methoden OnMouseLeftButtonDown und OnMouseRightButtonDown als vorab registrierte Klassenhandler. ButtonBase überschreibt diese Klassenhandler und legt innerhalb des überschreibenden Handlers die Eigenschaft Handled auf true fest und löst ein Click-Ereignis aus. Das Endergebnis für die meisten Listener sieht so aus, dass die Ereignisse MouseLeftButtonDown und MouseRightButtonDown ausgeblendet sind und das übergeordnete Click-Ereignis sichtbar ist.

Umgehen der Eingabeereignisunterdrückung

Manchmal kann die Ereignisunterdrückung in einzelnen Steuerelementen die Ereignisbehandlungslogik in Ihrer Anwendung beeinträchtigen. Wenn Ihre Anwendung beispielsweise die XAML-Attributsyntax verwendet hat, um einen Handler für das MouseLeftButtonDown-Ereignis an das XAML-Stammelement anzufügen, wird dieser Handler nicht aufgerufen, da das Button-Steuerelement das MouseLeftButtonDown-Ereignis als behandelt markiert. Wenn Elemente in Richtung des Stamms Ihrer Anwendung für ein behandeltes Routingereignis aufgerufen werden sollen, können Sie eine der folgenden Aktionen ausführen:

  • Fügen Sie Handler an, indem Sie die Methode UIElement.AddHandler(RoutedEvent, Delegate, Boolean) mit dem auf true festgelegten Parameter handledEventsToo aufrufen. Bei diesem Ansatz muss der Ereignishandler im CodeBehind angefügt werden, nachdem ein Objektverweis für das Element abgerufen wurde, an das er angefügt wird.

  • Wenn das als behandelt markierte Ereignis ein Bubbling-Eingabeereignis ist, fügen Sie Handler für das gepaarte Vorschauereignis an, sofern verfügbar. Wenn beispielsweise ein Steuerelement das MouseLeftButtonDown-Ereignis unterdrückt, können Sie stattdessen einen Handler für das PreviewMouseLeftButtonDown-Ereignis anfügen. Dieser Ansatz funktioniert nur für Vorschau- und Bubbling-Eingabeereignispaare, die Ereignisdaten gemeinsam nutzen. Achten Sie darauf, das PreviewMouseLeftButtonDown-Ereignis nicht als behandelt zu markieren, da dadurch das Click-Ereignis vollständig unterdrückt würde.

Ein Beispiel dafür, wie die Eingabeereignisunterdrückung umgangen werden kann, finden Sie unter Umgehen der Ereignisunterdrückung durch Steuerelemente.

Siehe auch