3.10 Ereignisse
»Kein Schwein ruft mich an …« (Max Raabe und das Palast-Orchester, 1992)
Kein Wunder, dass keine Anrufe eintreffen. Haben Sie schon jemandem Bescheid gesagt, wie Sie erreichbar sind? Alle bisherigen Programme gingen davon aus, dass Sie sich selbst darum kümmern, woran Sie interessiert sind. Da dies auf Dauer recht mühsam ist, hat Visual Basic einen Mechanismus, mit dem ein Objekt Interesse bekunden kann, wenn etwas Bestimmtes passiert. Dazu meldet es sich bei dem Objekt an, das die Aktion von Interesse durchführt. Danach wird bei jeder durchgeführten Aktion der Interessent benachrichtigt. Er muss nicht ständig selbst auf der Hut sein, sondern kann in Ruhe abwarten, bis etwas passiert: Er wird automatisch benachrichtigt. Dies hat noch einen weiteren Effekt: Nicht jeder muss allem zuhören. Stellen Sie sich vor, Sie müssten täglich alle im Fernsehen ausgestrahlten Sendungen sehen. Das entspricht der bisherigen Situation. Glücklicherweise gibt es eine Fernbedienung, mit der Sie als Zuschauer festlegen, was Sie empfangen wollen. Damit wir dies in Visual Basic realisieren können, brauchen wir:
- die Definition der Aktion von Interesse (Ereignis)
- die Registrierung von Interessenten (Ereignishandler)
- eine Aktion, die alle registrierten Interessenten benachrichtigt (Auslösen)
Abhängig von der Art der Registrierung wird zwischen statischer und dynamischer Bindung unterschieden. Wenn die Standardmechanismen nicht reichen, helfen benutzerdefinierte Ereignisse weiter.
Hinweis |
Die beiden Registrierungsarten (siehe Abschnitt 3.10.2, »Statische Methodenbindung WithEvents und Handles« und Abschnitt 3.10.3, »Dynamische Methodenbindung: AddHandler und RemoveHandler«) können gemischt werden. |
Hinweis |
Das im Ereignis implizit enthaltene Objekt kann nicht vom Garbage Collector aufgeräumt werden. Daher ist eine Deregistrierung von nicht mehr benötigten Ereignissen empfehlenswert. |
3.10.1 Ereignis: Event und RaiseEvent
Als Erstes schauen wir uns die Syntax an, mit der ein Ereignis deklariert wird. Dazu gibt es zwei Varianten. Die Definition eines Delegates ist in Abschnitt 3.9.1, »Funktionszeiger: Delegate«, beschrieben. Optionale Teile sind in eckige Klammern gesetzt und kursive Teile müssen Sie Ihren Bedürfnissen anpassen.
[<Modifikatoren>] Event Name As Delegate-Typ [Effekt] [<Modifikatoren>] Event Name([<Parameter>]) [Effekt] |
Hinweis |
Ein Ereignis wird nur deklariert, eine Wertzuweisung nimmt der Compiler automatisch vor (siehe den Unterabschnitt »Automatisch erzeugter Code«). |
Ein Ereignis kann durch die Modifikatoren in Tabelle 3.14 angepasst werden und darf einen der Effekte in Tabelle 3.15 haben.
Art | Beschreibung |
Sichtbarkeit |
Grad der Öffentlichkeit (siehe Abschnitt 3.2, »Kapselung«) |
Bindung |
Objekt- oder klassenbezogen (siehe Abschnitt 3.4, »Bindung«) |
Redefinition |
Ersatz einer Definition mit Shadows (siehe Abschnitt 3.13, »Vererbung«) |
Art | Beschreibung |
Implementation |
Erfüllung einer Schnittstelle (siehe Abschnitt 3.15, »Schnittstellen: Interface und Implements«) |
Anders als bei Delegates wird ein Ereignis nicht direkt angesprochen, sondern »ausgelöst«. Für Standard- und benutzerdefinierte Ereignisse gilt dieselbe Syntax (kursive Teile müssen Sie Ihren Bedürfnissen anpassen):
RaiseEvent Name([<Parameter>]) |
Hinweis |
Ein Ereignis kann nur in derselben Klasse ausgelöst werden, in der es deklariert ist. Dies gilt auch im Rahmen der Vererbung. Fehlt in einer Klasse mit einem Ereignis das korrespondierende RaiseEvent, ist eine Auslösung nur über Invoke() möglich (siehe den Unterabschnitt »Automatisch erzeugter Code«). |
Damit kann eine erste Anwendung mit Ereignissen erstellt werden. Sie macht noch nichts Sichtbares, da für das Ereignis noch keine Interessenten registriert worden sind.
'...\Klassendesign\Ereignisse\Ereignis.vb |
Option Strict On Namespace Klassendesign Class Einfach Event Ereignis(ByVal no As Integer) Sub Auslösung() RaiseEvent Ereignis(-3) End Sub Shared Sub Test() Dim ein As Einfach = New Einfach() ein.Auslösung() Console.ReadLine() End Sub End Class ... End Namespace
Hinweis |
Die Behandlung eines Ereignisses ist optional. Unterbleibt sie, passiert einfach »nichts«. |
Ereignisse und Delegates
Durch Event x() wird implizit ein Delegate definiert. Daher kann das vorige Beispiel auch ein Delegate identischer Signatur verwenden, wie das nächste Codefragment zeigt. Die Verwendung von Sub für ein solches Delegate ist zwingend. Ob Sie den Weg über das Delegate gehen oder ein Ereignis direkt spezifizieren, ist reine Geschmackssache.
'...\Klassendesign\Ereignisse\Ereignis.vb |
Option Strict On Namespace Klassendesign ... Class Delagat Delegate Sub Delegat(ByVal no As Integer) Event Ereignis As Delegat Sub Auslösung() RaiseEvent Ereignis(-3) End Sub Shared Sub Test() Dim del As Delagat = New Delagat() del.Auslösung() Console.ReadLine() End Sub End Class End Namespace
Hinweis |
Ein Function-Delegate mit Rückgabewert kann nicht als Typ für ein Ereignis verwendet werden. |
Automatisch erzeugter Code
Die Deklaration eines Ereignisses definiert implizit ein paar Klassenmitglieder:
Private EreignisEvent As EreignisEventHandler Sub add_Ereignis(ByVal obj As EreignisEventHandler) ... End Sub Sub remove_Ereignis(ByVal obj As EreignisEventHandler) ... End Sub Shared __ENCList As List(Of WeakReference) Class EreignisEventHandler '< MulticastDelegate < Delegate ... End Class
Hinweis |
Über eine Typanalyse mit den Klassen aus dem Namensraum Reflection ist ein Zugriff auch dann möglich, wenn der Compiler einen direkten Zugriff verbietet. |
Die automatisch erzeugten Klassenmitglieder können bis auf die Klasse nicht direkt benutzt werden. Wenn Sie versuchen, gleichnamige Klassenmitglieder zu definieren, bekommen Sie dennoch eine Fehlermeldung des Compilers. Wie bei allen Automatismen sollten Sie sich nicht darauf verlassen, dass eine zukünftige Version von Visual Basic dieselben Mitglieder definiert. Intern verwendet RaiseEvent die Invoke()-Methode des implizit definierten Delegates. Das folgende Codefragment vermittelt eine Idee davon, wie das passieren könnte. Bitte beachten Sie, dass der Typ EreignisEventHandler nicht im Quelltext definiert wird. Er steht implizit zur Verfügung.
'...\Klassendesign\Ereignisse\RaiseEvent.vb |
Option Strict On
Namespace Klassendesign
Class Umstandskrämer
Event Ereignis(ByVal no As String)
Sub Auslösung()
RaiseEvent Ereignis("normal")
Dim del As EreignisEventHandler = AddressOf Empfänger
del.Invoke("umständlich")
End Sub
Sub Empfänger(ByVal art As String) Handles Me.Ereignis
Console.WriteLine("Art {0}", art)
End Sub
Shared Sub Test()
Dim man As Umstandskrämer = New Umstandskrämer()
man.Auslösung()
Console.ReadLine()
End Sub
End Class
End Namespace
Die Ausgabe zeigt die zweifache Ansprache der Ereignisbehandlungsprozedur:
Art normal Art umständlich
Hinweis |
Die Benutzung der Klasse ist nicht von diesem Automatismus betroffen. |
3.10.2 Statische Methodenbindung WithEvents und Handles
Um nun dem Ereignis Leben einzuhauchen, wird eine Ereignisbehandlung festgelegt. Am einfachsten (und unflexibelsten) ist es, bereits im Quelltext durch Deklaration festzulegen, welche Methode sich um ein Ereignis kümmert. Die Syntax lautet wie folgt (optionale Teile stehen in eckigen Klammern, kursive Teile müssen Sie Ihren Bedürfnissen anpassen, der Unterstrich macht wie üblich aus zwei Quelltextzeilen eine logische Zeile):
[<Modifikatoren>] Sub Name([<Parameter>]) _ Handles Kontext.Ereignis [, Kontext.Ereignis, ...] [<Anweisungen>] End Sub [<Modifikatoren>] Function Name([<Parameter>]) As Typ _ Handles Kontext.Ereignis [, Kontext.Ereignis, ...] [<Anweisungen>] End Function |
Hinweis |
Die Behandlung eines Ereignisses ist optional, und an einem Ereignis ohne korrespondierendes Handles ist nichts auszusetzen. Unglücklich ist dagegen ein Handles an der das Ereignis auslösenden Methode, da es eine infinite Rekursion auslöst. |
Tabelle 3.16 zeigt die erlaubten Modifikatoren.
Art | Beschreibung |
Sichtbarkeit |
Grad der Öffentlichkeit (siehe Abschnitt 3.2, »Kapselung«) |
Bindung |
Objekt- oder klassenbezogen (siehe Abschnitt 3.4, »Bindung«) |
Redefinition |
Art des Ersatzes oder Zwangs zu einer Definition (siehe Abschnitt 3.13, »Vererbung« und Abschnitt 3.3.4, »Überladung (Overloads)«) |
Leer |
Ohne Anweisungen (siehe Abschnitt 3.3.6, »Reine Deklaration mit Partial«), korrespondierende Implementation erforderlich. |
Der Kontext des Ereignisses kann nicht beliebig sein. Es muss das Objekt oder die Klasse sein, die entweder selbst sowohl eine Methode als auch ein Ereignis enthält (Me, MyClass) oder auf einer Klasse basiert, die dies tut (MyBase). Dabei darf keine Objektreferenz oder ein Klassenname verwendet werden, auch wenn sie inhaltlich dasselbe sein sollten. Auf die einzige weitere Alternative in Tabelle 3.17 gehen wir etwas weiter unten ein.
Kontext | Beschreibung |
Me |
aktuelles Objekt, nur hier: aktuelle Klasse |
MyClass |
aktuelle Klasse, nur hier: aktuelles Objekt |
MyBase |
Elternklasse (siehe Abschnitt 3.13, »Vererbung«) |
withevents |
Variable, die mit WithEvents gekennzeichnet ist (siehe unten) |
Das Ereignis ist der im vorigen Abschnitt mit Event deklarierte Bezeichner. Das folgende Codefragment zeigt die Reaktion eines Zuschauers auf eine Werbeunterbrechung eines Films. Die Reihenfolge der Auswertung ist durch Zahlen kenntlich gemacht. Als Erstes wird ein neuer Film erstellt und im zweiten Schritt mit der Methode Pause() unterbrochen. In dieser wird als dritter Schritt das Ereignis Werbung() mit RaiseEvent ausgelöst. Dies hat zur Folge, dass für jede Handles-Deklaration, die das Ereignis Werbung() als zu behandeln kennzeichnet, die Methode aufgerufen wird, die mit dem Handles gekennzeichnet ist. Da Zuschauer() zweifach markiert ist, wird die Methode auch zweimal aufgerufen werden. Es ist egal, ob Sie das aktuelle Objekt, hier ist es der aktuelle Film, mit Me oder MyClass qualifizieren. Schließlich beendet ReadLine() im fünften Schritt das Programm.
'...\Klassendesign\Ereignisse\Statisch.vb |
Option Strict On Namespace Klassendesign Class Film Public Event Werbung() Sub Pause() RaiseEvent Werbung() '3 End Sub Sub Zuschauer() Handles Me.Werbung, MyClass.Werbung Console.WriteLine("Umschalten!") '4 End Sub Shared Sub Test() Dim sendung As Film = New Film() '1 sendung.Pause() '2 Console.ReadLine() '5 End Sub End Class ... End Namespace
Hinweis |
Auch wenn alle Ereignisse und Methoden mit Shared klassengebunden sind, können Me und MyClass gleichwertig verwendet werden. |
Die Ausgabe zeigt die zweifache Reaktion durch die zwei Ereignisse in der Handles-Klausel:
Umschalten! Umschalten!
Hinweis |
Eine Methode mit einer Handles-Klausel kann auch weiterhin ganz normal aufgerufen werden. |
Behandlung außerhalb der Klasse
Wenn Sie, was in der Praxis weit häufiger ist, ein Ereignis und dessen Behandlungsmethode in separaten Klassen haben, entfallen die drei Konstanten Me, MyClass und MyBase in der Handles-Klausel als Möglichkeit zur Spezifikation des Ereignisobjekts. Dann muss als Kontextangabe eine Objektreferenz angegeben werden, die wie folgt mit WithEvents als möglicher Emittent von Ereignissen gekennzeichnet wurde (optionale Teile stehen in eckigen Klammern und kursive Teile müssen Sie Ihren Bedürfnissen anpassen):
[<Modifikatoren>] WithEvents Name As typ [= Wert] |
Hinweis |
Der typ muss zwar eine Klasse oder Schnittstelle sein, darf aber ereignislos sein. Es wird nur die Möglichkeit eines Ereignisses deklariert. |
Da ein Ereignis nur innerhalb der Klasse ausgelöst werden kann, in der es deklariert ist, ist die Initialisierung von Name bezüglich der Behandlungsmethoden gewährleistet (es käme sonst vorher zu einer NullReferenceException bei dem Versuch, über Name das Ereignis auszulösen). Sie können also Name auch außerhalb der Deklarationszeile initialisieren, Hauptsache es passiert vor der Ereignisauslösung. Die Modifikatoren sind fast deckungsgleich mit denen für ein Feld auf Klassenebene (siehe Tabelle 3.18).
Art | Beschreibung |
Sichtbarkeit |
Grad der Öffentlichkeit (siehe Abschnitt 3.2, »Kapselung«) |
Bindung |
Objekt- oder klassenbezogen (siehe Abschnitt 3.4, »Bindung«) |
Redefinition |
Ersatz mit Shadows (siehe Abschnitt 3.13, »Vererbung«) |
Hinweis |
Insbesondere ReadOnly ist als Modifikator nicht erlaubt. |
Das folgende Codefragment zeigt eine Separierung einer Lotto-Klasse mit den Ereignissen Zahl und Rest und einem Modul, das die Ereignisse behandelt. Dies zeigt, dass eine Klasse auch mehrere Ereignisse und Ereignisauslösungen im selben Block definieren kann. Sie können auch sehen, dass dasselbe Ereignis auch von verschiedenen Prozeduren behandelt werden kann.
'...\Klassendesign\Ereignisse\Statisch.vb |
Option Strict On Namespace Klassendesign ... Class Lotto Event Zahl(ByVal no As Short) Event Rest(ByVal no As Short) Sub Ziehung(ByVal no As Short, ByVal rest As Short) Console.WriteLine("Gezogen: {0}", no) RaiseEvent Zahl(no) RaiseEvent Rest(rest) End Sub End Class Module Statisch WithEvents lotto As Lotto Sub Booch(ByVal no As Int32) Handles lotto.Zahl Console.WriteLine("B: {0}", If(no=16, "gut", "Mist")) End Sub Sub Jacobson(ByVal no As Int32) Handles lotto.Zahl Console.WriteLine("J: {0}", If(no=41, "gut", "Mist")) End Sub Sub Rumbaugh(ByVal no As Int32) Handles lotto.Rest Console.WriteLine("R: noch {0} Zahlen", no) End Sub Sub Test() lotto = New Lotto() lotto.Ziehung(16, 5) : lotto.Ziehung(41, 4) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt die Reaktion der verschiedenen registrierten Prozeduren. Jede Handles-Deklaration führt zu einer eigenständigen Behandlung eines Ereignisses.
Gezogen: 16 B: gut J: Mist R: noch 5 Zahlen Gezogen: 41 B: Mist J: gut R: noch 4 Zahlen
Hinweis |
Eine einmal im Quelltext deklarierte Ereignisbehandlung kann nicht zurückgenommen werden (auch nicht im Rahmen der Vererbung); weitere hinzuzufügen ist jedoch möglich. |
Bindung an ein Objekt
Die Bindung einer Behandlung eines Ereignisses mittels der Handles-Klausel erfolgt bereits im Quelltext. Aber wann wird das Ereignis an ein Objekt gebunden? Während einer Initialisierung oder dynamisch? Das folgende Codefragment initialisiert ein Partner-Objekt während der Deklaration und löst dann in Test() mittels Treffen() ein Ereignis aus. Danach wird ein neuer Partner zugewiesen, und ein erneutes Treffen findet statt.
'...\Klassendesign\Ereignisse\Bindung.vb |
Option Strict On Namespace Klassendesign Class Partner Dim name As String Sub New(ByVal name As String) Me.name = name End Sub Event Gruß(ByVal name As String) Sub Treffen() RaiseEvent Gruß(name) End Sub End Class Module Bindung WithEvents p As Partner = New Partner("Primus") Sub Gruß(ByVal name As String) Handles p.Gruß Console.WriteLine("Hallo {0}", name) End Sub Sub Test() p.Treffen() p = New Partner("Secundus") p.Treffen() Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt ganz klar, dass das Ereignisobjekt das zum Zeitpunkt der Ereignisauslösung gültige Objekt ist, die Bindung ist also trotz fester Handles-Klausel dynamisch.
Hallo Primus Hallo Secundus
Reihenfolge der Handler
Wenn es möglich ist, mehrere Prozeduren zur Behandlung einzusetzen, stellt sich die Frage, in welcher Reihenfolge sie zum Zuge kommen. Das folgende Codefragment zeigt eine Fütterung im Zoo. Die Tiere stellen sich mit ihren Handles-Klauseln in alphabetischer Reihenfolge an. Alle bis auf die Giraffe nehmen auch Wasser mit der Fütterung.
'...\Klassendesign\Ereignisse\ReihenfolgeHandles.vb |
Option Strict On Namespace Klassendesign Class Zoo Event Futter(ByRef no As Short) Event Wasser(ByRef no As Short) Sub Fütterung() Console.WriteLine("—Futter—") : RaiseEvent Futter(0) Console.WriteLine("—Wasser—") : RaiseEvent Wasser(0) End Sub Sub Affe(ByRef no As Short) Handles Me.Futter, Me.Wasser no += 1S : Console.WriteLine("Affe: {0}", no) End Sub Sub Bär(ByRef no As Short) Handles Me.Futter, Me.Wasser no += 1S : Console.WriteLine("Bär: {0}", no) End Sub Sub Giraffe(ByRef no As Short) Handles Me.Futter no += 1S : Console.WriteLine("Giraffe: {0}", no) End Sub Sub Löwe(ByRef no As Short) Handles Me.Futter, Me.Wasser no += 1S : Console.WriteLine("Löwe: {0}", no) End Sub Shared Sub Test() Dim zoo As Zoo = New Zoo() zoo.Fütterung() Console.ReadLine() End Sub End Module End Namespace
Wenn Prozeduren beim Auslösen eines Ereignisses in der Reihenfolge ihrer Handles-Deklarationen im Quelltext aufgerufen würden, müssten die Tiere in der Ausgabe in alphabetischer Reihenfolge erscheinen. Die folgende Ausgabe zeigt, dass dies nicht der Fall ist. Die gleiche Reihenfolge bei Futter und Wasser lässt vermuten, dass die Ordnung durch die betroffenen Prozeduren bestimmt ist und nicht durch die Anzahl der Prozeduren, die ein Ereignis behandeln.
Futter-- Löwe: 1 Bär: 2 Giraffe: 3 Affe: 4 --Wasser-- Löwe: 1 Bär: 2 Affe: 3
Der Vollständigkeit halber folgt ein Codefragment, mit dem sich die Aufrufliste explizit ermitteln lässt:
Dim f As Reflection.FieldInfo = zoo.GetType().GetField("futterEvent", _ Reflection.BindingFlags.Instance Or Reflection.BindingFlags.NonPublic) Dim h As Zoo.futterEventHandler = _ CType(f.GetValue(zoo), Zoo.futterEventHandler) For Each del As [Delegate] In h.GetInvocationList() Console.WriteLine("Methode {0}", del.Method.ToString()) Next
Automatisch erzeugter Code
Durch die Deklaration
WithEvents obj As Ereignis
werden vom Compiler automatisch folgende Klassenmitglieder definiert beziehungsweise umgewidmet (alle Private):
Property obj(ByVal WithEventsValue As Ereignis) As Ereignis _obj As Ereignis Shared __ENCList As List(Of WeakReference)
Innerhalb der Eigenschaft wird die Registrierung des Ereignisses automatisch während einer Zuweisung vorgenommen. Sie erfolgt auch automatisch während der Laufzeit. Die entsprechenden Anweisungen werden vom Compiler automatisch generiert. Wenn Sie versuchen, eines der automatisch generierten Klassenmitglieder selbst zu definieren, bekommen Sie eine Fehlermeldung des Compilers.
3.10.3 Dynamische Methodenbindung: AddHandler und RemoveHandler
Die im letzten Abschnitt gezeigte Bindung einer Methode an ein Ereignis durch einfache Deklaration ist zwar sehr komfortabel, aber auch unflexibel. Es gibt Situationen, in denen Sie zur Laufzeit die Kontrolle darüber brauchen, ob eine Methode durch ein Ereignis angesprochen wird. Im täglichen Leben kommt das zum Beispiel vor, wenn Sie Ihr Handy im Flugzeug abschalten. Ein Beispiel aus der Softwaretechnik ist eine Funktion, die in einem Fenster viele Änderungen gleichzeitig vornimmt. Es wäre nicht sinnvoll, das Fenster nach jeder kleinen Änderung neu zu zeichnen. Besser ist es, das Neuzeichnen temporär abzuschalten.
Zur Kontrolle der Bindung einer Methode an ein Ereignis wird die folgende Syntax verwendet (die kursiven Namen müssen Sie Ihren Bedürfnissen anpassen):
AddHandler EreignisObj, delegatObj RemoveHandler EreignisObj, delegatObj |
Hinweis |
Die Methode darf einen Rückgabewert haben. Er wird ignoriert. |
Der erste Parameter, EreignisObj, bezeichnet ein mit Event deklariertes Ereignis, und DelegatObj ist ein Funktionszeiger auf die behandelnde Methode. Wie in Abschnitt 3.9.1, »Funktionszeiger: Delegates«, beschrieben wurde, kann DelegatObj in vier Varianten erzeugt werden. Da der Rückgabewert einer das Ereignis behandelnden Funktion ignoriert wird und ein Funktionsobjekt nur indirekt etwas »bewirken« kann, werden Sie die Varianten mit Function eher selten einsetzen. Der Typ des Delegates EreignisEventHandler wird, wie in Abschnitt 3.10.1, »Ereignis: Event und RaiseEvent«, beschrieben wurde, automatisch vom Compiler erzeugt (optionale Teile stehen in eckigen Klammern und kursive Teile müssen Sie Ihren Bedürfnissen anpassen).
Hinweis |
Das im Ereignis implizit enthaltene Objekt kann nicht vom Garbage Collector aufgeräumt werden. Daher ist eine Deregistrierung von nicht mehr benötigten Ereignissen empfehlenswert. |
New EreignisEventHandler(AddressOf Methode) New EreignisEventHandler(Function([<Parameter>]) Anweisung) AddressOf Methode Function([<Parameter>]) Anweisung |
Als Beispiel folgt ein Haus, das mit einer Alarmanlage gesichert ist. Durch AddHandler wird sie scharf geschaltet, da dann eine Methode das Ereignis behandelt. Analog schaltet RemoveHandler sie ab, da die Ereignisauslösung mittels RaiseEvent in TürÖffnen() von keiner Methode behandelt wird und ungehört verhallt.
'...\Klassendesign\Ereignisse\Dynamisch.vb |
Option Strict On Namespace Klassendesign Class Haus Event Alarm(ByVal wo As String) Sub TürÖffnen(ByVal wo As String) RaiseEvent Alarm(wo) End Sub Sub AlarmAnlage(ByVal wo As String) Console.Beep() : Console.WriteLine("{0}türalarm!", wo) End Sub End Class Module Dynamisch Sub Test() Dim villa As Haus = New Haus() AddHandler villa.Alarm, AddressOf villa.AlarmAnlage villa.TürÖffnen("Haus") RemoveHandler villa.Alarm, AddressOf villa.AlarmAnlage villa.TürÖffnen("Terasse") Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, dass der Alarm beim Öffnen der zweiten Tür nicht ausgelöst wurde, da er mittels RemoveHandler abgeschaltet wurde.
Haustüralarm!
Typkompatibilität
Wenn es die Möglichkeit gibt, dass ein Ereignis und ein Delegate formell gleich sind, stellt sich die Frage, ob man sie beliebig untereinander austauschen kann. Dies ist nicht der Fall, da sie unabhängige Datentypen sind, die nur »zufällig« formell gleich sind. Daher stellen die beiden ersten Zeilen des folgenden Codefragments verschiedene Datentypen dar. Da sie nur gleich und nicht identisch sind, wird der Versuch in Test(), ein Delegate-Objekt mit dem Ereignis zu verknüpfen, vom Compiler zurückgewiesen.
Delegate Sub FutterTyp(ByRef no As Short) Event Futter(ByRef no As Short) Sub Tier(ByRef no As Short) End Sub Sub Test() AddHandler Futter, New FutterTyp(AddressOf Tier) 'Fehler!! End Sub
Wird das Ereignis mittels einer As-Klausel deklariert, ist nur ein Typ im Spiel, und die folgenden Zeilen werden vom Compiler fehlerfrei übersetzt:
Delegate Sub FutterTyp(ByRef no As Short) Event Futter As FutterTyp Sub Tier(ByRef no As Short) End Sub Sub Test() AddHandler Futter, New FutterTyp(AddressOf Tier) 'OK! End Sub
Reihenfolge der Handler
Wie bei der statischen Methodenbindung stellt sich auch hier die Frage der Reihenfolge der Methodenaufrufe bei Auslösung eines Ereignisses. Das folgende Codefragment simuliert eine Fütterung im Tierpark. Wolf und Uhu haben für das Futter eine statische Bindung neben der dynamischen, die in Test() etabliert wird.
'...\Klassendesign\Ereignisse\ReihenfolgeAddHandler.vb |
Option Strict On Namespace Klassendesign Class Tierpark Delegate Sub Nehmen(ByRef no As Short) Event Futter As Nehmen Event Wasser As Nehmen Sub Fütterung() Console.WriteLine("--Futter--") : RaiseEvent Futter(0) Console.WriteLine("--Wasser--") : RaiseEvent Wasser(0) End Sub Sub Wolf(ByRef no As Short) Handles Me.Futter no += 1S : Console.WriteLine("Wolf: {0}", no) End Sub Sub Uhu(ByRef no As Short) Handles Me.Futter no += 1S : Console.WriteLine("Uhu: {0}", no) End Sub Sub Reh(ByRef no As Short) no += 1S : Console.WriteLine("Reh: {0}", no) End Sub Shared Sub Test() Dim park As Tierpark = New Tierpark() AddHandler park.Futter, AddressOf park.Reh AddHandler park.Futter, AddressOf park.Wolf AddHandler park.Wasser, AddressOf park.Reh AddHandler park.Wasser, AddressOf park.Uhu AddHandler park.Wasser, AddressOf park.Reh AddHandler park.Wasser, AddressOf park.Wolf park.Fütterung() Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe für das Futter zeigt, dass die dynamisch gebundenen Methoden nach den statischen an die Reihe kommen. Beim Wasser ist zu sehen, dass die Reihenfolge der dynamisch gebundenen Methoden erhalten bleibt – im Gegensatz zu den statisch gebundenen.
--Futter-- Uhu: 1 Wolf: 2 Reh: 3 Wolf: 4 --Wasser-- Reh: 1 Uhu: 2 Reh: 3 Wolf: 4
Automatisch erzeugter Code
Durch die Definition eines Ereignisses werden vom Compiler einige zusätzliche Klassenmitglieder erzeugt (siehe Abschnitt 3.10.1, »Event und RaiseEvent«).
Hinweis |
Das im Ereignis implizit enthaltene Objekt kann nicht vom Garbage Collector aufgeräumt werden. Daher ist eine Deregistrierung von nicht mehr benötigten Ereignissen empfehlenswert. |
Diese werden bei der dynamischen Methodenbindung implizit genutzt. Die Deklarationen
AddHandler Ereignis, Delegate RemoveHandler Ereignis, Delegate
münden in den folgenden Aufrufen (beide Public):
Public add_Ereignis(Delegate) Public remove_Ereignis(Delegate)
Obwohl die Methoden öffentlich sind, weist er Compiler Zugriffsversuche zurück. Da sie Teil der Klasse sind, dürfen Sie nicht gleichnamige Klassenmitglieder definieren. Selbst eine Überladung ist nicht gestattet.
3.10.4 Benutzerdefinierte Ereignisse: Custom Event
Es gibt Situationen, in denen Sie mehr Kontrolle über Ereignisse benötigen. Zum Beispiel möchten Sie bei einer Alarmanlage jede Auslösung mitprotokollieren, auch wenn keine Methode zur Behandlung registriert ist und der Alarm »still« ist. Vielleicht möchten Sie auch die Reihenfolge von statisch an ein Ereignis gebundenen Methoden bestimmen. Diese Maßnahmen sollten unabhängig davon sein, wie ein Ereignis verwendet wird, um sicherzustellen, dass sie immer greifen. In der Realität ist es ja auch nicht sinnvoll, wenn eine Alarmanlage nur bei »richtiger« Verwendung durch den Einbrecher anschlägt.
Zur Definition eines benutzerdefinierten Ereignisses wird in Visual Basic die folgende Syntax verwendet (optionale Teile sind in eckige Klammern gesetzt und kursive Teile müssen Sie Ihren Bedürfnissen anpassen):
Custom Event Ereignis As Delegate-Typ AddHandler(ByVal handler As Delegate-Typ) [<Anweisungen>] End AddHandler RemoveHandler(ByVal handler As Delegate-Typ) [<Anweisungen>] End RemoveHandler RaiseEvent(([<Parameter>]) [<Anweisungen>] End RaiseEvent End Event |
Die Verwendung des Ereignisses ändert sich nicht: Die Operatoren AddHandler, RemoveHandler und RaiseEvent rufen implizit die korrespondierenden Methoden des benutzerdefinierten Ereignisses auf.
Hinweis |
Die Methode RaiseEvent muss entweder zum Delegate kompatible Parametertypen oder gar keine Parameter haben. Die Parameter des RaiseEvent-Operators müssen zur RaiseEvent-Methode passen (beide typkompatibel oder beide leer). |
Als Beispiel dient die Verwaltung einer Warteschlange, die auf ein Druckereignis wartet. Als Erstes definieren wir dazu einen Druckjob. Er besteht aus einer Priorität und einem Text. Außerdem enthält er eine Methode zum Drucken, die das Druck-Ereignis behandeln wird.
'...\Klassendesign\Ereignisse\Benutzerdefiniert.vb |
Option Strict On Namespace Klassendesign Class Job Friend ReadOnly prio As Integer Friend ReadOnly text As String Sub New(ByVal prior As Integer, ByVal text As String) Me.prio = prior : Me.text = text End Sub Sub Druck(ByVal no As Integer) Console.WriteLine("Druck {0} mit Prio {1}: {2}", no, prio, text) End Sub End Class ... End Namespace
Die Warteschlange fügt einen Druckjob abhängig von seiner Priorität in eine Liste ein. Dies ist mit Standardereignissen nicht möglich, da wir dort keinen Einfluss auf die Reihenfolge nehmen können. Die vielen Typumwandlungen mit CType könnten mit generischen Datentypen vermieden werden (siehe Abschnitt 4.4, »Generisches«). Um nicht vorzugreifen, wurde hier auf ihren Einsatz verzichtet. In RaiseEvent wird die behandelnde Methode nach deren Aufruf mit RemoveHandler aus der Liste entfernt, und der Druckjob wird gelöscht. Auch dies ist bei Standardereignissen nicht möglich. Es wird in der Schleife immer auf das erste Listenelement zugegriffen, um die Reihenfolge der Druckjobs beizubehalten.
'...\Klassendesign\Ereignisse\Benutzerdefiniert.vb |
Option Strict On Namespace Klassendesign ... Class Warteschlange Delegate Sub Typ(ByVal no As Integer) Dim liste As ArrayList = New ArrayList() Custom Event Druck As typ AddHandler(ByVal meth As Typ) Dim prio As Integer = CType(meth.Target, Job).prio For no As Integer = 0 To liste.Count If no = liste.Count Then : liste.Add(meth) : Else Dim pr As Integer = _ CType(CType(liste(no), typ).Target, Job).prio If prio > pr Then liste.Insert(no, meth) : Exit For End If End If Next End AddHandler RemoveHandler(ByVal meth As Typ) liste.Remove(meth) End RemoveHandler RaiseEvent(ByVal no As Integer) For nr As Integer = 0 To liste.Count – 1 CType(liste(0), typ)(no) RemoveHandler Druck, CType(liste(0), typ) Next End RaiseEvent End Event Sub drucken(ByVal no As Integer) RaiseEvent Druck(no) 'implizit Druck.RaiseEvent(no) End Sub End Class ... End Namespace
Schließlich werden einige Druckjobs mittels AddHandler in die Warteschlange eingefügt und es wird ein Druck-Ereignis mittels der Methode drucken() ausgelöst.
'...\Klassendesign\Ereignisse\Benutzerdefiniert.vb |
Option Strict On
Namespace Klassendesign
...
Module Benutzerdefiniert
Sub Test()
Dim w As Warteschlange = New Warteschlange()
'implizit w.Druck.AddHandler(new Job(...))
AddHandler w.Druck, AddressOf (New Job(2,"2-1")).Druck
AddHandler w.Druck, AddressOf (New Job(3,"3-1")).Druck
AddHandler w.Druck, AddressOf (New Job(1,"1-1")).Druck
AddHandler w.Druck, AddressOf (New Job(2,"2-2")).Druck
AddHandler w.Druck, AddressOf (New Job(1,"1-2")).Druck
w.drucken(7)
Console.ReadLine()
End Sub
End Module
End Namespace
Die Ausgabe zeigt die korrekte Sortierung nach Priorität:
Druck 7 mit Prio 3: 3-1 Druck 7 mit Prio 2: 2-1 Druck 7 mit Prio 2: 2-2 Druck 7 mit Prio 1: 1-1 Druck 7 mit Prio 1: 1-2
Automatisch erzeugter Code
Durch die Deklaration eines benutzerdefinierten Ereignisses werden vom Compiler automatisch folgende Methoden der Klasse hinzugefügt:
Public Sub add_Ereignis(ByVal handler As Delegate-Typ) Public Sub remove_Ereignis(ByVal handler As Delegate-Typ) Private Sub raise_Ereignis()
Eine Benutzung im eigenen Quelltext gestattet der Compiler nicht, er beschwert sich aber, wenn Sie versuchen, ein gleichnamiges Klassenmitglied zu definieren.
3.10.5 Umwandlungen
Da ein Ereignis implizit ein Delegate darstellt, finden auch die gleichen Umwandlungen statt (siehe Abschnitt 3.9.6, »Umwandlungen«, unter anderem kommt es zu impliziten Typkonvertierungen). Auch bei Ereignissen gibt es eine »unglückliche« Stolperfalle, auf die hier besonders hingewiesen wird. Wird ein Ereignis mit Parametern vereinbart, darf die behandelnde Methode ohne Parameter sein. In allen anderen Fällen muss der Aufruf mit korrekter Parameterzahl und Parametertypen erfolgen. Da die Methode keine Parameter hat, werden die übergebenen Parameter schlicht ignoriert. Das folgende Codefragment zeigt eine solche verwirrende Situation:
'...\Klassendesign\Ereignisse\Parameterlos.vb |
Option Strict On Namespace Klassendesign Class VergesseneParameter Event Ereignis(ByVal no As Integer) Sub Auslösung() RaiseEvent Ereignis(-3) End Sub Sub Reaktion() Handles Me.Ereignis Console.WriteLine("es geht auch ohne Parameter") End Sub Shared Sub Test() Dim ver As VergesseneParameter = New VergesseneParameter() ver.Auslösung() Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe bestätigt, dass die behandelnde Methode trotz fehlender Parameter aufgerufen wurde:
es geht auch ohne Parameter
3.10.6 Grafikbibliothek: Größenänderungen von Rechtecken überwachen
In diesem Abschnitt erweitern wir die in Abschnitt 3.1.12, »Grafikbibliothek: Beispiel für Kapitel 3 und 4«, eingeführte und zuletzt in Abschnitt 3.9.7, »Grafikbibliothek: Vergleich von Rechtecken«, erweiterte Grafikanwendung. Als Erstes wird das Ereignis BreiteÄndern definiert und bei einer Änderung der Eigenschaft Breite über RaiseEvent ausgelöst. Zur statischen Bindung einer Methode an das Ereignis wird ein Rechteck mittels WithEvents als ereignisbehaftet gekennzeichnet, durch den dritten Konstruktorparameter belegt und mit einer Handles-Klausel an die Methode BreiteGeändert() gebunden.
'...\Klassendesign\Graphik\Ereignisse.vb |
Option Explicit On Namespace Klassendesign Partial Public Class Rechteck Event BreiteÄndern As Vergleich Property Breite() As Double Get Return b End Get Set(ByVal b As Double) Me.b = b RaiseEvent BreiteÄndern(Me) End Set End Property Private WithEvents recht As Rechteck Sub New(ByVal a As Double, ByVal b As Double, ByVal recht As Rechteck) Me.New(a, b) Me.recht = recht End Sub Sub BreiteGeändert(ByVal recht As Rechteck) Handles recht.BreiteÄndern Vgl(recht, Function(r As Rechteck) r.b, "Breite") End Sub End Class End Namespace
Zum Test wird ein Rechteck akteur erzeugt, dessen Breite geändert wird. Als Ereignishandler werden mittels AddHandler die Rechtecke aufmerksam und schläfrig verwendet, wobei aufmerksam durch den übergebenen akteur und die Handles-Klausel des letzten Codefragments einen weiteren (statischen) Ereignishandler bekommt.
'...\Klassendesign\Zeichner\Ereignisse.vb |
Option Explicit On Namespace Klassendesign Partial Class Zeichner Sub Ereignisse() Dim akteur As New Rechteck(7, 2) Dim aufmerksam As Rechteck = New Rechteck(6, 7, akteur) Dim schläfrig As Rechteck = New Rechteck(12, 4) AddHandler akteur.BreiteÄndern, AddressOf aufmerksam.VglFläche AddHandler akteur.BreiteÄndern, AddressOf schläfrig.VglUmfang akteur.Breite = 7 End Sub End Class End Namespace
Zur Kontrolle sehen Sie hier die Ausgabe der Methode Ereignisse():
7x7 hat mehr Breite als 6x7 7x7 hat mehr Fläche als 6x7 7x7 hat weniger Umfang als 12x4
Die nächste Erweiterung erfolgt in Abschnitt 3.11.6, »Grafikbibliothek: Addition und Umwandlung von Rechtecken«.
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.