Sind Ihnen schon mal die hilfreichsten Hilfetexte des gesamten MSDN aufgefallen? Diese, mit den grundehrlichsten, richtigsten aber vermeintlich völlig unnützen Aussagen? Die in der Regel mit „On“ anfangen? Zum Beispiel dieser hier, OnLoad eines Formular. Lapidare Erklärung: Löst das Load-Ereignis aus. Oder der hier, OnResize, auch gut: Löst das Resize-Ereignis aus. Das ist in etwa so wie der Witz, bei dem sich zwei Piloten im Landeanflug auf Seattle verfliegen, weil es total nebelig ist. Völlig orientierungslos finden Sie in einem Hochhaus am Fenster jemanden rauchend eine Pause machen. Sie fliegen zu ihm, der Pilot kurbelt das Fenster runter, und fragt: „Hey, wissen Sie, wo wir sind?“. Der Mann sagt: „Sie befinden sich in einer Boing 767!“. Der Pilot fliegt darauf schnurstracks zum SEATAC Airport und landet seine Maschine sicher. Sein Kollege fragt ihn: „Wie hast du das jetzt gemacht?“ – Darauf der Pilot: „Ganz einfach: Als ich dem Mann eine Frage gestellt habe, auf die er eine vollkommen korrekte aber völlig unbrauchbare Antwort gegeben hat, wusste ich, ich bin am MSDN-Hochhaus in Bellevue – und von dort kenne ich den Weg zum Airport…“

Dabei bergen die OnXXX-Hilfetexte eigentlich mehr Brauchbares, als es auf den ersten Blick scheinen würde. Worum geht’s? Einfach um eine Unsitte in abgeleiteten Klasse die denkbar komplizierteste Weise zum Aufrufen einer Methode zu finden.

Stellen Sie sich vor, sie hängen sich direkt in die Windows-Warteschleife eines Steuerelementes oder eines Formulars, um eine bestimmte Nachricht abzuwarten, die, wenn sie eintritt, direkt zu einer Methode springen soll. Dann könnten Sie das auf folgende Weise implementieren:

Public Class MeinFormular

    Inherits Form

 

    Private Const WM_MEINENACHRICHT = 42

 

    Private Sub WindowsWarteschlage(M As Message)

        If M.Msg = WM_MEINENACHRICHT Then

            BeimMeineNachrichtEintretenTue()

        End If

    End Sub

 

    Private Sub BeimMeineNachrichtEintretenTue()

        'Hier wird meine Nachricht verarbeitet

    End Sub

 

End Class

Wie es jetzt genau dazu kommt, dass die Windows-Warteschlage aufgerufen wird, soll uns an dieser Stelle erst mal nicht interessieren. Gehen wir für den Moment einmal davon aus, dass das Formular es intern so machen würde (es tut es nicht, jedenfalls nicht unter diesem Namen).

Was halten Sie denn von der folgenden Weise, die Methode aufzurufen?

Public Class MeinFormular

    Inherits Form

 

    Sub New()

        AddHandler Me.MeineNachricht,
              AddressOf BeimMeineNachrichtEintretenTue

    End Sub

 

    Public Event MeineNachricht(sender As Object, e As EventArgs)

 

    Private Const WM_MEINENACHRICHT = 42

 

    Private Sub WindowsWarteschlage(M As Message)

        If M.Msg = WM_MEINENACHRICHT Then

            RaiseEvent MeineNachricht(Me, EventArgs.Empty)

        End If

    End Sub

 

    Private Sub BeimMeineNachrichtEintretenTue(s As Object, e As EventArgs)

        'Hier wird meine Nachricht verarbeitet

    End Sub

 

End Class

Hier bindet die eigene Klasse eine Ereignisbehandlungsroutine erst an ein Ereignis; wenn dann die gesuchte Nachricht eintritt, wird dieses Ereignis ausgelöst, das die Instanz der Klasse, die das Ereignis auslöst, direkt wieder konsumiert…ähh – würde keiner so programmieren, oder?

Ich behaupte doch. Das passiert sogar täglich, aus Unwissenheit, denn man merkt es nicht. Und es geht so schnell! Wie verarbeiten Sie das Resize-Ereignis eines Formulars? Ganz einfach: Sie binden es im Formularcode, also so:

Public Class MeinFormular

    Inherits Form

 

    Private Sub MeinFormular_Resize(sender As Object,

                                    e As System.EventArgs) Handles Me.Resize

        'Hier steht ihr Resize-Code

    End Sub

        .

        .

        .

Und damit binden Sie in der Klasse, die ein Ereignis auslöst, auch das Ereignis wieder ein. So, wie wir gerade noch ein Beispiel weiter oben festgestellt haben, dass es kein Mensch machen würde.

Schauen wir uns doch mal an, was an dieser Stelle tatsächlich passiert, und warum. Dazu müssen wir vorher allerdings eine Sache wissen: Ereignisse sind nicht vererbbar. Wenn ich unser Beispielform, wie wir es oben kennengelernt habe, vererbe, habe ich in dieser abgeleiteten Klasse keine Möglichkeit, das definierte Ereignis direkt aufzurufen:

Public Class MeinGeerbtesFormular

    Inherits MeinFormular

 

    Sub LöstEreignisAus()

        'FEHLER: Abgeleitete Klassen können keine Basisklasse-Ereignisse auslösen!

        RaiseEvent MeineNachricht(Me, EventArgs.Empty)

    End Sub

 

End Class

Das gibt eine Fehlermeldung. Nun wird es aber in Klassenhierarchien und Klassenmodellen wie WinForms oder WPF aber sehr oft erforderlich sein, dass eine abgeleitete Klasse ein Ereignis aufrufen muss, das in der Basisklasse definiert ist. Und dabei behilft man sich einfach mit einem Trick: Man ruft das Ereignis nicht direkt auf, sondern packt es in eine Methode, die das Ereignis auslöst, und die die abgeleitete Klasse erreichen kann, indem man den Zugriffsmodifizierer von Private in Protected abändert. Also etwa so:

Public Class MeinFormular

    Inherits Form

 

    Public Event MeineNachricht(sender As Object, e As EventArgs)

 

    Private Const WM_MEINENACHRICHT = 42

 

    Private Sub WindowsWarteschlage(M As Message)

        If M.Msg = WM_MEINENACHRICHT Then

            BeimMeineNachrichtEintretenTue(EventArgs.Empty)

        End If

    End Sub

 

    Protected Overridable Sub BeimMeineNachrichtEintretenTue(e As EventArgs)

        RaiseEvent MeineNachricht(Me, e)

    End Sub

End Class

 

Public Class MeinGeerbtesFormular

    Inherits MeinFormular

 

    Sub LöstEreignisAus()

        'FEHLER: Abgeleitete Klassen können keine Basisklasse-Ereignisse auslösen!

        'RaiseEvent MeineNachricht(Me, EventArgs.Empty)

 

        'Aber so geht's:

        MyBase.BeimMeineNachrichtEintretenTue(EventArgs.Empty)

    End Sub

 

End Class

So. Und jetzt übersetzen wir das ganze Zeugs mal ins Englische, und schauen, ob wir irgendwelche Ähnlichkeiten zu bestehenden, bekannten Gegebenheiten erkennen können:

Public Class MyForm

    Inherits Form

 

    Public Event MyMessage(sender As Object, e As EventArgs)

 

    Private Const WM_MYMESSAGE = 42

 

    Private Sub WindowsProcedure(M As Message)

        If M.Msg = WM_MYMESSAGE Then

            OnMyMessage(EventArgs.Empty)

        End If

    End Sub

 

    Protected Overridable Sub OnMyMessage(e As EventArgs)

        RaiseEvent MyMessage(Me, e)

    End Sub

End Class

 

Public Class MyDerivedForm

    Inherits MyForm

 

    Sub RaisesMyMessage()

        'FEHLER: Abgeleitete Klassen können keine Basisklasse-Ereignisse auslösen!

        'RaiseEvent MeineNachricht(Me, EventArgs.Empty)

 

        'Aber so geht's:

        MyBase.OnMyMessage(EventArgs.Empty)

    End Sub

 

End Class

Ha-haaa! Jetzt wird es durchsichtiger, oder nicht? OK – der Grund also, wieso es für jedes Ereignis auch eine Ereignisauslösende Methode gibt, die mit OnXXX beginnt ist also, dass Ereignisse nicht vererbt werden können. Nun ist es fast konsequente Tradition im .NET-Framework, dass die Methode der Basisklassen, die die Ereignisse auslösen, auch in den abgeleiteten Klassen überschrieben werden können (warum als eine der ganz wenigen Ausnahmen das Load-Ereignis des WPF-Windows davon abweichend ist, hat mir mal ein wirklich cleverer Microsofty zu erklären versucht – ich hab’s aber nicht verstanden; wahrscheinlich ist mein Englisch einfach zu schlecht… ;-). Wenn wir uns also in einer abgeleiteten Klasse befinden, die das Ereignis auslöst, das wir behandeln müssen, reicht es also aus, die Methode zu überschreiben, die das Ereignis auslöst, und nicht – von hinten durch die Brust ins Auge – das Ereignis zu konsumieren, das unsere Klasse untendrunter ja gerade mühselig ausgelöst hat!

 

Public Class MyDerivedForm

    Inherits MyForm

 

    Protected Overrides Sub OnResize(e As System.EventArgs)

        'Hier steht der Ereigniscode, der behandelt wird, bevor

        'alle anderen das Ereignis bekommen haben!

        MyBase.OnResize(e)

        'Hier der, nachdem allen anderen das Ereignis bekommen haben.

    End Sub

Und so bindet man Ereignisse ein, die die eigene Klasse in seiner Basisklassendefinition generiert. Direkt und ohne Umwege. Auch hier ruft die WindowsProcedure (die in Wahrheit übrigens WndProc heißt, und auch die lässt sich überschreiben) folgende Ereigniskette auf:

  1. WndProc prüft auf den Nachrichtenwert WM_RESIZE
  2. WndProc ruft – wenn gefunden – die interne Methode WM_RESIZE des Forms auf.
  3. WM_RESIZE ruft OnResize auf
  4. OnResize löst das Resize-Ereignis aus
  5. Alle registrierten Methoden in der Delegatliste des Ereignisses werden aufgerufen.

Wenn wir nun in einer abgeleiteten Klasse (und jedes selbstgestaltete Formular ist eine abgeleitete Klasse, nämlich von System.Windows.Forms.Form abgeleitet) arbeiten, überschreiben wir einfach OnResize, und sind bereits bei 3. dran, und nicht erst bei 5.

Doch einen Nachteil kann diese Technik bei selbstimplementierten Ereignissen in eigenen Klassen haben: und zwar immer dann, wenn die OnXXX-Methoden NICHT nur ein Ereignis auslösen, sondern auch noch andere Dinge tun. Das sollte gemäß Konvention eigentlich nicht der Fall sein - kann aber passieren. Dann nämlich bestimmt RaiseEvent die richtige Stelle der Ereignisbehandlung; beim Überschreiben der Methoden weiß man nicht, an welcher Stelle der Aufruf von MyBase.OnXXX gesetzt werden muss oder soll. Ruft man es als erste Zeile in der überschriebenen OnXXX-Methode auf, könnte die Basismethode vielleicht erwarten, dass die abgeleitete Klasse schon etwas getan hat, was aber dann noch nicht der Fall war. Ruft man es als letzte Zeile auf, sind bestimmte Dinge von der Basismethode vielleicht noch nicht vorbereitet. Aber wie gesagt, das ist eher die seltene Ausnahme: OnXXX sollte nichts Weiteres tun, als das Ereignis auszulösen, und an den meisten Stellen im Framework ist genau das der Fall. Im Zweifelsfall schauen Sie sich den betroffenen Code mit Reflector an (das seit April 2011 leider nicht mehr umsonst erhältlich ist).

 

Tags: , , | Categories: