Skip to content

8. Benutzerereignisse

Im vorigen Kapitel haben wir das Konzept von Ereignissen im Zusammenhang mit Formularkomponenten behandelt. Nun werden wir uns ansehen, wie man Ereignisse in eigenen Klassen erstellt.

8.1. Vordefinierte Delegatobjekte

Der Begriff des Objekt-Delegaten wurde bereits im vorigen Kapitel angesprochen, damals jedoch nur kurz gestreift. Als wir uns angesehen haben, wie die Ereignisbehandler der Formularkomponenten deklariert wurden, stießen wir auf Code, der dem folgenden ähnelt:


            this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);

wobei buttonAfficher eine [Button]-Komponente war. Diese Klasse verfügt über ein Click-Ereignis, das wie folgt definiert ist:

  • [1]: die [Button]-Klasse
  • [2]: ihre Ereignisse
  • [3,4]: das Ereignis „Click“
  • [5]: die Deklaration des Ereignisses [Control.Click] [4].
    • EventHandler ist ein Prototyp (ein Modell) einer Methode, die als Delegat bezeichnet wird.
    • „event“ ist ein Schlüsselwort, das die Funktionalität des Delegaten einschränkt. EventHandler: Ein Objekt-Delegat verfügt über umfangreichere Funktionen als ein „event“.

Der Delegat EventHandler ist wie folgt definiert:

 

Der Visit-Delegat EventHandler bezeichnet ein Methodenmodell:

  • mit dem Typ als erstem Parameter Object
  • dessen zweiter Parameter ein EventArgs ist
  • und der keine Ergebnisse zurückgibt

Eine Methode, die dem durch EventHandler definierten Modell entspricht, könnte wie folgt aussehen:


        private void buttonAfficher_Click(object sender, EventArgs e);

Um einen EventHandler zu erstellen, gehen Sie wie folgt vor:

EventHandler evtHandler=new EventHandler(méthode correspondant au prototype  défini par le type EventHandler);

Wir können somit schreiben:

EventHandler evtHandler=new EventHandler(buttonAfficher_Click);

Eine Variable vom Typ Delegate ist eigentlich eine Liste von Verweisen auf Methoden, die dem Delegate entsprechen. Um der obigen Variablen evtHandler eine neue Methode M hinzuzufügen, verwenden wir die folgende Syntax:

evtHandler+=new EvtHandler(M);

Die Notation += kann auch verwendet werden, wenn evtHandler eine leere Liste ist.

Anleitung:


            this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);

fügt einen EventHandler zur Liste der Ereignismethoden von buttonAfficher.Click hinzu. Wenn das Ereignis Click auf der Komponente buttonAfficher auftritt, führt VB Folgendes aus:

            buttonAfficher.Click(source, evt);

wobei:

  • source das Objekt hinter dem Ereignis ist
  • evt vom Typ EventArgs und enthält keine Informationen

Alle Signaturmethoden vom Typ void M(object, EventArgs), die mit dem Ereignis Click assoziiert sind, durch:


            this.buttonAfficher.Click += new System.EventHandler(M);

werden mit den von VB übergebenen Parametern (source, evt) aufgerufen.

8.2. Definition von Delegatobjekten

Anweisung


    public delegate int Opération(int n1, int n2);

definiert einen Typ namens Operation, der einen Funktionsprototyp darstellt, der zwei Ganzzahlen akzeptiert und eine Ganzzahl zurückgibt. Es ist das Schlüsselwort delegate, das Operation zu einer Funktionsprototypdefinition macht.

Eine Variable op vom Typ Operation registriert eine Liste von Funktionen, die dem Prototyp Operation entsprechen:

int f1(int,int)
int f2(int, int)
...
int fn(int, int)

Das Registrieren einer Methode fi in der Variablen op erfolgt durch op=new Operation(fi) oder einfach op=fi. Um eine Methode fj zur Liste der registrierten Funktionen hinzuzufügen, schreiben wir op+= fj. Um eine Methode fk zu entfernen, schreiben wir op-=fk. Wenn wir in unserem Beispiel n=op(n1,n2) schreiben, wird die in op gespeicherte Menge an Methoden mit den Parametern n1 und n2 ausgeführt. Das zurückerhaltene Ergebnis n ist das der zuletzt ausgeführten Methode. Es ist nicht möglich, die von allen Methoden erzeugten Ergebnisse zu erhalten. Aus diesem Grund geben delegierte Funktionen, in denen eine Liste von Methoden gespeichert ist, in der Regel ein Ergebnis vom Typ void zurück.

Betrachten Sie das folgende Beispiel:


using System;
namespace Chap6 {
    class Class1 {
         // function prototype definition
         // accepts 2 integers as parameters and returns an integer
        public delegate int Opération(int n1, int n2);
 
         // two instance methods corresponding to the prototype
        public int Ajouter(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
         }//add
 
        public int Soustraire(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
         }//subtract
 
         // a static method corresponding to the prototype
        public static int Augmenter(int n1, int n2) {
            Console.WriteLine("Augmenter(" + n1 + "," + n2 + ")");
            return n1 + 2 * n2;
         }//increase
 
        static void Main(string[] args) {
 
             // define an operation object to store functions
             // we register the static function increase
            Opération op = Augmenter;
             // the delegate is executed
            int n = op(4, 7);
            Console.WriteLine("n=" + n);
 
             // creation of a c1 object of type class1
            Class1 c1 = new Class1();
             // we register c1's add method in the delegate
            op = c1.Ajouter;
             // execution of delegated object
            n = op(2, 3);
            Console.WriteLine("n=" + n);
             // the subtract method of c1 is registered in the delegate
            op = c1.Soustraire;
            n = op(2, 3);
            Console.WriteLine("n=" + n);
             //registration of two functions in the delegate
            op = c1.Ajouter;
            op += c1.Soustraire;
             // execution of delegated object
            op(0, 0);
             // remove a function from the delegate
            op -= c1.Soustraire;
             // the delegate is executed
            op(1, 1);
        }
    }
}
  • Zeile 3: definiert eine Klasse Class1.
  • Zeile 6: Definition des Delegaten Operation: ein Prototyp von Methoden, die zwei Parameter vom Typ int akzeptieren und einen int zurückgeben
  • Zeilen 9–12: Die Instanzmethode Add wird der Signatur des Delegaten Operation hinzugefügt.
  • Zeilen 14–17: Die Instanzmethode `subtract` der Klasse `S` wird der Signatur des Delegaten `Operation` hinzugefügt.
  • Zeilen 20–23: Die Klassenmethode Ancrease wird der Signatur des Delegaten Operation hinzugefügt.
  • Zeile 25: Die Methode „Main“ wird ausgeführt
  • Zeile 20: Die Variable op vom Typ Delegate Operation enthält eine Liste von Methoden mit der Typsignatur Delegate Operation. Ihr wird eine erste Methodenreferenz zugewiesen, nämlich die der statischen Methode Class1.Augmenter.
  • Zeile 31: Der Delegat op wird ausgeführt: Alle von op referenzierten Methoden werden ausgeführt. Sie werden mit den an den Delegaten op übergebenen Parametern ausgeführt. Hier wird nur die statische Methode Class1.Augmenter ausgeführt.
  • Zeile 35: Eine Instanz c1 der Klasse Class1 wird erstellt.
  • Zeile 37: Die Instanzmethode c1.Ajouter wird dem Delegaten op zugewiesen. Augmenter war eine statische Methode, Add ist eine Instanzmethode. Wir wollten zeigen, dass dies keine Rolle spielt.
  • Zeile 39: Der Delegat op wird ausgeführt: Add wird mit den an den Delegaten op übergebenen Parametern ausgeführt.
  • Zeile 42: Machen Sie dasselbe mit der Instanzmethode Subtract.
  • Zeilen 46–47: Die Methoden Add und Subtract im Delegaten op.
  • Zeile 49: Der Delegat op wird ausgeführt: Sowohl Add als auch Subtract werden mit den an den Delegaten op übergebenen Parametern ausgeführt.
  • Zeile 51: Die Methode Subtract wird aus dem Delegate-Op entfernt.
  • Zeile 53: Der Delegate-Op wird ausgeführt: Die verbleibende Add-Methode wird ausgeführt.

Die Ergebnisse lauten wie folgt:

1
2
3
4
5
6
7
8
9
Augmenter(4,7)
n=18
Ajouter(2,3)
n=5
Soustraire(2,3)
n=-1
Ajouter(0,0)
Soustraire(0,0)
Ajouter(1,1)

8.3. Delegaten oder Schnittstellen?

Die Begriffe „Delegaten“ und „Schnittstellen“ mögen recht ähnlich erscheinen, und man fragt sich vielleicht, worin genau der Unterschied zwischen diesen beiden Begriffen besteht. Nehmen wir das folgende Beispiel, das dem bereits behandelten sehr ähnlich ist:


using System;
namespace Chap6 {
    class Program1 {
         // function prototype definition
         // accepts 2 integers as parameters and returns an integer
        public delegate int Opération(int n1, int n2);
 
         // two instance methods corresponding to the prototype
        public static int Ajouter(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
         }//add
 
        public static int Soustraire(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
         }//subtract
 
         // Executing a delegate
        public static int Execute(Opération op, int n1, int n2){
            return op(n1, n2);
        }
 
        static void Main(string[] args) {
             // delegate execution Add
            Console.WriteLine(Execute(Ajouter, 2, 3));
             // delegate execution Subtract
            Console.WriteLine(Execute(Soustraire, 2, 3));
             // executing a multicast delegate
            Opération op = Ajouter;
            op += Soustraire;
            Console.WriteLine(Execute(op, 2, 3));
             // remove a function from the delegate
            op -= Soustraire;
             // the delegate is executed
            Console.WriteLine(Execute(op, 2, 3));
        }
    }
}

Zeile 20: Die Methode Execute erwartet eine Referenz auf ein Objekt vom Typ Delegat Operation, das in Zeile 6 definiert ist. Dies wechselt zu den verschiedenen Methoden von Execute (Zeilen 26, 28, 32 und 36). Diese Polymorphismus-Eigenschaft kann auch mit einem : erreicht werden.


using System;
 
namespace Chap6 {
 
     // interface IOperation
    public interface IOperation {
        int operation(int n1, int n2);
    }
 
     // class Add
    public class Ajouter : IOperation {
        public int operation(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }
    }
 
     // class Subtract
    public class Soustraire : IOperation {
        public int operation(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
        }
    }
 
     // test class
    public static class Program2 {
         // Executing the single method of the IOperation interface
        public static int Execute(IOperation op, int n1, int n2) {
            return op.operation(n1, n2);
        }
 
        public static void Main() {
             // delegate execution Add
            Console.WriteLine(Execute(new Ajouter(), 2, 3));
             // delegate execution Subtract
            Console.WriteLine(Execute(new Soustraire(), 2, 3));
        }
    }
}
  • Zeilen 6–8: Die Schnittstelle [IOperation] definiert eine Methode operation.
  • Zeilen 11–16 und 19–24: Die Klassen [Add] und [Subtract] implementieren die Schnittstelle [IOperation].
  • Zeilen 29–31: Die Methode `Execute`, deren erster Parameter vom Schnittstellentyp `IOperation` ist. Die Methode `Execute` erhält nacheinander als ersten Parameter eine Instanz der Klasse `Add` und anschließend eine Instanz der Klasse `Subtract`.

Der polymorphe Aspekt des Delegaten aus dem vorherigen Beispiel. Gleichzeitig zeigen beide Beispiele die Unterschiede zwischen diesen beiden Konzepten auf.

Die Typen Delegat und Schnittstelle sind austauschbar

  • , wenn die Schnittstelle nur eine Methode hat. Tatsächlich ist der Delegat eine Hülle für eine einzelne Methode, während die Schnittstelle mehrere Methoden definieren kann.
  • wenn der Multicast-Aspekt des Delegaten nicht genutzt wird. Der Begriff des Multicasts existiert in der Schnittstelle nicht.

Wenn diese beiden Bedingungen erfüllt sind, können wir zwischen den folgenden beiden Signaturen für die Methode Execute wählen:


int Execute(IOperation op, int n1, int n2)
int Execute(Opération op, int n1, int n2)

Die zweite Variante, die den Delegaten verwendet, lässt sich flexibler einsetzen. Bei der ersten Signatur muss der erste Parameter der Methode die Schnittstelle IOperation implementieren. Dies erfordert die Erstellung einer Klasse, um die Methode zu definieren, die als erster Parameter an Execute übergeben werden soll. Bei der zweiten Signatur reicht jede vorhandene Methode mit der richtigen Signatur aus. Es ist keine zusätzliche Konstruktion erforderlich.

8.4. Ereignisverwaltung

Objekt-Delegatklassen können zur Definition von Ereignissen verwendet werden. Eine Klasse C1 kann ein Ereignis evt wie folgt definieren:

  • Ein Typ-Delegat wird innerhalb oder außerhalb der Klasse C1 definiert:
delegate TResult Evt(T1 param1, T2 param2, ...);
  • Die Klasse C1 definiert einen Delegaten Evt:
public Evt Evt1;
  • Wenn eine Instanz c1 der Klasse C1 ein Ereignis melden möchte, führt sie ihren Delegaten Evt1 aus und übergibt ihm die vom Delegaten Evt definierten Parameter. Alle im Delegaten Evt1 registrierten Methoden werden dann mit diesen Parametern ausgeführt. Man kann sagen, dass sie über das Ereignis Evt1 benachrichtigt wurden.
  • Wenn ein Objekt c2, das c1 verwendet, benachrichtigt werden möchte, wenn ein Ereignis Evt1 auf dem Objekt c1 eintritt, registriert es eine seiner Methoden c2.M im Delegatenobjekt c1.Evt1 des Objekts c1. Seine Methode c2.M wird jedes Mal ausgeführt, wenn das Ereignis Evt1 auf dem Objekt c1 eintritt. Es kann sich auch abmelden, wenn es nicht mehr über das Ereignis benachrichtigt werden möchte.
  • Da das delegierte Objekt c1.Evt1 mehrere Methoden registrieren kann, können sich verschiedene c1 bei c1.Evt1 registrieren, um über das Ereignis Evt1 auf c1 benachrichtigt zu werden.

In diesem Szenario haben wir:

  • eine Klasse, die ein Ereignis signalisiert
  • Klassen, die über dieses Ereignis benachrichtigt werden. Man sagt, sie abonnieren das Ereignis.
  • einen Typ-Delegaten, der die Signatur der Methoden definiert, die über das Ereignis benachrichtigt werden

Das .NET-Framework definiert:

  • eine Standardsignatur des Delegaten für ein Ereignis
public delegate void MyEventHandler(object source, EventArgs evtInfo);
  • source: das Objekt, das das Ereignis gemeldet hat
  • evtInfo: ein Objekt vom Typ EventArgs oder einem davon abgeleiteten Typ, das Informationen über das Ereignis bereitstellt
  • Der Name des Delegaten muss mit „EventHandler“ enden
  • Eine gängige Methode, einen MyEventHandler in einer Klasse zu deklarieren:
1
2
3
4
public Class C1{
    public event MyEventHandler Evt1;
...
}

Der Typ Evt1 ist ein Delegat. Das Schlüsselwort „event“ dient dazu, die Operationen einzuschränken, die auf ihn angewendet werden können:

  • Von außerhalb der Klasse C1 sind nur die Operationen += und -= möglich. Dies verhindert, dass Methoden, die das Ereignis abonniert haben, gelöscht werden (beispielsweise durch einen Entwicklerfehler). Sie können das Ereignis einfach abonnieren (+=) oder abbestellen (-=).
  • Nur eine Instanz vom Typ C1 kann den Aufruf Evt1(source,evtInfo) ausführen, der die Ausführung der für Evt1 abonnierten Methoden auslöst.

Das .NET-Framework stellt eine generische Methode bereit, die die Signatur des Delegaten eines Ereignisses erfüllt:

public delegate void EventHandler<TEventArgs>(object source, TEventArgs evtInfo) where TEventArgs : EventArgs
  • Der Delegat EventHandler verwendet den generischen Typ TEventArgs, der dem Typ seines zweiten Parameters entspricht
  • Der Typ TEventArgs muss von EventsArgs abgeleitet sein (where TEventArgs : EventArgs)

Mit diesem generischen Delegaten folgt die Deklaration eines Ereignisses X in der Klasse C dem folgenden empfohlenen Schema:

  • Definieren Sie einen von EventArgs abgeleiteten Typ XEventArgs, um die Ereignisinformationen X zu kapseln
  • Definieren Sie in der Klasse C einen EventHandler<XEventArgs>.
  • Definieren Sie in der Klasse C eine geschützte Methode
protected void OnXHandler(XEventArgs e);

um das Ereignis X an Abonnenten zu „veröffentlichen“.

Betrachten Sie das folgende Beispiel:

  • Eine Klasse Transmitter kapselt eine Temperatur. Diese Temperatur wird überwacht. Wenn diese Temperatur einen bestimmten Schwellenwert überschreitet, wird ein Ereignis ausgelöst. Wir nennen dieses Ereignis TemperatureTropHaute. Die Informationen zu diesem Ereignis werden in einem Typ TemperatureTropHauteEventArgs gekapselt.
  • Eine Klasse „Underwriter“ abonniert das vorgenannte Ereignis. Wenn sie über das Ereignis benachrichtigt wird, zeigt sie eine Meldung auf der Konsole an.
  • Ein Konsolenprogramm erstellt einen Transmitter und zwei Abonnenten. Es gibt die Temperaturen über die Tastatur ein und speichert sie in einem Transmitter. Ist diese zu hoch, veröffentlicht der Transmitter das Ereignis „TemperatureTropHaute“.

Um der empfohlenen Methode des Ereignismanagements zu entsprechen, definieren wir zunächst den Typ „TemperatureTropHauteEventArgs“, um die Ereignisinformationen zu kapseln:


using System;
 
namespace Chap6 {
    public class TemperatureTropHauteEventArgs:EventArgs {
         // temperature during evt
        public decimal Temperature { get; set; }
         // manufacturers
        public TemperatureTropHauteEventArgs() {
        }
        public TemperatureTropHauteEventArgs(decimal temperature) {
            Temperature = temperature;
        }
    }
}
  • Zeile 6: Die von TemperatureTropHauteEventArgs gekapselte Information ist die Temperatur, die das Ereignis TemperatureTropHaute ausgelöst hat.

Die Klasse Transmitter sieht wie folgt aus:


using System;
 
namespace Chap6 {
    public class Emetteur {
        static decimal SEUIL = 19;
 
         // observed temperature
        private decimal temperature;
         // name of source
        public string Nom { get; set; }
         // event reported
        public event EventHandler<TemperatureTropHauteEventArgs> TemperatureTropHaute;
 
         // read/write temperature
        public decimal Temperature {
            get {
                return temperature;
            }
            set {
                temperature = value;
                if (temperature > SEUIL) {
                     // subscribers are notified of the event
                    OnTemperatureTropHaute(new TemperatureTropHauteEventArgs(temperature));
                }
            }
        }
 
         // reporting an event
        protected virtual void OnTemperatureTropHaute(TemperatureTropHauteEventArgs evt) {
             // issue of event TemperatureTropHaute to subscribers
            TemperatureTropHaute(this, evt);
        }
    }
}
  • Zeile 5: Der Temperaturschwellenwert, ab dem das Ereignis „TemperatureTropHaute“ veröffentlicht wird.
  • Zeile 10: Der Absender hat einen Namen zur Identifizierung
  • Zeile 12: das Ereignis „TemperatureTropHaute“.
  • Zeilen 15–26: die Methode get, die die Temperatur ermittelt, und die Methode set, die sie speichert. Dies ist die Methode, die das Ereignis TemperatureTropHaute veröffentlicht, wenn die zu erfassende Temperatur den Schwellenwert in Zeile 5 überschreitet. Sie veröffentlicht das Ereignis mithilfe des OnTemperatureTropHauteHandler aus Zeile 29, indem sie ein TemperatureTropHauteEventArgs übergibt, in dem die Temperatur, die den Schwellenwert überschritten hat, gespeichert wurde.
  • Zeilen 29–32: Das Ereignis „TemperatureTropHaute“ wird mit dem Absender selbst als erstem Parameter und dem als Parameter empfangenen Objekt „TemperatureTropHauteEventArgs“ veröffentlicht.

Die Klasse Underwriter, die das Ereignis TemperatureTropHaute abonniert, sieht wie folgt aus:


using System;
 
namespace Chap6 {
    public class Souscripteur {
         // name
        public string Nom { get; set; }
 
         // event manager TemperatureTropHaute
        public void EvtTemperatureTropHaute(object source, TemperatureTropHauteEventArgs e) {
             // operator console display
            Console.WriteLine("Souscripteur [{0}] : la source [{1}] a signalé une température trop haute : [{2}]", Nom, ((Emetteur)source).Nom, e.Temperature);
        }
    }
}
  • Zeile 6: Jeder Abonnent wird anhand seines Namens identifiziert.
  • Zeilen 9–12: Die Methode, die dem Ereignis „TemperatureTropHaute“ zugeordnet werden soll. Sie hat die Signatur des Typs „EventHandler<TEventArgs>“, über die ein Ereignismanager verfügen muss. Die Methode gibt auf der Konsole Folgendes aus: den Namen des Abonnenten, der die Meldung anzeigt, den Namen des Absenders, der das Ereignis gemeldet hat, sowie die Temperatur, die das Ereignis ausgelöst hat.
  • Die Ereignisabonnierung „TemperatureTropHaute“ eines Objekts vom Typ „Transmitter“ erfolgt nicht in der Klasse „Underwriter“. Sie wird von einer externen Klasse übernommen.

Das Programm [Program.cs] verknüpft all diese Elemente miteinander:


using System;
namespace Chap6 {
    class Program {
        static void Main(string[] args) {
             // creation of an evts transmitter
            Emetteur e1 = new Emetteur() { Nom = "e" };
             // creation of a table of 2 subscribers
            Souscripteur[] souscripteurs = new Souscripteur[2];
            for (int i = 0; i < souscripteurs.Length; i++) {
                 // creation subscriber
                souscripteurs[i] = new Souscripteur() { Nom = "s" + i };
                 // we subscribe him to e1's TemperatureTropHaute event
                e1.TemperatureTropHaute += souscripteurs[i].EvtTemperatureTropHaute;
            }
             // temperatures are read from the keyboard
            decimal temperature;
            Console.Write("Température (rien pour arrêter) : ");
            string saisie = Console.ReadLine().Trim();
             // as long as the line entered is not empty
            while (saisie != "") {
                 // is the input a decimal number?
                if (decimal.TryParse(saisie, out temperature)) {
                     // correct temperature - recorded
                    e1.Temperature = temperature;
                } else {
                    // on signale l'erreur
                    Console.WriteLine("Température incorrecte");
                }
                 // new entry
                Console.Write("Température (rien pour arrêter) : ");
                saisie = Console.ReadLine().Trim();
             }//while
        }
    }
}
  • Zeile 6: Erstellung des Senders
  • Zeilen 8–14: Erstellung von zwei Abonnenten für das Ereignis „TemperatureTropHaute“ des Senders.
  • Zeilen 20–32: Schleife zur Temperatureingabe über die Tastatur
  • Zeile 24: Wenn die eingegebene Temperatur korrekt ist, wird sie an das Objekt Transmitter e1 übertragen, das das Ereignis TemperatureTropHaute auslöst, wenn die Temperatur über 19 °C liegt.

Die Ergebnisse lauten wie folgt:

1
2
3
4
Température (rien pour arrêter) : 17
Température (rien pour arrêter) : 21
Souscripteur [s0] : la source [e] a signalé une température trop haute : [21]
Souscripteur [s1] : la source [e] a signalé une température trop haute : [21]