In dem heutigen HowTo geht es um das Erstellen von eigenen .NET Events samt dem dazu gehörigen testen mit einem Unit-Test.
Was ist ein Event aus "Anfängersicht"?
Jeder der (wahrscheinlich) in einer X-beliebigen IDE für eine X-beliebige Sprache bereits irgendein Button auf ein Fenster gezogen hat, wird als Resultat dann so einen ähnlichen Code sehen:
private void button1_Click(object sender, EventArgs e) { }
Hier kann man nun ganz genau definieren, was passiert wenn der "button1" geklickt wird.
Eigentlich eine tolle Geschichte :)
Was passiert denn technisch im Grunde genommen dahinter?
Events sind von der Idee her bereits sehr alt. Der Grundgedanke ist einfach nach dem "Hollywood-Prinzip": Ruf nicht uns an - wir rufen dich an.
Im Code brauchen wir nicht ständig prüfen ob der Button geklickt wird oder nicht - der Button sagt uns, wann er geklickt wird.
Ohne jetzt die .NET Implementierung (also was im Framework passiert) näher untersucht zu haben, würde ich meinen, dass das Grundkonzept aus dem Observer-Pattern abgeleitet ist.
Was ist ein Event: Beispiel aus der realen Welt
Um mal ein Beispiel aus der realen Welt aufzugreifen - auch wenn dies manchmal arg abstrakt ist ;)
Wenn jemand ein "Bild-Zeitungs-Abo" hat, fragt der normal-deutsche-Leser auch nicht ständig den Herrn Springer ob es nun eine neue Ausgabe gibt oder nicht - er bekommt sein Exemplar automatisch sobald es gedruckt wurde.
Einsatzgebiet von Events
Sobald man irgendwelche "Prozesse" oder "Abläufe" modelliert, könnte man im Prinzip auf Events setzen - ich persönlich bin erst vor kurzem auf das Thema gekommen. Das liegt vor allem daran, dass ich bisher meistens mich mit Web-Projekten beschäftigt habe. In der ASP.NET Welt sind meiner Meinung nach Events nicht so unglaublich nützlich. Der Grund liegt auf dem, wie HTTP funktioniert:
Der Client macht eine Anfrage und der Server antwortet entsprechend. Rein theoretisch geht nun ein Ablauf los - der irgendwann beeendet ist - das "Ich-bin-fertig-mit-meiner-Aufgabe" könnte über ein Event mitgeteilt werden.
An dieser Stelle sollte der Server an den Client zurückschicken: Bin fertig. Allerdings geht dies nicht:
Im HTTP Umfeld kann der Server nicht einfach Daten zum Client schicken - der Client (Browser) muss immer erst die Anfrage stellen. Dadurch muss man auf der Clientseite (über AJAX z.B.) ein Polling durchführen. Das führt natürlich dazu, dass Events leicht nutzlos werden.
Allerdings sind sie in sämtlichen Client-Anwendungen die nicht auf HTTP beruhen äußerst nützlich :)
Eigene Events definieren
Dan Wahlin hat ein sehr schönes Video erstellt, in dem die Grundgedanken sehr gut vermittelt werden.
Mein Beispiel:
Wir haben eine bestimmte Anwendung, welche jeweils einen Verbindungsstatus haben kann:
Diese "ConnectionStates" in der Anwendung werden von einem "ConnectionManager" verwaltet.
Jetzt wäre es ja schön, wenn uns unser "ConnectionManager" sofort informiert, wenn sich der Status ändert.
Dazu erstmal das grobe Konstrukt unserer Klasse:
public class ConnectionManager { private ConnectionStates _state; public ConnectionStates State { get { return _state; } set { _state = value; } } public ConnectionManager() { this.State = ConnectionStates.Disconnected; } }
Der Anfangsstatus ist erstmal auf "Disconnected" gestellt. Der Rest sollte soweit klar sein.
Jetzt kommen wir zur eigentlichen Eventdeklaration.
Schritt 1: Delegat definieren
Als ersten Schritt schreiben wir uns ein Delegat:
public delegate void StateChangedEventHandler(object sender, StateChangedEventArgs e);
Ein Delegat darf man als eine Art "Funktionszeiger" (im Video von Dan Wahlin wird es auch als Pipe zwischen Objekten verglichen) verstehen.
Schritt 2: StateChangeEventArgs definieren
In unserem Delegat definieren wir eine Methodendeklaration die so später auch der Clientcode sieht - als EventArgs definieren wir ebenfalls unsere eigene Klasse:
public class StateChangedEventArgs : EventArgs { public ConnectionStates NewConnectionStates { get; set; } }
Hier definieren wir einfach, was wir in unseren EventArgs später haben wollen - uns interessiert natürlich am meisten, was nun der neue Status ist.
Schritt 3: Event definieren
Jetzt definieren wir unser Event - an diesem können sich später die entsprechenden Clients melden:
public event StateChangedEventHandler StateChanged;
Dieses Event ist vom Typ "StateChangedEventHandler" - welches unser vorher definiertes Delegat ist.
Zwischenschritt: Der Client
Allein durch diese Definition des Events und des Delegates ist es möglich, das sich "Clientcode" an das Event dran hängt:
class Program { static void Main(string[] args) { ConnectionManager man = new ConnectionManager(); Console.WriteLine("Start state: " + man.State.ToString()); man.StateChanged += new ConnectionManager.StateChangedEventHandler(man_OnStateChanged); man.State = ConnectionStates.Connecting; man.State = ConnectionStates.Connected; man.State = ConnectionStates.Disconnected; Console.ReadLine(); } static void man_OnStateChanged(object sender, StateChangedEventArgs e) { Console.WriteLine("State changed..."); Console.WriteLine("New state is: " + e.NewConnectionStates.ToString()); } }
Die "man_OnStateChanged" Methode ist zwar definiert - allerdings rufen wir in unserem "ConnectionManager" nie das Event auf.
Schritt 4: Event in der Klasse aufrufen
In unserem Setter müssen wir natürlich das Event werfen - dies geschieht über eine weitere Methode in der Klasse. Hier mal der komplette Source Code:
public class ConnectionManager { private ConnectionStates _state; public ConnectionStates State { get { return _state; } set { _state = value; OnStateChanged(); } } public delegate void StateChangedEventHandler(object sender, StateChangedEventArgs e); public event StateChangedEventHandler StateChanged; protected void OnStateChanged() { if (StateChanged != null) { StateChangedEventArgs args = new StateChangedEventArgs(); args.NewConnectionStates = this.State; StateChanged(this, args); } } public ConnectionManager() { this.State = ConnectionStates.Disconnected; } }
Bei jedem setzen eines ConnectionStates wird die "OnStateChanged" Methode aufgerufen - diese ist nur intern erreichbar ("protected") bzw. von vererbten Klassen.
Diese Methode prüft, ob das "StateChanged" Event irgendwelche Beobachter hat - if(StateChanged != null).
Falls irgendwer im Clientcode sich an das Event angehangen hat, wird das Event mit unseren EventArgs geworfen.
Es klingt komplizierter als es ist
Da ich ebenfalls "neu" darin bin, musste ich mich ebenfalls erst mal in das Einarbeiten. Um es mal in kurzen Worten zu formulieren (soweit mein Verständnis richtig ist):
- Am "event" StateChanged können sich beliebige Clienten anmelden. Der Clientcode hat die selbe Methodensignatur (object Sender, EventArgsXYZ args) wie in dem "delegat" definiert.
- Das "delegat" ist nur eine definierte Schnittstelle zwischen den Objekten. Hier wird die Methodensignatur von dem Clientcode bestimmt.
- Die "EventArgs" sind eigene Datenklassen um entsprechende sinnvolle Daten zu übermitteln wenn das Event geworfen wurde
- Die interne "OnStateChanged" Methode prüft ob irgendwas am "event" hängt - wenn ja, dann löse es aus und leite es (über das delegat) an die richtige Stelle im Clientcode.
Resultat
In der Clientanwendung (die Consolen-Applikation) wird jedesmal die Ausgabe gemacht, sobald sich der Status ändert. Ohne jedes mal eine extra Methode aufzurufen oder die Ausgabe an den Manager zu ketten:
Unit-Tests: Wie teste ich Events?
Events kann man über eine nette C# 2.0 Sache testen: ein anonyme delegate. Den Trick habe ich bei Phil Haack gefunden.
[TestMethod] public void ConnectionManager_Raise_StateChanged_Event() { ConnectionManager man = new ConnectionManager(); Assert.AreEqual(ConnectionStates.Disconnected, man.State); bool eventRaised = false; man.StateChanged += delegate(object sender, StateChangedEventArgs args) { eventRaised = true; }; man.State = ConnectionStates.Connecting; Assert.IsTrue(eventRaised); }
In diesm Test lege ich einen bool "eventRaised" an - sobald das Event geworfen wird, wird ein anonymes delegat aufgerufen (man spart sich hier die zweite Methode) und ich setzt einfach diesen boolean auf "true".
Sehr einfach und genial um zu testen, ob das Event wie gehofft auch geworfen wird :)