# 5 Delegates und Ereignisse Grafische Benutzeroberflächen reagieren ausschließlich auf ausgelöste Ereignisse. Klicken Sie z. B. auf eine Schaltfläche, wird ein Ereignis ausgelöst und es findet eine Operation statt. Mit Ereignissen wollen wir uns in diesem Kapitel beschäftigen. Dazu müssen wir uns allerdings vorher noch den Delegates widmen, die die Grundlage der Ereignisse bilden. ## 5.1 Delegates ### 5.1.1 Einführung in das Prinzip der Delegates Das Prinzip des Delegates ist nicht ganz neu, wohl aber der Begriff an sich. Delegate ist das englische Wort für »Delegierter«, also für jemanden, der einen Auftrag, z. B. seiner Wähler, weiterleiten soll. Tatsächlich leitet ein Delegate weiter, es leitet nämlich einen Methodenaufruf an eine bestimmte Methode weiter. Die Technik, die sich dahinter verbirgt, wird in der Sprache C auch als Funktionszeiger bezeichnet. Wie Sie wissen, basiert ausnahmslos alles in .NET auf Objekten. Da verwundert es nicht, dass auch die Funktionszeiger in ein Objekt verpackt werden und, als Delegate bezeichnet, ihren Weg in die Laufzeitumgebung finden.
Definition
Ein Delegate ist ein Typ, der den Zeiger auf eine Methode beschreibt.
Bevor wir uns mit den Details von Delegates beschäftigen, wollen wir uns zunächst an einem einfachen Beispiel die grundsätzliche Arbeitsweise verdeutlichen. Die Operation, die von diesem Code ausgeführt wird, ist recht einfach: Der Anwender gibt zwei Zahlen an der Konsole ein und hat anschließend die Wahl, ob beide Zahlen addiert oder subtrahiert werden sollen. Das Resultat der Operation wird abhängig von der Wahl des Anwenders an der Konsole ausgegeben.
    // ---------------------------------------------------------
    // Beispiel: ...\Kapitel 5\EinfachesDelegate
    // ---------------------------------------------------------
    public delegate double CalculateHandler(double dblVar1, double dblVar2);
    class Program {
      static void Main(string[] args) {
      // Variable vom Typ des Delegates
      CalculateHandler calculate;
      do{
        // Eingabe der Operanden
        Console.Clear();
        Console.Write("Geben Sie den ersten Operanden ein: ");
        double input1 = Convert.ToDouble(Console.ReadLine());
        Console.Write("Geben Sie den zweiten Operanden ein: ");
        double input2 = Convert.ToDouble(Console.ReadLine());
        // Wahl der Operation
        Console.Write("Operation: Addition - (A) oder Subtraktion - (S)? ");
        string wahl = Console.ReadLine().ToUpper();
        // In Abhängigkeit von der Wahl des Anwenders wird die Variable
        // 'calculate' mit einem Zeiger auf die auszuführende Methode
        // initialisiert
        if (wahl == "A")
          calculate = new CalculateHandler(Demo.Addition);
        else if (wahl == "S")
          calculate = new CalculateHandler(Demo.Subtraktion);
        else {
          Console.Write("Ungültige Eingabe");
          Console.ReadLine();
          return;
        }
        // Aufruf der Operation 'Addition' oder 'Subtraktion' über das Delegate
        double result = calculate(input1, input2);
        Console.WriteLine("----------------------------------");
        Console.WriteLine("Ergebnis = {0}\n\n", result);
        Console.WriteLine("Zum Beenden F12 drücken, mit beliebiger Taste weiter...");
      } while (Console.ReadKey(true).Key != ConsoleKey.F12);
    }
    class Demo {
      public static double Addition(double x, double y) {
        return x + y;
      }
      public static double Subtraktion(double x, double y) {
        return x - y;
      }
    }
In der Klasse Demo sind zwei statische Methoden definiert, die aus Main heraus aufgerufen werden und die beiden Operationen Addition und Subtraktion beschreiben. Die Wahl, ob die beiden Zahlen addiert oder subtrahiert werden sollen, trifft der Anwender durch die Eingabe von »A« oder »S« an der Konsole. Um die Eingabe in Kleinschreibweise ebenfalls zu berücksichtigen, wird die Eingabe mit der Methode ToUpper der Klasse String in Großschreibweise umgewandelt.
    string wahl = Console.ReadLine().ToUpper();
Nachdem der Anwender seine Wahl getroffen hat, muss zuerst überprüft werden, wie diese ausgefallen ist, um entsprechend im Programmcode zu reagieren. Vermutlich hätten Sie eine solche Aufgabenstellung bisher wie folgt gelöst:
    double result;
    if(wahl == "A")
      result = Demo.Addition(input1, input2);
    else if(wahl == "S")
      result = Demo.Subtraktion(input1, input2);
Es gibt keinen Zweifel daran, dass diese Implementierung natürlich auch zum richtigen Ergebnis führt. Das Resultat der Operation wird in den Anweisungsblöcken hinter if bzw. else if abgerufen. Nun betrachten wir die entscheidenden Anweisungen der Lösung im Beispiel EinfachesDelegate:
    if(wahl == "A")
      calculate = new CalculateHandler(Demo.Addition);
    else if(wahl == "S")
      calculate = new CalculateHandler(Demo.Subtraktion);
    ...
    double result = calculate(input1, input2);
Das Ergebnis der Addition bzw. Subtraktion wird nun nicht mehr in den beiden Anweisungsblöcken der if-Struktur abgerufen, sondern außerhalb derselben. Da aber außerhalb der if-Struktur die Wahl des Anwenders nicht bekannt sein kann, stehen wir vor der Frage, wie es möglich ist, eine Methode an einem Allgemeinplatz dynamisch aufzurufen. Die Antwort darauf ist prinzipiell nicht schwierig: Wir müssen in einer Variablen einen Verweis auf die aufzurufende Methode speichern, der später an einer beliebigen Stelle im Code ausgewertet werden kann. Bisher kennen wir Verweise nur im Zusammenhang mit Objekten. Mit Objektverweisen werden zusammenhängende Datenblöcke im Hauptspeicher adressiert, in denen die Zustandsdaten eines ganz bestimmten Objekts beschrieben werden. Ein Verweis auf Programmcode ist im Grunde genommen nicht anders, zeigt aber auf Bytesequenzen, die anders interpretiert werden müssen – nämlich als ausführbarer Programmcode. Damit ist auch klar, dass ein Verweis auf Code anders definiert werden muss als der Verweis auf Datenblöcke, den wir bisher verwendet haben. Aus diesem Grund wurden in .NET die Delegates eingeführt. Wie schon oben erwähnt, kapselt ein Delegate den Zeiger auf eine Methode. Sehen wir uns jetzt an, wie diese Anforderung gelöst wird. Im Code des Beispiels EinfachesDelegate wird mit
    public delegate double CalculateHandler(double dblVar1, double dblVar2);
ein Delegate definiert. Diese Definition erinnert ein wenig an die Methodensignatur einer Methode namens CalculateHandler, die zwei Parameter vom Typ double empfängt und als Rückgabewert einen double liefert – nur ergänzt um das Schlüsselwort delegate. Ein Delegate kapselt den Zeiger auf eine Methode – oder mit anderen Worten: Es steht für einen beliebigen Methodenaufruf. Ganz beliebig ist der Methodenaufruf allerdings nicht, denn jede Methode hat eine exakt definierte Parameterliste mit Parametern eines bestimmten Typs. Ein Delegate beschreibt einen Zeiger auf eine Methode, wobei die Typen der Parameterliste der Methode, auf die das Delegate zeigt, mit der Parameterliste der delegate-Definition übereinstimmen müssen. In unserem Beispiel werden in der Parameterliste des Delegates CalculateHandler zwei Parameter vom Typ double aufgeführt. Damit ist ein Delegate-Objekt in der Lage, jede x-beliebige Methode eines x-beliebigen Objekts aufzurufen – vorausgesetzt, die Methode definiert eine Parameterliste, die genau zwei double-Argumente erwartet. Die Methode darf natürlich auch statisch sein, sie darf sich in einer anderen Klasse befinden, alles das spielt keine Rolle. Die einzige Bedingung ist, dass die durch das Delegate beschriebene Methode mit Programmcode angesprochen werden kann, also erreichbar ist. Neben der Parameterliste spielt auch der Rückgabewert eine entscheidende Rolle. Im Beispiel des Delegates CalculateHandler muss die Methode in jedem Fall einen Rückgabewert vom Typ double haben. Nicht jede Methode hat einen Rückgabewert. Beabsichtigen Sie beispielsweise, ein Delegate zu definieren, das in der Lage ist, einen Zeiger auf sämtliche Methoden zu beschreiben, die parameterlos sind und keinen Rückgabewert haben, sähe die Definition folgendermaßen aus:
    public delegate void MyDelegate();
Sie können die Definition eines Delegates mit der Definition einer Klasse vergleichen, denn beide beschreiben einen Typ. Um ein konkretes Objekt zu erhalten, muss zuerst eine Variable vom Typ der Klasse deklariert werden – das ist bei einem Delegate nicht anders. Im Beispiel oben dient dazu folgende Anweisung:
    CalculateHandler calculate;
Damit ist die Variable calculate vom Typ CalculateHandler deklariert, aber noch nicht initialisiert. Mit anderen Worten: calculate ist ein Delegate und kann auf eine Methode verweisen, die zwei double-Argumente erwartet und ein double als Resultat des Aufrufs zurückliefert. In diesem Moment weiß das Delegate allerdings noch nicht, um welche Methode es sich dabei genau handelt. Die Initialisierung erfolgt – analog zur Instanziierung einer Klasse – mit dem Operator new unter Angabe des Delegate-Typs. Delegates haben nur einen einfach parametrisierten Konstruktor, der Bezeichner der Methode erwartet, die später aufgerufen werden soll. In unserem Beispiel handelt es sich um
    calculate = new CalculateHandler(Demo.Addition);
bzw. um:
    calculate = new CalculateHandler(Demo.Subtraktion);
Jetzt ist dem Delegate bekannt, welche Methode ausgeführt werden soll: entweder Addition oder Subtraktion. Allerdings wird die Methode, auf die das Delegate in Form eines Zeigers verweist, noch nicht sofort ausgeführt, denn dazu bedarf es eines Anstoßes durch den Aufruf des Delegates:
    double result = calculate(input1, input2);
Der Aufruf erinnert an den Aufruf einer Methode, dabei wird allerdings der Methodenname (hier: Addition bzw. Subtraktion) durch die Variable vom Typ des Delegates ersetzt. In den Klammern werden die erforderlichen Argumente an die Methode übergeben. Damit die Anwendung nicht schon nach der ersten Berechnung beendet wird, ist die gesamte Programmlogik der Methode Main in einer do-while-Schleife codiert. Im Schleifenfuß erfolgt eine Überprüfung, ob der Anwender die Anwendung beenden möchte. Dazu muss er die Taste drücken.
    do {
      // Anweisungen
    } while (Console.ReadKey(true).Key != ConsoleKey.F12);
Die statische Methode ReadKey ruft dabei die gedrückte Zeichen- oder Funktionstaste ab. Das Übergabeargument true sagt aus, dass das entsprechende Zeichen nicht in die Konsole geschrieben werden soll. Der Rückgabewert der Methode ReadKey ist vom Typ ConsoleKeyInfo. Darauf rufen wir mit der Eigenschaft Key die gedrückte Taste ab, die über die Konstantenauflistung ConsoleKey beschrieben wird. ### 5.1.2 Vereinfachter Aufruf eines Delegates Es gibt noch eine weitere Notation, um ein Delegate zu instanziieren und ihm gleichzeitig die auszuführende Methode anzugeben. Diese ist etwas einfacher in der Handhabung und erspart uns ein wenig Tipparbeit. Sie können nämlich anstelle der Anweisung
    CalculateHandler calculate = new CalculateHandler(Demo.Addition);
auch wie folgt codieren:
    CalculateHandler calculate = Demo.Addition;
### 5.1.3 Multicast-Delegate .NET bietet die Möglichkeit, mehrere Delegates zu einem einzigen zusammenzufassen. Dadurch entsteht ein Delegate-Verbund, der auch als Multicast-Delegate bezeichnet wird. Der Vorteil ist, dass durch den Aufruf eines Delegates mehrere Delegates der Reihe ausgeführt werden können. Dazu ein Beispiel. Stellen Sie sich vor, Sie hätten den Auftrag bekommen, eine Software zu entwickeln, die eine Pumpenanlage zum Füllen des Schwimmbeckens eines Schwimmbades ansteuert. Es handelt sich bei dieser Anlage um Pumpen verschiedener Hersteller. Grundsätzlich sollen alle Pumpen eingeschaltet werden, wenn das Becken gefüllt wird. Anzahl und Typ der Pumpen können dabei durchaus variieren. Ihre Software soll so flexibel sein, sich an solche Änderungen automatisch anpassen zu können. Wie kann das Problem am besten gelöst werden? Man kann davon ausgehen, dass jede Pumpe anders angesteuert werden muss. Daher bietet es sich an, für jede in Frage kommende Pumpe eine eigene Klasse mit einer Methode zu entwickeln, aus der heraus die Pumpe gestartet wird. Wir wollen zunächst zwei Klassen bereitstellen, PumpeA und PumpeB, deren Methoden SwitchOnA und SwitchOnB für die komplexen Einschaltvorgänge stehen sollen.
    class PumpeA {
      public void SwitchOnA() {
        Console.WriteLine("Pumpe A wird eingeschaltet.");
      }
    }
    class PumpeB {
      public void SwitchOnB() {
        Console.WriteLine("Pumpe B wird eingeschaltet.");
      }
    }
Eine weitere Klasse ist als Komponente in der Benutzeranwendung implementiert und ruft eine Methode in der pumpensteuernden Klasse auf, mit der der Startvorgang in Gang gesetzt wird. Hier sehen Sie den Code der Anwendung:
    // --------------------------------------------------------------
    // Beispiel: ...\Kapitel 5\MulticastDelegate
    // --------------------------------------------------------------
    public delegate void PumpDelegate();
    class Program {
      static void Main(string[] args) {
        ControlPumps obj = new ControlPumps();
        // Array vom Typ PumpDelegate
        PumpDelegate[] del = new PumpDelegate[4];
        // Pumpenobjekte erzeugen
        PumpeA p1 = new PumpeA();
        PumpeB p2 = new PumpeB();
        PumpeA p3 = new PumpeA();
        PumpeA p4 = new PumpeA();
        // Startmethoden der Pumpen durch ein Delegate-
        // Objekt beschreiben
        del[0] = new PumpDelegate(p1.SwitchOnA);
        del[1] = new PumpDelegate(p2.SwitchOnB);
        del[2] = new PumpDelegate(p1.SwitchOnA);
        del[3] = new PumpDelegate(p1.SwitchOnA);
        // alle Delegates kombinieren
        PumpDelegate arrDel = (PumpDelegate)Delegate.Combine(del);
        // Delegate-Array an die Steuerklasse übergeben
        obj.AddPump(arrDel);
        // Pumpen starten
        obj.StartAllPumps();
        Console.ReadLine();
      }
    }
    // ------------- steuernde Klasse -------------
    class ControlPumps {
      private PumpDelegate delPumps;
      public void AddPump(PumpDelegate pumps) {
        delPumps = pumps;
      }
      public void StartAllPumps() {
        delPumps();
      }
    }
Die Anwendung beschreibt ein benutzerdefiniertes Delegate, das PumpDelegate heißt. Ein Objekt vom Typ dieses Delegates kann auf eine parameterlose Methode ohne Rückgabewert zeigen. Das entspricht exakt der Methodensignatur der beiden Startmethoden in den Pumpenklassen. In der Anwendung ist neben der obligatorischen Klasse Program eine weitere Klasse implementiert worden (ControlPumps). In dieser ist ein Feld vom Typ des Delegates PumpDelegate definiert, dem über die Methode AddPump ein Wert zugewiesen wird. Zudem enthält die Klasse mit StartAllPumps eine zweite Methode, die die Aufgabe hat, alle Pumpen quasi gleichzeitig zu starten. Werfen wir nun einen Blick auf den Code in Main. Es fällt als Erstes auf, dass die den Pumpenobjekten zugeordneten Delegates zu Elementen des Arrays del werden. Dabei handelt es sich um ein Array von PumpDelegate-Objekten. Anschließend folgt die Anweisung
    PumpDelegate arrDel = (PumpDelegate)Delegate.Combine(del);
Da Delegates wie alles in der .NET-Welt Objekte sind, wird dieser Typ durch eine eigene Klasse im Namespace System beschrieben: Delegate. Mit der statischen Methode Combine dieses Typs lassen sich mehrere Delegates miteinander verknüpfen. Combine ist wie folgt überladen:
    public static Delegate Combine(Delegate[]);
    public static Delegate Combine(Delegate del1, Delegate del2);
Sie können als Argument entweder ein Array vom Typ Delegate übergeben oder haben die Alternative, zwei Delegates miteinander zu verknüpfen. Der Rückgabewert ist in beiden Fällen ein Delegate – oder präziser gesagt: Es handelt sich um ein Multicast-Delegate, für das es in der Klassenbibliothek eine eigene Typdefinition gibt, die von Delegate abgeleitet ist: MulticastDelegate. Der Rückgabewert wird nach einer expliziten Konvertierung dem benutzerdefinierten Delegate zugewiesen:
    PumpDelegate arrDel = (PumpDelegate)Delegate.Combine(del);
Wir haben nun eine Objektvariable namens arrDel, die ein Multicast-Delegate referenziert, das seinerseits vier Singlecast-Delegates kombiniert. Dem Objekt der steuernden Klasse ControlPumps müssen wir jetzt nur noch die Referenz arrDel übergeben. Wenn Sie das Programm ausführen, wird an der Konsole folgende Ausgabe erscheinen:
    Pumpe A wird eingeschaltet.
    Pumpe B wird eingeschaltet.
    Pumpe A wird eingeschaltet.
    Pumpe A wird eingeschaltet.
#### Methoden eines Multicast-Delegates Jedes Delegate steht für eine Liste von Methodenaufrufen, die durchlaufen wird, sobald das Delegate ausgeführt wird. Im Falle eines Singlecast-Delegates enthält diese Liste nur ein Element, bei einem Multicast-Delegate können es mehrere sein. Auf diese Aufrufliste können Sie mit der Methode GetInvocationList der Klasse Delegate bzw. MulticastDelegate zugreifen; der Rückgabewert ist ein Delegate-Array. In der Klasse Delegate ist diese Methode wie folgt definiert:
    public virtual Delegate[] GetInvocationList();
Um eine Methode zu der Aufrufliste hinzuzufügen oder aus ihr zu entfernen, definiert die Delegate-Klasse die beiden statischen Methoden Combine und Remove. Wir hatten in unserem Beispiel oben ein Multicast-Delegate erzeugt, indem wir vier Singlecast-Delegates zu einem Array zusammengefasst und als Argument der Combine-Methode übergeben haben. Die überladene Version dieser Methode wollen wir uns zusammen mit der Remove-Methode anschauen:
    public static Delegate Combine(Delegate del1, Delegate del2);
    public static Delegate Remove(Delegate source, Delegate value);
Beide Parameterlisten sind identisch und erwarten sowohl im ersten als auch im zweiten Argument die Referenz auf ein Delegate. Dem ersten Parameter wird die Referenz auf das Delegate übergeben, zu dessen Aufrufliste ein weiteres Delegate hinzugefügt werden soll. Bei der Remove-Methode soll es natürlich entfernt werden. Der zweite Parameter beschreibt das Delegate, das hinzugefügt bzw. entfernt werden soll. Dazu ein kleines Beispiel:
    PumpDelegate del1 = new PumpDelegate(p1.SwitchOnA);
    PumpDelegate del2 = new PumpDelegate(p2.SwitchOnB);
    del2 = Delegate.Combine(del2, del1);
    ...
    del2 = Delegate.Remove(del2, del1);
In der dritten Codezeile wird das Delegate zu einem Multicast-Delegate, und in der letzten Zeile wird diese Zuordnung wieder aufgehoben. Es ist möglich, im zweiten Argument ein Multicast-Delegate anzugeben; letztendlich verkleinert sich dadurch allerdings nicht der Programmcode. Bei einer Kombination mehrerer Delegates ist daher die Variante mit der Übergabe eines Arrays vorzuziehen. Interessiert der Name der von einem Delegate gekapselten Methode, lässt sich dieser durch die schreibgeschützte Eigenschaft Method nebst weiteren Informationen in Erfahrung bringen. Der Aufruf dieser Eigenschaft liefert als Rückgabewert die Referenz auf ein Objekt vom Typ System.Reflection.MethodInfo, das die unterschiedlichsten Informationen zu einer Methode bereitstellt, beispielsweise über die Instanzeigenschaft Name den Namen der von einem Delegate eingeschlossenen Methode.
    Console.WriteLine(del1.Method().Name);
Die ebenfalls schreibgeschützte Eigenschaft Target liefert eine Referenz auf das Objekt, dessen Instanzmethode das aktuelle Delegate aufruft:
    public object Target {get;}
Da der Rückgabewert Object ist, wird man in der Regel zuerst explizit konvertieren müssen:
    PumpeA newPump = (PumpeA)del1.Target;
### 5.1.4 Anonyme Methoden Delegates haben wir bisher konstruiert, indem wir das Delegate instanziiert und dem Konstruktor dabei einen Methodennamen übergeben haben. Das setzt voraus, dass die Methode auch irgendwo im Programmcode namentlich bekannt ist. Das kann dazu führen, dass sehr viele Methoden nur die Aufgabe haben, ein bestimmtes Delegate zu bedienen. Der auszuführende Programmcode kann auch direkt mit dem Delegate verknüpft werden. Der Code ist nicht mehr mit einem Methodenbezeichner verbunden und wird deshalb als anonyme Methode bezeichnet. Wir wollen uns das an einem Beispiel ansehen und ändern dazu das Beispielprogramm so ab, dass anstelle der ausgeschriebenen Methoden Addition und Subtraktion nun anonyme Methoden verwendet werden.
    // ---------------------------------------------------------
    // Beispiel: ...\Kapitel 5\AnonymeMethoden
    // ---------------------------------------------------------
    public delegate double CalculateHandler(double dblVar1, double dblVar2);
    class Program {
      static void Main(string[] args) {
        // Variable vom Typ des Delegates
        CalculateHandler calculate;
        do {
          // Eingabe der Operanden
          Console.Clear();
          Console.Write("Geben Sie den ersten Operanden ein: ");
          double input1 = Convert.ToDouble(Console.ReadLine());
          Console.Write("Geben Sie den zweiten Operanden ein: ");
          double input2 = Convert.ToDouble(Console.ReadLine());
          // Wahl der Operation
          Console.Write("Operation: Addition - (A) oder Subtraktion - (S)? ");
          string wahl = Console.ReadLine().ToUpper();
          if (wahl == "A")
            calculate = delegate(double x, double y)
            {
              return x + y;
            };
          else if (wahl == "S")
            calculate = delegate(double x, double y)
            {
              return x - y;
            };
          else
          {
            Console.Write("Ungültige Eingabe");
            Console.ReadLine();
            return;
          }
          double result = calculate(input1, input2);
          Console.WriteLine("----------------------------------");
          Console.WriteLine("Ergebnis = {0}\n\n", result);
          Console.WriteLine("Zum Beenden ...");
        } while (Console.ReadKey(true).Key != ConsoleKey.F12);
      }
    }
Der Code, der vorher noch in den Methoden Addition und Subtraktion implementiert war, wird nun direkt nach der Deklaration der Variablen vom Typ des Delegates angegeben.
    if (wahl == "A")
      calculate = delegate(double x, double y)
                {
                  return x + y;
                };
Das Schlüsselwort delegate dient dazu, ein Delegate zu instanziieren und das Objekt direkt mit einer anonymen Methode zu verbinden. Hinter delegate ist die Parameterliste entsprechend der Delegate-Definition angegeben. Handelt es sich um eine parameterlose, anonyme Methode, bleibt die Liste leer und man kann auf die Angabe der runden Klammern verzichten. Da sich der Anweisungsblock einer anonymen Methode immer innerhalb einer äußeren Methode befindet, kann aus der anonymen Methode heraus auf jede andere Variable der äußeren Methode zugegriffen werden.