Mir fällt gelegentlich auf, das Begriffe im Zusammenhang mit Events falsch verwendet werden, vermutlich weil sie nicht richtig bekannt sind. Auch Galileio-Book kann da nicht helfen - die behandeln das Thema nur unzureichend, und wissen selbst nicht recht Bescheid - zB verkaufen sie den codeseitigen Aufruf einer Ereignis-Methode als Ereignis-Kette, und eine Endlos-Rekursion als Endlos-Ereignis-Kette.
Event-orientiertes versus prozedurales Denken
Ja, mitte Events ist das eine ganze Denkweise. Gelegentlich finden sich Fragen in Foren, wie:
"ich habe 5 Items, und möchte jede Sekunde eins davon meiner Listbox zufügen - wie muss ich die For-Schleife machen?"
Der Fehler liegt hier im prozeduralen Denken: Klar, der Computer arbeitet grundsätzlich prozedural, und eine For-Schleife hat in NullkommaNixMinusEins die 5 Items eingefügt - aber jeweils die Sekunde warten - grad das kannernich.
Natürlich kann man ihn ausbremsen, mit Thread.Sleep(1000) - aber das versetzt die ganze App ins Koma - da wird dann auch die Anzeige der neuen Items nicht mehr geupdated.
Nein - man musses ereignisorientiert angehen: Jede Sekunde ein Ereignis - füge ein Item zu, und weitermachen, mit - nichts.
Das ist nämlich, was ein Computer am meisten macht: Nichts.
Warten, dass sich ein Ereignis ereignet. Das ist, wie eine Anwendung interaktiv ist, und endlos Interaktions-Möglichkeiten anbietet: Grundsätzlich nichts tun - aber wenn der User Button1 klickst, oder in Textbox1 schreibt, oder, oder, oder... - dann tut sie schnell mal was, und dann wieder nichts.
Diese beiden Denkweisen sind übrigens nicht verfeindet, sondern Player desselben Teams: Die Grundstruktur ist ereignisorientiert und im Wartezustand. Ereignet sich ein Ereignis, wird dieses schnellstmöglich prozedural abgearbeit,denn der Prozessor will seine Ruhe haben damit die App gleich wieder bereitsteht für User-Interaktionen.
Begrifflichkeiten
Bei einem Event gibt es immer einen Sender, der das Event versendet, und keinen, einen oder mehrere Empfänger, die es abonnieren - (oder auch wieder abbestellen).
Der Sender "verschickt" sein Event, indem er alle Methoden aufruft, die sein Event abonnieren. Diese Methoden heißen Handler-Methoden, EventHandler oder einfach Handler. Mit diesen Methoden behandelt der Empfänger das abonnierte Event.
Ich stelle das deshalb so deutlich heraus, weil viele Leuts haben keine klare Unterscheidung von Event (ein Aufruf-Mechanismus im Sender) und EventHandler (eine Methode im Empfänger)
Dieses Muster: "mehrere Empfänger können Nachrichten eines Senders abonnieren oder abbestellen" heißt in der Informatik: Observer-Pattern
Der erste Kontakt
Die Begriffs-Verwirrung rührt sicher auch daher, dass man als Anfänger über lange Zeit Events ausschließlich von der Handler-Seite her kennt. Das hier:
ist vmtl. der erste Code, den ein Anfänger schreibt - (tatsächlich schreibters ja nicht selbst, sondern lässtes sich generieren, etwa, indem er im Form-Designer einen Button doppelklickst).
Erst relativ spät kommt man in Verlegenheit, auch die Sender-Seite implementieren zu müssen. Jo, und wenn man gar nicht weiß, dass das ühaupt geht, kommts u.U. zu ziemlich schlimmen Frickel-Lösungen - ich komme noch dazu.
Observer-Pattern: Handles-Klausel versus AddHandler / RemoveHandler
Was uns der Designer hingeneriert, ist überaus praktisch und zuverlässig, aber es verschleiert ein bischen den dahinterstehenden Observer-Pattern: Denn im Hintergrund erstellt die Handles-Klausel eine unsichtbare Property, in deren Setter mit
Bei dynamisch erzeugten Objekten geht das statische "Handles" schon rein syntaktisch nicht.
Ereigniskette
Eine Ereigniskette hat man am Hals, wenn der EventHandler Code ausführt, welcher das Ereignis selbst wieder auslöst. So eine Ereigniskette unterhält uns mit allerlei originellem Verhalten: miese Performance, erstaunliche Berechnungen "Nanu - wo issn der Wert her??", Geflacker, Rumhopsen von Controls, und im Extremfall Endlos-Ereigniskette - hängt die App.
Wie gesagt: Hier kann man das Ereignis einfach abbestellen, solange man den sensiblen Code ausführt.
Beispiel zu AddHandler / RemoveHandler: Richtextbox, die nur bestimmte Zeichen zulassen soll
Wann immer ein unerlaubtes Zeichen sich anfindet, soll der Text auf den vorherigen Zustand zurückgesetzt werden, inklusive der Selection. Deshalb muß die Überprüfung im _SelectionChanged-EventHandler stattfinden, und ggfs. müssen Text und Selection restauriert werden (was bei der Selection eine Ereigniskette verursachen würde):
(die SelStore-Klasse ist klar? Damit kann man die Selection einer Richtextbox speichern und wieder restaurieren)
Schwierigkeiten mit der Signatur
Obiges Beispiel sieht schmissig aus, aber wenn man selbst mal versucht, ein Ereignis mit AddHandler zu abonnieren, baut man schnell folgenden Fehler, nachdem man die HandlerMethode hingeschreiben hat, und will abonnieren:
WtF...?
Ja, ist schon absolut genau und richtig, wasser sagt - blos: wovon redet er? Was ist eine Signatur, was ein Delegat, und wieso kommter jetzt mit diesem EventHandler-Dingsbums an?
Also langsam: Wir wollen ja ein Event abonnieren. Das heißt, unsere Handler-Methode soll aufgerufen werden durch den Event-Mechanismus. Aber das Event kann ja nicht jede x-beliebige Methode aufrufen, sondern nur eine, die auch passt.
Logisch: Eine Methode, die einen Integer erwartet, kann nicht ein Event abonnieren, welches einen String verschickt. (Jaja - für Option Strict Off - Progger ist das nicht logisch - aber die brauchen noch grundlegendere Tutorials ;))
Also Anzahl, Reihenfolge und Datentyp von Argumenten und Rückgabetyp - das ist die Signatur einer Methode.
Der Verschick-Mechanismus des Events benutzt einen Delegaten: Das ist eine Variable, deren Datentyp eben eine Signatur ist, und der man Methoden zufügen kann (so verrückt das auch klingt). Mehrere sogar (noch verrückter), und wenn man diesen Delegaten dann aufruft, werden dadurch alle geaddeten Methoden aufgerufen. (In C hat man Function-Pointer für sowas - aber die sind total unsicher, weil können überall hinzeigen, und der Absturz bleibt nur aus, wenn sie tatsächlich auf eine passende Funktions-Addresse zeigen.)
Jdfs, was uns der Compiler hier haarklein (und unverständlich) erklärt, ist, dass der Datentyp des Delegaten, der das Event verschicken will, nicht zur Signatur der Methode passt, die es abonnieren soll.
Er führt uns sogar nochmal die Signatur unserer eigenen Handler-Methode vor Augen, und stellt sie der Definition des Event-Delegaten gegenüber - gugge nochma:
So, dassis jetzt geklärt, und wie mans richtig macht, ist ja weiter oben gezeigt.
Zur Übung bringe ich nochn paar Beispiele von Signaturen - weil dassis fast ebenso wichtig zu verstehen, wie das Konzept der Datentypen überhaupt:bereits gesagt: erwartet ein Object und ein EventArgs, kein Rückgabewert - nächstes Beispiel:
Diese Methode erwartet zwei Doubles und gibt einen zurück
Diese erwartet eine List(Of String) und einen String - gibt einen Integer zurück.
Abschweifung: die generischen Delegaten Action(Of...) und Func(Of...) für jede Signatur
Im Framework sind tausende verschiedener Delegaten definiert - eigentlich ganz überflüssigerweise, denn seit Einführung der Generika kann man (fast) jede Signatur als Action oder Func definieren: Der obige EventHandler Button1_Click etwa wäre ebensogut als Action(Of Object, EventArgs) definiert, Multiplicate ist eine Func(Of Double, Double, Double) und FindIndex eine Func(Of List(Of String), String, Integer).
Ist das Prinzip klar geworden? Bei Action(Of...) definieren die TypParameter die Methoden-Argument-Typen von Subs, bei Func(Of...) dito, nur gilt der letzte TypParameter für den Rückgabewert.
So, jetzt wisst ihr das auch, und dass man Signaturen am kürzesten als Action oder Func ausdrückt.
Noch abgeschweifter
Ihr habt euch bei der Betrachtung dieser Zeile sicher gefragt:
"WtF! wie kann man in einer Code-Zeile alle Zeichen von .Text gegen alle Zeichen des allowed-Strings abgleichen?" - habt ihr nicht gefragt? - egal - erklär ich trotzdem, weil das geht mit Func und Signaturen:
Also im CodeEditor per Rechtsklick auf "All" - "zu Definition gehen" - zeigt uns die .All()-Signatur im ObjectBrowser:
.Text.All(predicate As Func(Of Char, Boolean)) - als Argument erwartet wird also ein "Bedingungs"-Delegat ("predicate"), der für jedes Zeichen im Text aufgerufen wird, nämlich, um das Zeichen mit True oder False zu "bewerten" (daher Boolean als Rückgabewert der predicate-Func).
Nur wenn alle Zeichen des Textes mit True bewertet werden, gibt .All() sinnigerweise auch True zurück. Auf diese Weise drückt .All() aus, ob eine Bedingung für alle Zeichen zutrifft, und zwar ohne die Bedingung zu kennen - die kann übergeben werden - als Parameter!
Jo, und Function(c) allowed.Contains(c) ist so ein Bedingungs-Delegat-Parameter (notiert als anonyme Methode): Die Bedingung für ein Zeichen c ist True, wenn es im allowed-String enthalten ist.
Und dieses prüft .All() gegen jedes im Text enthaltene Zeichen ab.
Naja - für wen das jetzt zu schnell war weckts vlt. Interesse, VB2008 richtig zu lernen, u.U. sogar dieses Buch zu lesen
Ereignisse selbst schreiben
Neues Code-Beispiel: Angenommen wir wollen ein UserControl, welches alle KnownColors in einer Listbox auflistet, sodass man auswählen kann, ohne den WinForms-ColorDialog jedesmal öffnen und schließen zu müssen:
Wie kriegen wirs nun hin, dass zB ein Button auf dem Form1 immer die im uclColorSelector angewählte Farbe hat?
Event-orientiertes versus prozedurales Denken
Ja, mitte Events ist das eine ganze Denkweise. Gelegentlich finden sich Fragen in Foren, wie:
"ich habe 5 Items, und möchte jede Sekunde eins davon meiner Listbox zufügen - wie muss ich die For-Schleife machen?"
Der Fehler liegt hier im prozeduralen Denken: Klar, der Computer arbeitet grundsätzlich prozedural, und eine For-Schleife hat in NullkommaNixMinusEins die 5 Items eingefügt - aber jeweils die Sekunde warten - grad das kannernich.
Natürlich kann man ihn ausbremsen, mit Thread.Sleep(1000) - aber das versetzt die ganze App ins Koma - da wird dann auch die Anzeige der neuen Items nicht mehr geupdated.
Nein - man musses ereignisorientiert angehen: Jede Sekunde ein Ereignis - füge ein Item zu, und weitermachen, mit - nichts.
Das ist nämlich, was ein Computer am meisten macht: Nichts.
Warten, dass sich ein Ereignis ereignet. Das ist, wie eine Anwendung interaktiv ist, und endlos Interaktions-Möglichkeiten anbietet: Grundsätzlich nichts tun - aber wenn der User Button1 klickst, oder in Textbox1 schreibt, oder, oder, oder... - dann tut sie schnell mal was, und dann wieder nichts.
Diese beiden Denkweisen sind übrigens nicht verfeindet, sondern Player desselben Teams: Die Grundstruktur ist ereignisorientiert und im Wartezustand. Ereignet sich ein Ereignis, wird dieses schnellstmöglich prozedural abgearbeit,
Begrifflichkeiten
Bei einem Event gibt es immer einen Sender, der das Event versendet, und keinen, einen oder mehrere Empfänger, die es abonnieren - (oder auch wieder abbestellen).
Der Sender "verschickt" sein Event, indem er alle Methoden aufruft, die sein Event abonnieren. Diese Methoden heißen Handler-Methoden, EventHandler oder einfach Handler. Mit diesen Methoden behandelt der Empfänger das abonnierte Event.
Ich stelle das deshalb so deutlich heraus, weil viele Leuts haben keine klare Unterscheidung von Event (ein Aufruf-Mechanismus im Sender) und EventHandler (eine Methode im Empfänger)
Dieses Muster: "mehrere Empfänger können Nachrichten eines Senders abonnieren oder abbestellen" heißt in der Informatik: Observer-Pattern
Der erste Kontakt
Die Begriffs-Verwirrung rührt sicher auch daher, dass man als Anfänger über lange Zeit Events ausschließlich von der Handler-Seite her kennt. Das hier:
Erst relativ spät kommt man in Verlegenheit, auch die Sender-Seite implementieren zu müssen. Jo, und wenn man gar nicht weiß, dass das ühaupt geht, kommts u.U. zu ziemlich schlimmen Frickel-Lösungen - ich komme noch dazu.
Observer-Pattern: Handles-Klausel versus AddHandler / RemoveHandler
Was uns der Designer hingeneriert, ist überaus praktisch und zuverlässig, aber es verschleiert ein bischen den dahinterstehenden Observer-Pattern: Denn im Hintergrund erstellt die Handles-Klausel eine unsichtbare Property, in deren Setter mit
AddHandler
/RemoveHandler
das angegebene Event registriert/deregistriert wird.Bei dynamisch erzeugten Objekten geht das statische "Handles" schon rein syntaktisch nicht.
Ereigniskette
Eine Ereigniskette hat man am Hals, wenn der EventHandler Code ausführt, welcher das Ereignis selbst wieder auslöst. So eine Ereigniskette unterhält uns mit allerlei originellem Verhalten: miese Performance, erstaunliche Berechnungen "Nanu - wo issn der Wert her??", Geflacker, Rumhopsen von Controls, und im Extremfall Endlos-Ereigniskette - hängt die App.
Wie gesagt: Hier kann man das Ereignis einfach abbestellen, solange man den sensiblen Code ausführt.
Beispiel zu AddHandler / RemoveHandler: Richtextbox, die nur bestimmte Zeichen zulassen soll
Wann immer ein unerlaubtes Zeichen sich anfindet, soll der Text auf den vorherigen Zustand zurückgesetzt werden, inklusive der Selection. Deshalb muß die Überprüfung im _SelectionChanged-EventHandler stattfinden, und ggfs. müssen Text und Selection restauriert werden (was bei der Selection eine Ereigniskette verursachen würde):
VB.NET-Quellcode
- Public Class frmEreignisDemo
- Private _SelStore As New SelStore
- Private Sub frmEreignisDemo_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
- RichTextBox1.Text = "0"
- _SelStore.Store(RichTextBox1)
- AddHandler RichTextBox1.SelectionChanged, AddressOf RichTextBox1_SelectionChanged
- End Sub
- Private Sub RichTextBox1_SelectionChanged(ByVal sender As Object, ByVal e As EventArgs)
- Dim allowed = "0123456789"
- With RichTextBox1
- If .Text.All(Function(c) allowed.Contains(c)) Then
- _SelStore.Store(RichTextBox1)
- Else
- RemoveHandler .SelectionChanged, AddressOf RichTextBox1_SelectionChanged
- _SelStore.Restore(RichTextBox1)
- AddHandler .SelectionChanged, AddressOf RichTextBox1_SelectionChanged
- End If
- End With
- End Sub
- End Class
- Public Class SelStore
- Private Text As String
- Private Start, Length As Integer
- Public Sub Store(ByVal tb As RichTextBox)
- Text = tb.Text
- Start = tb.SelectionStart
- Length = tb.SelectionLength
- End Sub
- Public Sub Restore(ByVal tb As RichTextBox)
- tb.Text = Text
- tb.Select(Start, Length)
- End Sub
- End Class
Schwierigkeiten mit der Signatur
Obiges Beispiel sieht schmissig aus, aber wenn man selbst mal versucht, ein Ereignis mit AddHandler zu abonnieren, baut man schnell folgenden Fehler, nachdem man die HandlerMethode hingeschreiben hat, und will abonnieren:
WtF...?
Ja, ist schon absolut genau und richtig, wasser sagt - blos: wovon redet er? Was ist eine Signatur, was ein Delegat, und wieso kommter jetzt mit diesem EventHandler-Dingsbums an?
Also langsam: Wir wollen ja ein Event abonnieren. Das heißt, unsere Handler-Methode soll aufgerufen werden durch den Event-Mechanismus. Aber das Event kann ja nicht jede x-beliebige Methode aufrufen, sondern nur eine, die auch passt.
Logisch: Eine Methode, die einen Integer erwartet, kann nicht ein Event abonnieren, welches einen String verschickt. (Jaja - für Option Strict Off - Progger ist das nicht logisch - aber die brauchen noch grundlegendere Tutorials ;))
Also Anzahl, Reihenfolge und Datentyp von Argumenten und Rückgabetyp - das ist die Signatur einer Methode.
Der Verschick-Mechanismus des Events benutzt einen Delegaten: Das ist eine Variable, deren Datentyp eben eine Signatur ist, und der man Methoden zufügen kann (so verrückt das auch klingt). Mehrere sogar (noch verrückter), und wenn man diesen Delegaten dann aufruft, werden dadurch alle geaddeten Methoden aufgerufen. (In C hat man Function-Pointer für sowas - aber die sind total unsicher, weil können überall hinzeigen, und der Absturz bleibt nur aus, wenn sie tatsächlich auf eine passende Funktions-Addresse zeigen.)
Jdfs, was uns der Compiler hier haarklein (und unverständlich) erklärt, ist, dass der Datentyp des Delegaten, der das Event verschicken will, nicht zur Signatur der Methode passt, die es abonnieren soll.
Er führt uns sogar nochmal die Signatur unserer eigenen Handler-Methode vor Augen, und stellt sie der Definition des Event-Delegaten gegenüber - gugge nochma:
So, dassis jetzt geklärt, und wie mans richtig macht, ist ja weiter oben gezeigt.
Zur Übung bringe ich nochn paar Beispiele von Signaturen - weil dassis fast ebenso wichtig zu verstehen, wie das Konzept der Datentypen überhaupt:bereits gesagt: erwartet ein Object und ein EventArgs, kein Rückgabewert - nächstes Beispiel:
Abschweifung: die generischen Delegaten Action(Of...) und Func(Of...) für jede Signatur
Im Framework sind tausende verschiedener Delegaten definiert - eigentlich ganz überflüssigerweise, denn seit Einführung der Generika kann man (fast) jede Signatur als Action oder Func definieren: Der obige EventHandler Button1_Click etwa wäre ebensogut als Action(Of Object, EventArgs) definiert, Multiplicate ist eine Func(Of Double, Double, Double) und FindIndex eine Func(Of List(Of String), String, Integer).
Ist das Prinzip klar geworden? Bei Action(Of...) definieren die TypParameter die Methoden-Argument-Typen von Subs, bei Func(Of...) dito, nur gilt der letzte TypParameter für den Rückgabewert.
So, jetzt wisst ihr das auch, und dass man Signaturen am kürzesten als Action oder Func ausdrückt.
Noch abgeschweifter
Ihr habt euch bei der Betrachtung dieser Zeile sicher gefragt:
"WtF! wie kann man in einer Code-Zeile alle Zeichen von .Text gegen alle Zeichen des allowed-Strings abgleichen?" - habt ihr nicht gefragt? - egal - erklär ich trotzdem, weil das geht mit Func und Signaturen:
Also im CodeEditor per Rechtsklick auf "All" - "zu Definition gehen" - zeigt uns die .All()-Signatur im ObjectBrowser:
.Text.All(predicate As Func(Of Char, Boolean)) - als Argument erwartet wird also ein "Bedingungs"-Delegat ("predicate"), der für jedes Zeichen im Text aufgerufen wird, nämlich, um das Zeichen mit True oder False zu "bewerten" (daher Boolean als Rückgabewert der predicate-Func).
Nur wenn alle Zeichen des Textes mit True bewertet werden, gibt .All() sinnigerweise auch True zurück. Auf diese Weise drückt .All() aus, ob eine Bedingung für alle Zeichen zutrifft, und zwar ohne die Bedingung zu kennen - die kann übergeben werden - als Parameter!
Jo, und Function(c) allowed.Contains(c) ist so ein Bedingungs-Delegat-Parameter (notiert als anonyme Methode): Die Bedingung für ein Zeichen c ist True, wenn es im allowed-String enthalten ist.
Und dieses prüft .All() gegen jedes im Text enthaltene Zeichen ab.
Naja - für wen das jetzt zu schnell war weckts vlt. Interesse, VB2008 richtig zu lernen, u.U. sogar dieses Buch zu lesen
Ereignisse selbst schreiben
Neues Code-Beispiel: Angenommen wir wollen ein UserControl, welches alle KnownColors in einer Listbox auflistet, sodass man auswählen kann, ohne den WinForms-ColorDialog jedesmal öffnen und schließen zu müssen:
VB.NET-Quellcode
- Public Class uclColorSelector
- Private _Brushes As New List(Of SolidBrush)
- Private Sub uclColorSelector_Load(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Load
- For Each pinf As PropertyInfo In GetType(Brushes).GetProperties().Skip(1)
- _Brushes.Add(DirectCast(pinf.GetValue(Nothing, Nothing), SolidBrush))
- Next
- ListBox1.DataSource = _Brushes
- End Sub
- Private Sub ListBox1_DrawItem(ByVal sender As Object, ByVal e As DrawItemEventArgs) Handles ListBox1.DrawItem
- e.DrawBackground()
- Dim brsh = _Brushes(e.Index)
- With e.Bounds
- e.Graphics.FillRectangle(brsh, .X + 2, .Y + 3, .Width - 5, .Height - 6)
- End With
- With brsh.Color
- Dim col = If(CInt(.R) + .B + .G > 400, Color.Black, Color.White)
- TextRenderer.DrawText(e.Graphics, .Name, Font, e.Bounds, col)
- End With
- End Sub
- End Class
Wie kriegen wirs nun hin, dass zB ein Button auf dem Form1 immer die im uclColorSelector angewählte Farbe hat?
Dieser Beitrag wurde bereits 4 mal editiert, zuletzt von „ErfinderDesRades“ ()