Tutorial zu Events - Pre-Release

Es gibt 23 Antworten in diesem Thema. Der letzte Beitrag () ist von hal2000.

    Tutorial zu Events - Pre-Release

    Hi!

    Wichtiger Edit: Aufgrund der Diskussion dieses ersten Entwurfs habe ich das Teil neu überarbeitet. Ich finde diese Ur-Version (und die Diskussion dazu) immer noch interessant, und die neuere Version weiter unten hat den Schwerpunkt bisserl anders.
    Muß man sich halt überlegen, ob man nur die neuere Version wissen will, oder sich den Thread komplett antun möchte.
    [/Wichtiger Edit]

    Ich denke mir grade ein Tut zum Thema Events aus, und dachte, vlt. fallen euch noch Verbesserungen ein. Und da dachte ich, man könnte besser erstmal im Vorfeld dazu herum-diskutieren, als dassichs gleich als fixnfertig bei die Tuts einstelle. Also los gehts:



    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.

    Jut. Was ist also ein Ereignis?
    Jedem bekannt: In der Intellisense bekommt man die Ereignisse immer mit sonem kleinen Blitz-Icon angezeigt - sinnigerweise, denn Ereignisse können aus heiterem Himmel auftreten, anders als Methoden, die von irgendwoher aufgerufen werden.

    Im Grunde aber sind Ereignisse nix anderes als besondere Variablen, nämlich Delegaten.
    Wenn man eine Delegat-Variable mit dem Schlüsselwort "Event" deklariert, wird sie zu einem Event, und das bedeutet: Sie ist fast ReadOnly - man kann ihren Wert nur noch mit genau 3 Anweisungen verändern: AddHandler, RemoveHandler und die Handles-Klausel (eine Art initialisierendes AddHandler). (Und sie bekommt den Blitz-Icon ;)).

    Was genau ist ein Delegat?
    Delegaten sind Variablen, deren Datentyp nicht Double, Integer oder sonstwas ist, sondern die Signatur einer Methode.
    Aha.
    Und was bitte ist "Signatur einer Methode"?
    So bezeichnet man ihre Aufrufe-Syntax - Beispiel:

    VB.NET-Quellcode

    1. Private Sub Button1_Click( _
    2. ByVal sender As Object, ByVal e As EventArgs)
    Obige Methode erwartet 2 Argumente: Das erste vom Typ Object (kann also alles sein), das zweite vom Typ EventArgs. Die Methode gibt keinen Wert zurück.

    nächstes Beispiel:

    VB.NET-Quellcode

    1. Public Function Multiplicate( _
    2. ByVal arg1 As Double, ByVal arg2 As Double) As Double
    Diese Methode erwartet zwei Doubles und gibt einen zurück

    VB.NET-Quellcode

    1. Private Function FindIndex( _
    2. ByVal strings As List(Of String), ByVal pattern As String) As Integer
    Diese erwartet eine List(Of String) und einen String - und gibt einen Integer zurück.

    Wie kann sowas komplexes - mehrere ArgumentTypen und ggfs. ein Rückgabewert - selbst wieder Datentyp einer Variablen sein?
    Jo, is eben so - da gewöhnt man sich dran ;)
    Einen Delegaten für obige Function FindIndex() könnte man etwa so deklarieren:

    VB.NET-Quellcode

    1. Dim IndexFinder As Func(Of List(Of String), String, Integer)
    Ists erkennbar, wie Func(Of List(Of String), String, Integer) genau die Signatur von FindIndex wiederspiegelt?
    Der letzte TypParameter - Integer - bezeichnet bei Func(Of ...) den Rückgabewert.

    Für Subs - Methoden ohne Rückgabewert - muß man halt Action-Delegaten definieren:

    VB.NET-Quellcode

    1. Dim buttonKlickser As Action(Of Object, EventArgs)

    Func(Of ...) und Action(Of ...) sind generische Delegat-Typen, mit denen man praktisch jede Signatur spezifizieren kann.

    Schön - bleiben 2 Fragen offen: a) was hat man davon? b) was hat das mit Events zu tun?

    Zu a): Ein Delegat kann eine Methode aufrufen, wenn deren Signatur passt.

    VB.NET-Quellcode

    1. Dim buttonKlickser As Action(Of Object, EventArgs) = AddressOf Button1_Click
    2. buttonKlickser(Nothing, EventArgs.Empty) ' rufe Button1_Click() auf

    Ein Delegat kann sogar mehrere Methoden (fast) gleichzeitig aufrufen.

    VB.NET-Quellcode

    1. ' Kombiniere buttonKlickser mit einer 2. Button_Click-Methode
    2. buttonKlickser = DirectCast(Delegate.Combine( _
    3. buttonKlickser, AddressOf Button2_Click), _
    4. Action(Of Object, EventArgs))
    5. ' rufe Button1_Click() **und** Button2_Click() auf
    6. buttonKlickser(Nothing, EventArgs.Empty)

    Und man kann die Methoden auch wieder entfernen:

    VB.NET-Quellcode

    1. buttonKlickser = DirectCast(Delegate.Remove( _
    2. buttonKlickser, AddressOf Button1_Click), _
    3. Action(Of Object, EventArgs))
    4. buttonKlickser(Nothing, EventArgs.Empty) ' rufe nurnoch Button2_Click() auf

    Und das ist jetzt wirklich toll, denn hier haben wir ein fabelhaftes Instrument zur Implementation des Observer-Patterns.
    Das bedeutet: Ein Objekt versendet Benachrichtigungen, und andere Objekte können diese Nachrichten abonnieren oder auch nicht.
    Zum Beispiel ein Button namens Button1 versendet die Click - Nachricht, und mit folgender Methode kann man dieses Ereignis behandeln:

    VB.NET-Quellcode

    1. Private Sub Button1_Click( _
    2. ByVal sender As Object, ByVal e As EventArgs) Handles Button1.Click
    3. '...
    4. End Sub

    Hoho - da habich euch aber schön an der Nase herum geführt: vonne Ereignisse über Delegaten, Signaturen, Observer-Pattern und wieder zurück zu Ereignissen, wie sie der blutigste Anfänger schon kennt, noch bevor sein "Hallo Welt" - Programm überhaupt das Licht eben dieser erblickt hat ;).
    Aber tatsächlich war das gar keine Abschweifung, weil - ich wiederhole: Events sind Delegaten. Sie unterliegen nur zusätzlichen Regeln, nämlich dass sie Readonly sind und dasses statt der wirklich abscheulichen Verknüpfungs-Syntax (das mittm DirectCast(Delegate.Combine(....),...)) super-praktische Schlüsselworte gibt: AddHandler, RemoveHandler, Handles-Klausel (und niemals nich vergesse diese hübsche Blitz-Icone ;) ) .
    Und Ereignis-Behandlung ist die einfachste und vollkommenste Implementation des Observer-Patterns, die man sich denken kann - so einfach, dass nur die wenigsten überhaupt wissen, was für einen fabelhaften Pattern sie da grade implementieren ;)

    Die Begriffe aber nochmal genau genommen
    Also .Click ist das Ereignis, und das ist Member der Button-Klasse. Und Button1_Click() ist kein Event, sondern eine EventHandler-Methode (wenn sie tatsächlich das .Click-Event abonniert).
    Beim Button ist .Click fertig eingebaut - da kommt man nicht ran. Aber wir können Handler-Methoden für dieses Event schreiben, oder uns sogar generieren lassen (etwa wenn wir im Form-Designer auf einen Button doppelklicken).

    Mal was praktisches: Eine Richtextbox, die nur bestimmte Zeichen zulässt.
    Das Problem ist, dasses nicht reicht, Tasten-Ereignisse zu überwachen, denn es kann ja auch per Drag & Drop Text eingeben werden, oder nach Guttenberg (copy & paste ;)).
    Also müssen wir das TextChanged behandeln, mit folgender Tücke: Wird beim Auftreten eines unerwünschten Zeichens der Text korrigiert, so löst die Korrektur gleich wieder ein TextChanged-Ereignis aus (das ist nämlich eine EreignisKette - nicht wenn man codeseitig einen EventHandler aufruft).
    Eine weitere, noch tückischere Tücke: Das TextChanged ist als Event unzureichend, denn wir wollen auch die Selection wieder herstellen, wie sie war, bevor das böse Zeichen eingegeben wurde. Also nehmen wir das SelectionChanged-Event - dort kann man ebensogut den neuen Text abrufen, und man ist zusätzlich immer über die aktuelle Selection im Bilde (und es gibt ebenso eine Ereigniskette, wenn man innerhalb des SelectionChanged-Events die Selection changed ;)).
    Ja, die klugen Dinge über Observer-Pattern, und dass man Events abonnieren kann und wieder abbestellen - das wirkt sich hier sehr segensreich aus - ich glaub, ich muß den Code gar nicht weiter kommentieren:

    VB.NET-Quellcode

    1. Public Class frmEreignisDemo
    2. Private _SelStore As New SelStore
    3. Private Sub frmEreignisDemo_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
    4. RichTextBox1.Text = "0"
    5. _SelStore.Store(RichTextBox1)
    6. AddHandler RichTextBox1.SelectionChanged, AddressOf RichTextBox1_SelectionChanged
    7. End Sub
    8. Private Sub RichTextBox1_SelectionChanged(ByVal sender As Object, ByVal e As EventArgs)
    9. Dim allowed = "0123456789"
    10. With RichTextBox1
    11. If .Text.All(Function(c) allowed.Contains(c)) Then
    12. _SelStore.Store(RichTextBox1)
    13. Else
    14. RemoveHandler .SelectionChanged, AddressOf RichTextBox1_SelectionChanged
    15. _SelStore.Restore(RichTextBox1)
    16. AddHandler .SelectionChanged, AddressOf RichTextBox1_SelectionChanged
    17. End If
    18. End With
    19. End Sub
    20. End Class
    21. Public Class SelStore
    22. Private Text As String
    23. Private Start, Length As Integer
    24. Public Sub Store(ByVal tb As RichTextBox)
    25. Text = tb.Text
    26. Start = tb.SelectionStart
    27. Length = tb.SelectionLength
    28. End Sub
    29. Public Sub Restore(ByVal tb As RichTextBox)
    30. tb.Text = Text
    31. tb.Select(Start, Length)
    32. End Sub
    33. End Class
    (die SelStore-Klasse ist klar - ja? Eine Klasse, die die Selection einer Richtextbox speichern und wieder restaurieren kann)

    Einzig diese (eigentlich ganz unschuldig und plausibel aussehende) Zeile

    VB.NET-Quellcode

    1. If .Text.All(Function(c) allowed.Contains(c)) Then
    wird manchem ein Rätsel bleiben: wie zum Kuckuck kann man in einer Zeile Code alle Zeichen von .Text gegen alle Zeichen des allowed-Strings abgleichen?
    Hmm - das wäre ein anderes Tutorial, nämlich zu den Themen Linq, Interfaces, Extensions, Generica, Delegaten (schon wieder!), anonymen Methoden und Type-Inference.
    Naja - kurz das mit die Delegaten: .Text.All(predicate As Func(Of Char, Boolean)) erwartet einen "Bedingungs"-Delegaten, den es für jedes Zeichen aufruft, um das Zeichen mit True oder False zu "bewerten" (daher Boolean als Rückgabewert der predicate-Signatur). 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 (gewissermaßen ohne die Bedingung selbst zu kennen - listig, nicht?).
    Jo, und Function(c) allowed.Contains(c) ist so ein Bedingungs-Delegat (allerdings auch noch als anonyme Methode notiert): Die Bedingung ist, dass ein Zeichen c im allowed-String enthalten ist, und das prüft .All() ab, gegen jedes im Text enthaltene Zeichen.
    Naja - für wen das jetzt zu schnell war machts hoffentlich Appetit darauf, VisualBasic.Net 2008 richtig zu lernen, u.U. auch mal dieses Buch zu lesen ;)
    Dateien
    • EventDemo.zip

      (17,45 kB, 98 mal heruntergeladen, zuletzt: )

    Dieser Beitrag wurde bereits 7 mal editiert, zuletzt von „ErfinderDesRades“ ()

    Ereignisse selbst schreiben
    Ganz anneres Beispiel: Angenommen der WinForms-ColorDialog gefällt uns nicht, und wir wollen ein UserControl, welches alle KnownColors auflistet, sodass man jederzeit eine auswählen kann, ohne einen Extra-Dialog öffnen zu müssen:

    VB.NET-Quellcode

    1. Public Class uclColorSelector
    2. Private _Brushes As New List(Of SolidBrush)
    3. Private Sub uclColorSelector_Load(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Load
    4. For Each pinf As PropertyInfo In GetType(Brushes).GetProperties().Skip(1)
    5. _Brushes.Add(DirectCast(pinf.GetValue(Nothing, Nothing), SolidBrush))
    6. Next
    7. ListBox1.DataSource = _Brushes
    8. End Sub
    9. Private Sub ListBox1_DrawItem(ByVal sender As Object, ByVal e As DrawItemEventArgs) Handles ListBox1.DrawItem
    10. e.DrawBackground()
    11. Dim brsh = _Brushes(e.Index)
    12. With e.Bounds
    13. e.Graphics.FillRectangle(brsh, .X + 2, .Y + 3, .Width - 5, .Height - 6)
    14. End With
    15. With brsh.Color
    16. Dim col = If(CInt(.R) + .B + .G > 400, Color.Black, Color.White)
    17. TextRenderer.DrawText(e.Graphics, .Name, Font, e.Bounds, col)
    18. End With
    19. End Sub
    20. End Class

    Wie kriegen wirs nun hin, dass zB ein Button auf dem Form1 immer die im uclColorSelector angewählte Farbe hat?

    Wie mans nicht hinkriegen sollte:
    1. Quick & Dirty

      VB.NET-Quellcode

      1. Private Sub ListBox1_SelectedIndexChanged( _
      2. ByVal sender As Object, ByVal e As EventArgs) Handles ListBox1.SelectedIndexChanged
      3. Form1.Button1.BackColor = _Brushes(ListBox1.SelectedIndex).Color
      4. End Sub
      Diesen Leuten muß man vergeben, "denn sie wissen nicht, was sie tun" ;)
      Form1 ist gar kein Objekt, sondern ist eine Klasse, also der Bauplan eines Objektes. Trotzdem geht das, und daran ist Microsoft schuld, dasses die VB-Progger so verblödet, dass diese alsbald nicht mehr zwischen Bauplan und Gebautem unterscheiden können (und die c#-ler können sich über uns lustig machen :cursing: )

    2. gut gemeint, aber eigentlich nur umständlicher ist folgende "Konsequenz" aus 1):
      "Mach eine Public Property vom Typ Form1 in den ColorSelector, initialisiere die, und dann kann der ColorSelector deren Button1 bunt machen"
      Tja, das wird zu Fehlern im Designer führen, denn wenn man so einen uclColorSelector aufs Form zieht, dann ist die form1-Variable erstmal uninitialisiert, der uclColorSelector will aber gleich den Button bunt machen, aber der ist ja noch gar nicht da.
    Nein, beide Varianten sind im Grunde Spaghetti-Code, zu enge Kopplung, und verstoßen gegen das Kapselungs-Prinzip, nach dem es nur dem Form zusteht, seine Buttons zu colorieren - nicht iwelchen draufgezogenen UserControls.

    Die zu enge Kopplung wird unangenehm, wenn man den ColorSelector mal in eine andere Solution einbauen möchte - das geht nämlich gar nicht: Weil er ist davon abhängig (gekoppelt), dass es ein Form1 gibt, und zwar eines mit einem Button1 drauf. Architektonischer Mist - siehe auch VeryBasics - Kommunikation der Objekte.

    Korrekte Lösung - Observer-Pattern
    Der ColorSelector soll logisch eine Nachricht verschicken, und wer die erhält, soll selber zusehen, wie er seine Buttons bunt kriegt.

    VB.NET-Quellcode

    1. <DefaultEvent("SelectedColorChanged")> _
    2. Public Class uclColorSelector
    3. Private _Brushes As New List(Of SolidBrush)
    4. Public Event SelectedColorChanged As EventHandler
    5. Private Sub ListBox1_SelectedIndexChanged( _
    6. ByVal sender As Object, ByVal e As EventArgs) Handles ListBox1.SelectedIndexChanged
    7. RaiseEvent SelectedColorChanged(Me, EventArgs.Empty)
    8. End Sub
    9. Public ReadOnly Property SelectedColor As Color
    10. Get
    11. Return _Brushes(ListBox1.SelectedIndex).Color
    12. End Get
    13. End Property
    (Das <DefaultEvent()> - Attribut veranlasst übrigens den Form-Designer, beim Doppelklick auf meinen uclColorSelector gleich den korrekten EventHandler in den Code-Editor zu generieren - wie man es auch kennt von Button.Click und Konsorten).
    Ansonsten haben wir hier genau, was oben versprochen wurde: Eine Public Delegat-Variable - SelectedColorChanged, die mittels des Event-Schlüsselwortes in ein Event verzaubert wurde. Und übrigens das 4. Event-spezifische Schlüsselwort: RaiseEvent. Weil den in ein Event verzauberten Delegaten kann man nun nicht mehr so simpel aufrufen wie weiter oben buttonKlickser(Nothing, EventArgs.Empty), sondern nurnoch mittm RaiseEvent-Schlüsselwort.
    Aber war buttonKlickser nicht deklariert As Action(Of Object, EventArgs)? Hier steht jetzt aber As EventHandler!
    Ja, weil ist dasselbe ;)
    EventHandler ist ein vorgefertigter DelegatTyp, der ebenfalls die Signatur Action(Of Object, EventArgs) spezifiziert.
    Früher (vor 2005) hatte man für jeden Delegaten erst noch einen extra Delegat-Typen deklarieren müssen, in dieser Art:

    VB.NET-Quellcode

    1. Public Delegate Sub EventHandler(ByVal sender As Object, ByVal e As EventArgs)
    Heute kann man fast jede mögliche Signatur mit den generischen Delegat-Typen Action(Of...) und Func(Of...) nach Bedarf modellieren. Aber das Framework ist weiterhin voll von vorgefertigten ungenerischen Delegat-Typen, und im Falle von Events spricht nix dagegen, die auch zu benutzen.

    Design-Richtlinie Event-Signatur
    Theoretisch kann man für seine Events absolut beliebige Signaturen festlegen - man macht sich damit aber sogleich des Dilettierens verdächtig, weil im Framework ist die folgende Richtlinie ausnahmslos bei allen(!!) Events umgesetzt, und ich habe noch nie erlebt, dass ein Nicht-Dilettant sich unterstanden hätte, von dieser Richtlinie abzuweichen (Ausnahme: EBC - event-driven Architecture - aber das ist wirklich ein anderes Tutorial)
    Also: Die Signatur eines Events ist immer Sub EventName(sender As Object, e As EventArgs).
    Anhand des senders kann in Handlern, die Events mehrerer Objekte behandeln, der jeweilige Sender identifiziert werden. Ich nutze das gerne zB für Menüs:

    VB.NET-Quellcode

    1. Private Sub MenuStrip_MenuClicked(ByVal Sender As Object, ByVal e As EventArgs) _
    2. Handles SaveToolStripMenuItem.Click, ReloadToolStripMenuItem.Click, _
    3. TestToolStripMenuItem.Click
    4. Select Case True
    5. Case Sender Is SaveToolStripMenuItem
    6. 'save
    7. Case Sender Is ReloadToolStripMenuItem
    8. 'load
    9. Case Sender Is TestToolStripMenuItem
    10. 'test
    11. End Select
    12. End Sub


    Eigene EventArgs
    EventArgs ist eine wirklich eigentümliche Klasse, mit überhaupt keinen Properties. Die Klasse kann aber beerbt und mit Properties und allem Möglichen erweitert werden.
    Häufige Verwendung findet etwa das CancelEventArgs, mit seiner Property e.Cancel, die man auf True setzen kann, um beispielsweise in einem Form_Closing-EventHandler das Closing zu canceln.
    Im Beispiel mittm ColorSelector könnte man es nun praktisch finden, beim Versenden der ColorChanged - Nachricht gleich die neue Color mitzuschicken.
    Dazu muß man aber erstmal so ein EventArgs erfinden, mit dem man eine Color auch herumschicken kann:

    VB.NET-Quellcode

    1. Public Class ColorChangedEventArgs : Inherits EventArgs
    2. Public ReadOnly Color As Color
    3. Public Sub New(ByVal color As Color)
    4. Me.Color = color
    5. End Sub
    6. End Class

    Und dann folglich:

    VB.NET-Quellcode

    1. Public Event ColorChanged As EventHandler(Of ColorChangedEventArgs)
    2. Private Sub ListBox_SelectedIndexChanged( _
    3. ByVal sender As Object, ByVal e As EventArgs) Handles ListBox1.SelectedIndexChanged
    4. RaiseEvent ColorChanged(Me, _
    5. New ColorChangedEventArgs(_Brushes(ListBox1.SelectedIndex).Color))
    6. End Sub

    Dann kommt das im Form so raus:

    VB.NET-Quellcode

    1. Private Sub UclColorSelector1_ColorChanged(ByVal sender As Object, _
    2. ByVal e As uclColorSelector.ColorChangedEventArgs) _
    3. Handles UclColorSelector1.ColorChanged
    4. Me.Button1.BackColor = e.Color
    5. End Sub


    Vererbungslehre
    Ereignisse sind wirklich beschränkte Delegaten: Readonly, man kann sie nicht als Methoden-Argumente weiterreichen - ja man kann sie nicht einmal von außerhalb der Klasse auslösen

    VB.NET-Quellcode

    1. RaiseEvent(Me.UclColorSelector1.SelectedColorChanged(Nothing, EventArgs.Empty))
    gehtnich.

    Nicht einmal ein Erbe von uclColorSelector könnte RaiseEvent in Anschlag bringen.
    Diese Einschränkungen sind durchaus sinnvoll, und verhindern, dass Schindluder getrieben wird. Will man eine Event-bestückte Klasse beerben und der abgeleiteten Klasse gestatten, auch das Event auszulösen, muß man halt codetechnisch was dafür tun - sprich: eine OnEvent-Methode bereitstellen. Die gesamte Choose - vonne EventArgs-Deklaration, über die OnEvent-Methode bis zum Auslösen - mag sich dann so darstellen:

    VB.NET-Quellcode

    1. Public Class uclColorSelector
    2. Public Class ColorChangedEventArgs : Inherits EventArgs
    3. Public ReadOnly Color As Color
    4. Public Sub New(ByVal color As Color)
    5. Me.Color = color
    6. End Sub
    7. End Class
    8. Public Event ColorChanged As EventHandler(Of ColorChangedEventArgs)
    9. Protected Sub OnColorChanged(ByVal e As ColorChangedEventArgs)
    10. RaiseEvent ColorChanged(Me, e)
    11. End Sub
    12. Private _Brushes As New List(Of SolidBrush)
    13. Private Sub ListBox_SelectedIndexChanged( _
    14. ByVal sender As Object, ByVal e As EventArgs) Handles ListBox1.SelectedIndexChanged
    15. OnColorChanged(New ColorChangedEventArgs(_Brushes(ListBox1.SelectedIndex).Color))
    16. End Sub

    Jetzt kann auch ein Erbe das Event raushauen:

    VB.NET-Quellcode

    1. Public Class uclColorSelectorEx : Inherits uclColorSelector
    2. Private Sub AnythingHappens()
    3. MyBase.OnColorChanged(New ColorChangedEventArgs(Color.Blue))
    4. End Sub
    5. End Class
    Beachte auch die Unterscheidung zwischen dem ungenerischen DelegatTypen EventHandler und dem generischen DelegatTypen EventHandler(Of ColorChangedEventArgs)

    BenamungsRichtlinie
    Obiges ist ein voll ausprogrammiertes Beispiel eines benutzerdefinierten Events, und folgt den MS-Richtlinien, was Benamung und Signaturen betrifft, sowohl die Signatur des Events als auch die der OnEvent-Methode, und auch die zusammenhängende Benamung all dieser Elemente.

    In der Praxis muß man nur selten diesen Aufwand treiben. Tatsächlich ist ersteres - das SelectedColorChanged-Event ohne selbstdefinierte EventArgs auch noch flexibler - die SelectedColor-Property braucht man ja eh - und es unterstützt so nebenbei Databinding - wenn man sich an die zusammenhängende Benamung der Property Xy und des XyChanged-Events hält.
    Aber das ist auch ein anderes Tutorial

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „ErfinderDesRades“ () aus folgendem Grund: wichtiger Grund

    @ ErfinderDesRades



    Im Grunde aber sind Ereignisse nix anderes als besondere Variablen, nämlich Delegaten.

    ...

    Events sind Delegaten


    Hier würde ich erstmal gerne wiedersprechen. ^^

    Events sind definitiv keine Delegaten, auch wenn sie sich in manchen Dingen ähneln. Hier die Hauptunterschiede:

    - Events verweisen nicht auf andere Funktionen
    - Events wissen selber nichts von Ihren "Target"

    Im Gegensatz dazu muss ein Delegate immer auf eine andere Funktion (oder einem anderen Delegaten) verweisen und kennt damit diese.

    Das einfachste Event-Construct wäre folgendes:

    VB.NET-Quellcode

    1. Public Class1
    2. Public Event MyTestEvent(ByVal strTest As String)
    3. Public Sub TestingEvent()
    4. RaiseEvent MyTestEvent("Ich bin ein Event")
    5. End Sub
    6. End Class


    Das funktioniert nun jederzeit und kann so oft man mag ausgeführt werden, bzw. das Event kann so oft man mag gefeuert werden. Dem Event ist es dabei völlig egal ob es abgehört wird oder nicht und vor allem ist es dem Event egal von wem es abgehört wird.

    Das funktioniert so mit einem Delegaten nicht, der muss irgendwo bevor er angesprochen wird ein Ziel zur Ansprache zugewiesen bekommen. Und da liegt meiner Meinung nach der gewaltige Unterschied zwischen Events und Delegaten.

    Just my 5 Cents. ;)

    Gruß

    Rainer
    richtig weil jedes Event dazu da ist auf eine Methode zu verweisen hat es auch einen Delegaten hat(kann ja auch auf mehrere Methoden verweisen), somit ist das evtl. nicht ganz korrekt formuliert, aber richtig gemeint :P
    @picoflop: So ist es auch mit anderen dingen:
    Mit den neueren IDEs:

    VB.NET-Quellcode

    1. Property Bla() As String
    2. 'Erstellt automatisch:
    3. Private _Bla As String

    Und außerdem gibt es versteckt(merkt man mit Reflection), für Properties - genauso wie bei C++ - Getter und Setter Funktionen...

    Aber ich glaube weicht langsam ab^^
    Ich wollte auch mal ne total überflüssige Signatur:
    ---Leer---

    raist10 schrieb:

    - Events verweisen nicht auf andere Funktionen
    - Events wissen selber nichts von Ihren "Target"
    Im Gegensatz dazu muss ein Delegate immer auf eine andere Funktion (oder einem anderen Delegaten) verweisen und kennt damit diese.
    Klar verweist ein Event auf eine andere Funktion. Man muß sie dem Event halt zuweisen - in VB mittels AddHandler. Ein Button.Click - Event verweist auf mw. Button1_Click(sender As Object, e As EventArgs).
    In c# wird die Identität von Event und Delegat noch deutlicher - da gibt es überhaupt keine syntaktischen Unterschiede. Allein durch das Schlüsselwort "event" werden die spezifischen Zugriffsbeschränkungen definiert, sodaß unten die auskommentierte Zeile failt

    Quellcode

    1. using System;
    2. public class Class2 {
    3. public event Action<string> MyTestEvent;
    4. public void TestingEvent() {
    5. MyTestEvent("Ich bin ein Event");
    6. }
    7. }
    8. public class Class3 {
    9. public Action<string> MyTestDelegat;
    10. public void TestingEvent() {
    11. MyTestDelegat("Ich bin ein Event");
    12. }
    13. }
    14. class EventTester {
    15. private Class2 _C2 = new Class2();
    16. private Class3 _C3 = new Class3();
    17. private void HandleEvent(string arg) {
    18. System.Windows.Forms.MessageBox.Show(arg);
    19. }
    20. public void SubSribe() {
    21. _C2.MyTestEvent += HandleEvent;
    22. _C3.MyTestDelegat += HandleEvent;
    23. }
    24. public void UnSubSribe() {
    25. _C2.MyTestEvent -= HandleEvent;
    26. _C3.MyTestDelegat -= HandleEvent;
    27. }
    28. public void RaiseFromOuter() {
    29. _C3.MyTestDelegat("from outer");
    30. //_C2.MyTestEvent("from outer"); // fail
    31. }
    32. }

    Die VB-Schlüsselworte AddHandler und RemoveHandler sind nur syntaktischer Zucker, über den auf die Delegate.Combine/Remove - Methoden zugegriffen wird.
    RaiseEvent hat noch etwas zusätzliches eingebaut, damit ein unabonniertes Event nicht zum Fehler führt - was dahintersteckt kann man an c# ziemlich klar erkennen - die müssen das nämlich selbst coden:

    Quellcode

    1. public class Class2 {
    2. public event Action<string> MyTestEvent;
    3. public void TestingEvent() {
    4. // event auslösen, abgesichert für den Fall, dasses ühaupt nicht abonniert ist
    5. if(MyTestEvent!=null) MyTestEvent("Ich bin ein Event");
    6. }
    7. }

    jvbsl schrieb:

    Schon klar, aber genau darum ging es ja ebenfalls bei den Events mit den Delegaten

    Gibt schon nen Unterschied ...

    VB.NET-Quellcode

    1. Public Class EventSample
    2. Public Event foo()
    3. Public Sub bar()
    4. Dim d1 As [Delegate] = fooEvent
    5. Dim d2 As [Delegate] = AddressOf foo
    6. Dim o As Object = AddressOf foo
    7. End Sub
    8. End Class

    Die erste Anweisung geht. Die beiden anderen sind ein Fehler. Bei den "Kurzproperties" kann man hingegen das ganze auch komplett selber schreiben.

    Braucht man zb für:
    [VB.NET] CRC32 Prüfsumme berechnen - Geschwindigkeits Problem


    Oder hab ich was übersehen und das ganze geht auch ohne den Autodelegaten?
    Die erste Anweisung geht. Die beiden anderen sind ein Fehler.

    Logisch, AddressOf foo ist nämlich nicht eindeutig, da du ja schließlich vom Prinzip her merhrere Delegaten hinzufügen kannst, oder hab ich was falsch verstanden? :P
    Ich wollte auch mal ne total überflüssige Signatur:
    ---Leer---

    Klar verweist ein Event auf eine andere Funktion. Man muß sie dem Event halt zuweisen - in VB mittels AddHandler. Ein Button.Click - Event verweist auf mw. Button1_Click(sender As Object, e As EventArgs).


    Klar meinst Du schon das Richtige, aber erklärst es ein wenig "unglücklich".

    Du weisst nie einem Event eine Funktion zu, sondern Du weisst einer Funktion zu auf ein Event zu reagieren. Das Event selber hat nach wie vor keinerlei Ahnung davon von wem es abgehört wird.

    Du kannst einen Button.click ja auch ausführen ohne einer Funktion mittels AddHandler die Abhörung des Events zu zu weisen. Dann klickt man sich halt dumm und dämlich und feuert ein Button.Click-Event nach dem anderen bloss es gibt keine Funktion die zu hört und reagieren könnte.

    Erst durch die Hinzufügung von Handles wird die Funktion angewiesen auf das Event zu reagieren. Dabei ist es allerdings richtig das die zu hörende Funktion die gleiche Signatur verwenden muss wie das feuernde Event besitzt.

    Delegaten und Events funktionieren zwar ähnlich sind aber quasi zwei Seiten der Medaille:

    - ein Delegat hat keine Ahnung von wem er aufgerufen wird und kann von überall her aufgerufen werden, muss aber dafür Bescheid wissen auf welche Funktion er zeigen soll

    - ein Event hat keine Ahnung welche Funktion reagiert wenn es feuert, "weiss" aber dafür woher es aufgerufen wird (kann ja nur aus der eigenen Klasse heraus gefeuert werden)

    Technisch gesprochen ist ein Delegat ein Zeiger auf eine Funktion.

    Ein Event hat aber keinen Zeiger und wird sowas auch nie besitzen. Wie NET durch interne Vorgänge ein Event abfängt wird und intern alle Funktionen ansteuert die einen Handler auf das Event besitzen ist die andere Thematik (die ich auch gar nicht genau beantworten könnte ^^).

    Guck Dir mal Events in VBA/VB6 an ... da sieht man recht deutlich was ich meine. Weder VBA noch VB6 können eine Delegaten handlen, aber gefeuerte Events abzuhören beherrschen beide problemlos.

    Das Event-Handling in C# anders funktioniert ist gar keine Frage, aber was wie in C# funktioniert kannst Du nicht 1 zu 1 nach VB.NET übersetzen. ;)

    Gruß

    Rainer

    raist10 schrieb:

    Du weisst nie einem Event eine Funktion zu, sondern Du weisst einer Funktion zu auf ein Event zu reagieren. Das Event selber hat nach wie vor keinerlei Ahnung davon von wem es abgehört wird.

    nein, das ist definitiv falsch.

    Dem Event wird der EventHandler zugewiesen, nicht dem EventHandler das Event - AddHandler addet einen EventHandler, sonst hiesse es AddEvent.
    Ein EventHandler ist eine Methode, die aufgerufen wird - so etwas hat gar keinen Status, in dem es etwa zugewiesene Events speichern könnte.

    Auch hier zeigt c# wieder klarer, was abgeht:

    Quellcode

    1. public class Class2 {
    2. public event Action<string> MyTestEvent;
    3. public void TestingEvent() {
    4. // event auslösen, abgesichert für den Fall, dasses ühaupt nicht abonniert ist
    5. if(MyTestEvent!=null) MyTestEvent("Ich bin ein Event");
    6. }
    7. public Delegate[] GetInvocationList() {
    8. return MyTestEvent.GetInvocationList();
    9. }
    10. }

    Das Event MyTestEvent verfügt über eine Methode GetInvocationList - kann also seine Aufrufeliste zur Verfügung stellen - und GetInvocationList() findet sich im ObjectBrowser als Member der abstrakten (MustInherit) Klasse System.Delegate.

    Daran gefällt mir auch, zu sehen, dass das auch nur mit Wasser gekocht ist: "Lauschen" ist eigentlich etwas, was ein Computer gar nicht kann - es muß immer eine Quelle von Aktivität geben - bei Events eben den Event-Sender.
    So sieht man, dass hinter der ereignisorientierten Programmierung auch nix als die gute alte prozedurale Logik steckt: Der Sender ruft brav einen Abonnenten nach dem andern auf - "Ereignis aus heiterem Himmel" - in Wirklichkeit gibts das nicht.

    picoflop schrieb:

    btw: Hat eigentlich schon jemand den versteckten automatischen Delegaten erwähnt?

    Wenn
    public Event Foo()

    dann existiert auch der Delegate
    FooEvent

    Jo - stimmt. Und belegt ein stückweit meine Behauptung, dass Events Delegaten sind.
    Nungut, was man da als Event zu sehen bekommt ist eine art Wrapper-Property, die dann die zulässigen Operationen, die hinter AddHandler, RemoveHandler, Handles-Klausel und RaiseEvent stehen, an den Delegaten durchreicht.
    @ ErfinderDesRades

    Ich glaube wir reden hier aneinander vorbei. ^^

    Natürlich kannst Du über spezielle Methoden einen Event wie einem Delegaten ein Target angeben was bei Event Feuerung angesprungen wird. Aber das ist eine optionale Methode und nicht das grundsätzliche Event-Handling.

    Guckst Du hier:

    VB.NET-Quellcode

    1. Public Class FireEvents
    2. Public Event MyEvent()
    3. Public Sub RaiseEvents()
    4. RaiseEvent MyEvent()
    5. End Sub
    6. End Class
    7. Public Class ListenEvents
    8. Public Sub New(ByVal fire As FireEvents)
    9. m_FireEvents = fire
    10. End Sub
    11. Private WithEvents m_FireEvents As FireEvents
    12. Private Sub m_FireEvents_MyEvent() Handles m_FireEvents.MyEvent
    13. MessageBox.Show("Es wurde das Event gefeuert")
    14. End Sub
    15. End Class
    16. Module TestEvents
    17. Public Sub TestingEvents()
    18. Dim fire As New FireEvents
    19. Dim listen As New ListenEvents(fire)
    20. fire.RaiseEvents()
    21. End Sub
    22. End Module


    Siehst Du da irgendwo einen Delegaten? Das Event wird gefeuert dadurch das der Listener auf die gleiche Objekt-Instanz eingestellt ist bekommt er mit das hier eine Ereignismeldung ausgelöst wurde. Das Abonnement läuft hier nur zwischen den Objektinstanzen ... durch das Schlüsselwort WithEvents abonniert die Listener-Class alle Ereignisse die von der Fire-Class ausgelöst werden. Durch die Handles-Anweisung wird dann von der Funktion direkt das Ereignis MyEvent abonniert.

    Das gleiche System wird genauso auch in den Windows-Forms verwendet um Control-Events zu handeln. Jeder Windows-Form hält "hinten dran" eine Private Referenz mit WithEvents auf jedes Control in Ihr. Deswegen kannst Du auch so problemlos die Control-Events abhören. Guck Dir einfach den Code an der hinter einer Form im Designer steckt. ;)

    Zumindest in Visual Basic ist das die Grundlage des Event-Handlings seit eh und je. Hauptsächlich weil es VB vor NET keine Deleghaten (also Methodenzeiger) kannte und auch nicht verarbeiten konnte. Erst mit dem NET-Framework kennt VB Methodenzeiger und hat daher diese Möglichkeit analog zu C# ins Event-Handling eingebaut. Das trotzdem die alte Funktionsweise beibehalten wurde, kannst Du darüber prüfen das ein Event das mit AddHandler einen Methodenzeiger bekommen hat, trotzdem noch gleichzeitig wie in der obigen Methode abgehört werden kann.

    Geht das in C# das eine Methode auf ein Event reagiern kann obwohl das Event nirgendwo eine Delegat-Zuweisung auf die Methode bekommt? ;)

    Gruß

    Rainer
    Der Unterschied zwischen Event und Delegat wird deutlich, wenn ihr euch ein "Custom Event" anseht. Das sieht dann fast wie eine Property aus, nur mit den "Methoden" AddHandler, RemoveHandler und RaiseEvent. Der Aufruf "AddHandler Object.EventName, AddressOf TheHandler" lautet in Langform "AddHandler Object.EventName, New EventNameEvent(AddressOf TheHandler)". An die AddHandler-Prozedur wird demnach eine Delegateninstanz übergeben. Diese Methode verschmilzt standardmäßig (also ohne "Custom") alle übergebenen Delegateninstanzen zu einer einzigen mittels Delegate.Combine, sodass alle Handler in RaiseEvent aufgerufen werden. Ein Event ist sozusagen ein Container für Delegateninstanzen, die jeweils einen Zeiger auf einen Eventhandler darstellen.

    Edit: Nach genauerem Lesen des Tutorials steht genau das auch drin, was ich gerade geschrieben habe. Aber dass Events Delegaten sind, ist definitiv falsch - siehe oben.

    Edit2: Einige Leute verrennen sich in diesem Thread auch zwischen statischer (WithEvents und Handles) und dynamischer (Add- und RemoveHandler) Bindung. Bitte nochmal im MSDN nachlesen - bei der statischen Variante wird auch nur die dynamische benutzt, nur eben verdeckt, implizit und automatisch. Deshalb sieht man dort auch keinen Delegaten. Das Prinzip ist aber dasselbe.

    OffTopic: Mal sehen, wann der erste mit IL-Code als Beweis seiner These ankommt :P /OffTopic
    Gruß
    hal2000

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „hal2000“ ()

    hatte ich bereits vor, aber bin bisher noch nicht dazu gekommen etwas zusammenzustellen und zu kompilieren und ob es überhaupt noch nötig ist, ist fraglich: Meins wäre ebenfalls(nur wesentlich anders formuliert) darüber gewesen, dass Events den Delegaten enthalten und es ohne Delegate kein Event gibt...
    Ich wollte auch mal ne total überflüssige Signatur:
    ---Leer---
    Jo, über picoFlops Hinweis habich nochma gehirnt, und glaube, folgende Aussage zur Beziehung Event <-> Delegat machen zu können:
    Ein (nicht custom-) Event kapselt einen verborgenen Delegaten in ebenfalls verborgene Zugriffs-Methoden, die sich nämlich hinter den Schlüsselworten AddHandler, RemoveHandler, WithEvents, Handles und RaiseEvent verbergen.
    Diese Schlüsselworte vereinfachen das abonnieren und abbestellen nach dem Observer-Pattern und schützen den Delegaten vor allzu kreativem Umgang (etwa auf Nothing setzen, überschreiben oder sowas)
    CodeSample dazu:

    VB.NET-Quellcode

    1. Public Class EventSample
    2. Public Event foo(ByVal txt As String)
    3. Public Sub RaiseTwoTimes()
    4. RaiseEvent foo("Aufruf per RaiseEvent")
    5. fooEvent("direkter Aufruf des verborgenen Delegaten")
    6. End Sub
    7. End Class
    8. Public Module Program
    9. Private WithEvents _ESample As New EventSample
    10. Public Sub Main()
    11. _ESample.RaiseTwoTimes()
    12. End Sub
    13. Private Sub _ESample_foo(ByVal txt As String) Handles _ESample.foo
    14. Console.WriteLine(txt)
    15. End Sub
    gecodet ist ein ganz klassisches Event, inklusive Abonnement. Aber in RaiseTwoTimes() wird zusätzlich zur normalen Auslösung per RaiseEvent das Event nochmal quasi "als Hack" ausgelöst, indem nämlich der verborgene Delegat direkt aufgerufen wird.


    Auch eine WithEvents-Variable stellt eine verborgene Property dar - ungefähr die folgende:

    VB.NET-Quellcode

    1. Public Module Program
    2. Private _ESample2 As EventSample
    3. 'PseudoProperty, ungefähr einer verborgenen WithEvents-Property entsprechend
    4. 'Die Identifizierung der Abonnenten mit Handles-Klausel
    5. 'entspricht nur sehr sehr grob - ich hab k.A., wie das wirklich funzt.
    6. 'Beachte aber den ebenfalls verborgenen Delegat-Typ:
    7. '"EventSample.fooEventHandler" - der Compiler akzeptiert das wirklich so.
    8. Private _CollectionOfAbonnents As IEnumerable(Of [Delegate])
    9. Public Property ESample2() As EventSample
    10. Get
    11. Return _ESample2
    12. End Get
    13. Set(ByVal value As EventSample)
    14. If _ESample2 IsNot Nothing Then
    15. For Each mtd As EventSample.fooEventHandler In _CollectionOfAbonnents
    16. RemoveHandler _ESample2.foo, mtd
    17. Next
    18. End If
    19. _ESample2 = value
    20. For Each mtd As EventSample.fooEventHandler In _CollectionOfAbonnents
    21. AddHandler _ESample2.foo, mtd
    22. Next
    23. End Set
    24. End Property

    Diese verborgene Property ist ziemlich praktisch: Indem man eine Withevents-Variable auf Nothing setzt, löscht man alle Abonnements aller ihrer Events, und indem man ihr eine Instanz zuweist, stöpselt man gleich alle per Handles spezifizierten Methoden an.
    Das kann man überprüfen, indem man ein Custom-Event proggt, und einen Haltpunkt auf die Custom AddHandler-Methode setzt.
    Da eine WithEvents-Variable in Wirklichkeit eine verborgene Property ist, mit Setter und Getter, kann man auch keine Readonly WithEvent - Variablen deklarieren - schomal aufgefallen?

    Ja, da mussich nochmal weiter-hirnen, wie und was davon ich ins Tut reinmach. Von praktischem Belang ist eigentlich nur, dass man lernt, Event und Handler ausnander zu halten, und was Signaturen sind (und die fabelhaften Action / Func(Of ...) ), und dass hinter Events dieselben Delegaten stecken, die einem auch noch an so einigen anneren Ecken übern Weg laufen (Threading, Linq, anonyme Methoden und einiges mit Interfaces)
    @ hal2000


    Einige Leute verrennen sich in diesem Thread auch zwischen statischer (WithEvents und Handles) und dynamischer (Add- und RemoveHandler) Bindung. Bitte nochmal im MSDN nachlesen - bei der statischen Variante wird auch nur die dynamische benutzt, nur eben verdeckt, implizit und automatisch. Deshalb sieht man dort auch keinen Delegaten. Das Prinzip ist aber dasselbe.


    Du kannst das Kind ruhig beim Namen nennen, habe ich kein Problem mit. ;)

    Aber richtig ist sicherlich das durch VB (ohne NET ;) ) und auch VBA Vorbelastete unter Event-Handling die klassisches Variation mit WithEvents eher kennen und das automatisch umlegen. Die dynamische Version per Add-/Remove-Handler gibt es ja erst sei .NET, vor dem .NET-Framework war ja VB nicht in der Lage mit Methodenzeiger umzugehen.

    Das in NET nun auch WithEvents automatisch im Hintergrund durch ein AddHandler eincompiliert wird war mir allerdings neu und hätte ich tatsächlich so auch nicht vermutet. Tja, mal wieder was dazu gelernt. ^^

    @ ErfinderDesRades

    Vielleicht solltest Du einfach nur auf den Umstand des "alten" WithEvents-Handling kurz eingehen und dabei auch dann eben erläutern das hierbei in der .NET-Umgebung automatisch im Hintergrund die Kombination WithEvents und einer Methode mit Handles mittels AddHandler einkompliliert wird.

    In dem Sinne muss ich wirklich einiges zurück nehmen was ich gesagt habe ... allerdings nur für VB.NET. ^^

    Gruß

    Rainer
    also ich wollte mich bei euch noch für die fabelhafte mitarbeit zu bedanken :thumbsup:

    Ich denk, ich werd den ganzen Salmon nochmal neu anfangen, und diesmal nicht mit "Event = Delegat" einsteigen, sondern das Zeugs vlt. aus einem Einstieg ala "Event = Observer-Pattern" heraus entwickeln.

    Weil was es ist ist ja weniger wichtig als wozu.