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

  • Carriage Return / Neue Zeile in Textareas

    Eine kleine Aufgabe: Jede neue Textzeile (Carriage Return/Wenn man Enter drückt ) in einer Textarea soll ein Element in einer Auflistung sein – wie mach ich das jetzt am einfachsten? Eigentlich ein grundlegendes Element im Web und der Nutzer macht bewusst Absätze – daher wäre es nur gerecht, wenn man das auch entsprechend würdigt. Kleine ...

  • image.png
    Doom, Quake, Wolfenstein & co. Source Code auf GitHub

    id Software, die Macher von Doom, Quake, Wolfenstein & co., stellen regelmäßig ihre älteren Spieltitle als Open Source zur Verfügung. Das Ganze runterzuladen fand ich bisher immer recht mühselig, allerdings gibt es seit kurzer Zeit die Sourcen auch auf GitHub. Darunter Spiele wie Doom 3, Quake 3, Wolfenstein für iOS. Wer also schon immer mal ...

  • image.png
    Twitter Bootstrap 2.0 released & “Release Präsentation”

    Wie bereits vom Twitter Bootstrap Team angekündigt wurde offiziel die Version 2.0 des UI Toolskits “Twitter Bootstrap” veröffentlich. Zudem wurden die Slides, welche bei der Release Party gezeigt wurden auch veröffentlicht: Downloads finden sich auf der Twitter Bootstrap Seite auf GitHub. Wenn dir der Blogpost gefallen hat, dann hinterlasse doch einen Kommentar. Wenn du auf ...

  • image.png
    Javascript zu Dart Translator

    Dart, Google Javascript Alternative, wurde vor ein paar Monaten vorgestellt und die Webentwickler Szene ist noch etwas gespalten, ob Dart nun überflüssig ist oder einfach nur cool und längst überfällig ist. Um die Sprache näher zu erläutern hat Google die grundlegenden Javascript Basics nach Dart übersetzt. Das Ergebnis ist der “Translator”. Der Name mag momentan ...

  • Twitter Bootstrap 2.0–“Beta”

    Twitter Bootstrap, ein UI-Toolkit für Web-Applikationen von Twitter, erscheint (wie bereits berichtet) demnächst in der Version 2.0. Der offizielle Release ist am 31. Januar, allerdings beginnt jetzt laut Mark Otto (einer der Hauptentwickler von Twitter Bootstrap) die intensive Test-Phase. Das heisst, das es nun offiziel auch die 2.0 Dokumentation online gibt. Im Vergleich zur aktuellen ...

Support us!

Facebook