HowTo: Eigene .NET Events definieren und mit Unit-Tests testen

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:

image 
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:
image
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:

image

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:

image

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 :)

[Download Source Code]


Kick It auf dotnet-kicks.de
Wenn dir der Blogpost gefallen hat, dann hinterlasse doch einen Kommentar. Wenn du auf dem Laufenden bleiben willst, abonniere unseren RSS Feed oder folge uns auf Twitter.

About the author

Written by Robert Mühsig

Robert Mühsig (@robert0muehsig) ist Webentwickler und beschäftigt sich mit Web-Frameworks (vor allem dem ASP.NET MVC Framework) und scheut sich auch nicht vor Javascript. Ansonsten bloggt er über all jene Probleme, die ihm über den Weg laufen. Seit 2008 ist er Microsoft MVP für ASP.NET und er arbeitet bei der T-Systems Multimedia Solutions GmbH in Dresden. Treffen kann man ihn online via Twitter (@robert0muehsig) oder dieser Seite oder bei der .NET User Group Dresden.

3 Responses

  1. Hallo Robert,

    interessanter Artikel, allerdings ist es wohl laut Code Analyse mittlerweile empfohlen statt eines eigenen Delegaten nur die EventArgs zur Verfügung zu stellen und das Ereignis als EventHandler<TEventArgs> zu definieren. Das spart spezifizieren eines eigenen Delegatens. Darüber hinaus ist wohl die sichere Method ein Ereignis auszulösen diese hier:

    EventHandler<StateChangedEventArgs> tmp = StateChanged;
    if (tmp != null)
    {
        tmp(this, e);
    }

    Könnte sogar sein, dass dein obiger Code eine NullReferenceException wirft, wenn du kein Ereignis zugewiesen hast. 

    Schau dir auch mal die Funktionen, die mit "On***" beginnen in der Control Klasse von Windows Forms oder das WebControl an. In dem man das Ereignis in einer protected virtual Funktion aufruft kann man in einer abgeleiteten Klasse das Ereignis behandeln ohne eines Ereignishandler dafür abzustellen. Steht so auch in der MSDN bei der Funktionsbeschreibung. 

    Reply

Comment on this post

Letzte Posts

  • image.png
    RavenHQ–RavenDB in der Cloud

    Ayende Rahien hat es heute verkündet – RavenHQ, der RavenDB Cloud Hoster (natürlich von und mit Ayende) ist ab heute raus aus der Beta und man kann es von überall aus nutzen. In der Betaphase waren nur Nutzer von AppHarbor zugelassen. Was ist RavenHQ? RavenHQ ist im Grunde ein gehostes RavenDB in den Rechenzentren von ...

  • image.png
    GitHub for Windows–erste Eindrücke

    Git ist schon eine tolle Sachen und eröffnet viele neue Möglichkeiten – allerdings ist der Einstieg recht hart und selbst wenn man die guten Hilfsanleitungen auf GitHub befolgt, kommt man am Anfang nur langsam vorwärt. Insbesondere ist das Tooling für Windows / .NET Entwickler auch nicht gerade “bekanntes Terrain”. GitHub to the rescue! Die GitHub ...

  • image.png
    Chocolatey–apt-get für Windows

    Durch Zufall bin ich auf das Tool “Chocolatey” gestoßen. Wer die Website sich anschaut, wird evtl. eine Verwandschaft mit NuGet ausmachen. Was macht Chocolatey? Chocolatey ist ein “Maschine Package Manager”, das bedeutet, dass man für seine Maschine einfach Tools runterladen und Updaten kann – direkt über die Konsole. Was ist der Unterschied zu NuGet? NuGet ...

  • image.png
    SASS, LESS & Coffeescript in Visual Studio mit der Web Workbench

    CSS und Javascript sind die “kleinste” Schnittmenge von allen Browsern für die Erstellung von Web-Applikationen. Leider geht dabei etwas komfort verloren, daher lieben alle Webentwickler jQuery! SASS und LESS sind zwei Varianten, wie man “schöner” CSS schreiben kann und Coffeescript versucht Javascript Entwicklung zu vereinfachen. Aber immer der Reihe nach… Was ist SASS? SASS steht ...

  • image.png
    Code-Inside Sample nun auf GitHub: Google Code zu GitHub Migration

    Seit einiger Zeit habe ich Beispielcode auf Google Code bereitgestellt. Einfach nur noch weg von Google Code O-Ton damals war: Ich hatte mich für Google Code entschieden, weil ich hoffe dass früher oder später die Google Code Suche nutzbar ist und es dadurch wenigstens ein kleiner Mehrwert entsteht. Allerdings wirft es momentan noch ein Fehler. ...

Auf Amazon einkaufen & unterstützen

Facebook