So werden Ereignisse in .NET gemeldet und behandelt

Veröffentlicht: 27. Jan 2002 | Aktualisiert: 13. Jun 2004

Von Jeffrey Richter

Mit Hilfe von Delegates können sich Objekte in eine Liste eintragen, um über Ereignisse informiert zu werden. Wir zeigen Ihnen Schritt für Schritt, wie Sie diesen Mechanismus für sich nutzen können.

Auf dieser Seite

Entwurf eines Typs, der Ereignisse anbietet
Entwurf eines Empfängertyps

Diesen Artikel können Sie hier lesen dank freundlicher Unterstützung der Zeitschrift:

Bild02

In meinen beiden letzten .NET-Beiträgen habe ich einen Blick auf die Delegierten (Delegates) geworfen und mich damit beschäftigt, wie sie in Anwendungen, die für das .NET Framework vorgesehen sind, entworfen und benutzt werden. Diesmal möchte ich auf den wohl wichtigsten Verwendungszweck der Delegierten eingehen, nämlich auf die Meldung von Ereignissen.
Ereignismeldungen ermöglichen es einem Objekt, andere Objekte darüber zu informieren, das etwas Bestimmtes geschehen ist. Wird zum Beispiel eine Schaltfläche angeklickt, so wollen normalerweise ein oder mehrere Objekte in der Anwendung darüber informiert werden, damit sie bestimmte Arbeiten durchführen können. Ereignisse (Events) sind nun bestimmte Bestandteile einer Klasse, die solche Vorgänge ermöglichen. Die Definition eines Ereignisses bedeutet, dass ein bestimmter Bestandteil der Klasse Folgendes ermöglicht:

  • Andere Objekte können ihr Interesse an dem Ereignis bekunden und sich anmelden.

  • Die Objekte können ihr Interesse an dem Ereignis bei Bedarf auch wieder zurückziehen.

  • Das Objekt, das für dieses Ereignis oder die Ereignismeldung zuständig ist, ist in der Lage, über die angemeldeten Objekte Buch zu führen und die Objekte über das betreffende Ereignis zu informieren, sobald es eintritt.

Lassen Sie mich zum besseren Verständnis ein Szenario skizzieren, in dem Ereignismeldungen von Nutzen sind. Nehmen wir an, Sie möchten eine E-Mail-Anwendung entwickeln. Der Anwender möchte vielleicht, dass eine E-Mail nach ihrem Eintreffen an ein Faxgerät oder an einen Pager weitergeleitet wird. Bei der Konzeption solch einer Anwendung würde ich zuerst einen Typ entwerfen, der die eintreffenden E-Mails annimmt. Nennen wir ihn MailManager. Der MailManager bietet ein Ereignis an, das MailMsg genannt wird. Andere Typen (zum Beispiel Fax und Pager) können ihr Interesse an diesem Ereignis anmelden. Sobald der MailManager dann eine neue E-Mail erhält, meldet er das Ereignis weiter, wobei er die E-Mail an jedes angemeldete Objekt weitergibt. Jedes dieser Objekte kann nun die E-Mail nach Bedarf bearbeiten.
Beim Start der Anwendung würde ich nur eine Instanz des MailManagers anlegen. Von den Fax- und Pager-Typen kann ich aber so viele Instanzen anlegen, wie der Anwender für erforderlich hält. Bild B1 zeigt die Initialisierung der Anwendung und beschreibt die Geschehnisse beim Eintreffen einer neuen E-Mail.

Bild01

B1 Der Ablauf beim Eintreffen einer neuen E-Mail

Lassen Sie mich kurz beschreiben, wie das System funktioniert. Meine Anwendung legt bei ihrer Initialisierung eine Instanz des MailManagers an. Der MailManager bietet ein MailMsg-Ereignis an. Wenn die Fax- und Pager-Objekte angelegt werden, melden sie sich für das MailMsg-Ereignis beim MailManager an. Daher weiß der MailManager, dass er die Fax- und Pager-Objekte informieren muss, wenn eine neue E-Mail eintrifft. Sobald also der MailManager eine neue E-Mail erhält, meldet er das MailMsg-Ereignis und gibt somit allen angemeldeten Objekten die Gelegenheit, die neue E-Mail in der gewünschten Weise zu bearbeiten.

Entwurf eines Typs, der Ereignisse anbietet

Nachdem Sie nun erfahren haben, worum es eigentlich geht, sollten wir einen Blick auf die Typdefinition des MailManagers werfen. Der Code in Listing L1 zeigt das empfohlene Entwurfsmuster, das zum Angebot von Ereignissen befolgt werden sollte. Die gesamte Arbeit, die mit der Implementierung der Architektur verbunden ist, wird praktisch dem Entwickler aufgebürdet, der den MailManager-Typ definiert. Der Entwickler muss die folgenden fünf Dinge erledigen.

L1 Das Entwurfsmuster für das Angebot von Ereignissen

class MailManager { 
   public class MailMsgEventArgs : EventArgs { 
      // 1. Der Typ definiert die Informationen, die an 
      //    die Empfänger der Ereignismeldungen übergeben 
      //    werden. 
      public MailMsgEventArgs( 
         String from, String to, String subject, String body) { 
         this.from    = from;  
         this.to      = to;  
         this.subject = subject;  
         this.body    = body;  
      } 
      public readonly String from, to, subject, body;  
   } 
   // 2. Der Delegiertentyp, der den Prototyp der aufzu- 
   //    rufenden Methode definiert. Diese Methode wird 
   //    vom Empfänger implementiert. 
   public delegate void MailMsgEventHandler( 
      Object sender, MailMsgEventArgs args); 
   // 3. Das Ereignis selbst 
   public event MailMsgEventHandler MailMsg; 
   // 4. Diese geschützte virtuelle Methode ist für die 
   //    Benachrichtigung der angemeldeten Empfänger 
   //    zuständig. 
   protected virtual void OnMailMsg(MailMsgEventArgs e) { 
      // Interessiert sich überhaupt ein Objekt für  
      // dieses Ereignis? 
      if (MailMsg != null) { 
         // Ja. Informiere alle Objekte, die in der  
         // Liste zu finden sind. 
         MailMsg(this, e); 
      } 
   } 
   // 5. Diese Methode übersetzt die Eingabe in die  
   //    gewünschte Ereignismeldung. Die Methode wird 
   //    aufgerufen, sobald eine neue E-Mail eintrifft. 
   public void SimulateArrivingMsg(String from, String to, 
      String subject, String body) { 
      // Konstruiere ein Objekt, das die Informationen 
      // aufnimmt, die ich an die Empfänger der Ereignis- 
      // meldung übermitteln möchte. 
      MailMsgEventArgs e =  
         new MailMsgEventArgs(from, to, subject, body); 
      // Rufe meine virtuelle Methode auf, damit mein  
      // Objekt über das Ereignis informiert wird. Sofern 
      // kein abgeleiteter Typ die Methode überschreibt, 
      // informiert mein Objekt alle angemeldeten 
      // Empfänger. 
      OnMailMsg(e); 
   } 
}

Erstens definieren Sie einen Typ, der die zusätzlichen Informationen aufnehmen kann, die an die Empfänger der Ereignismeldung übermittelt werden sollen. Per Konvention werden Typen, die mit Ereignisinformationen hantieren, von System.EventArgs abgeleitet und der Name des Typs endet auf "EventArgs". In diesem Beispiel hat der Typ MailMsgEventArgs Felder, aus denen der Absender der Nachricht hervorgeht (from), der Empfänger (to), der Gegenstand der Nachricht (subject) und der eigentliche Nachrichtentext (body).
Der EventArgs-Typ leitet sich von Object ab und sieht folgendermaßen aus:

[Serializable] 
public class EventArgs { 
   public static readonly EventArgs Empty = new EventArgs(); 
   public EventArgs() {  } 
}

Wie Sie sehen, gibt es über diesen Typ nicht viel zu erzählen. Er dient einfach als Basistyp, von dem andere Typen abgeleitet werden. Bei vielen Ereignismeldungen müssen keine zusätzlichen Informationen übermittelt werden. Will eine Schaltfläche zum Beispiel ihre angemeldeten Empfänger über einen Klick informiert, reichte es völlig aus, die dafür vorgesehenen Methoden aufzurufen. Wenn Sie ein Ereignis definieren, bei dem keine zusätzlichen Daten übergeben werden müssen, benutzen Sie einfach EventArgs.Empty.
Zweitens definieren Sie einen Delegiertentyp, aus dem der Prototyp der Methode hervorgeht, die zur Meldung des Ereignisses aufgerufen wird. Per Konvention endet der Name des Delegierten auf "EventHandler". Außerdem hat der Prototyp per Konvention den Ergebnistyp void und zwei Parameter (allerdings gibt es einige Handler wie ResolveEventHandler, die sich nicht an diese Konvention halten). Der erste Parameter ist ein Object, das sich auf das Objekt bezieht, von dem die Ereignismeldung stammt. Der zweite Parameter ist ein von EventArgs abgeleiteter Typ, der bei Bedarf die erforderlichen zusätzlichen Informationen für die Empfänger enthält.
Sofern Sie ein Ereignis definieren, bei dessen Meldung keine zusätzlichen Informationen an die Empfänger übergeben werden müssen, brauchen Sie auch keinen neuen Delegierten zu definieren. Sie können den Delegierten System.EventHandler benutzen und als Argument für den zweiten Parameter EventArgs.Empty übergeben. EventHandler hat folgenden Prototypen:

public delegate void EventHandler(Object sender, EventArgs e); 

Drittens definieren Sie ein Ereignis. In diesem Fall lautet der Name des Ereignisses auf MailMsg. Das Ereignis ist vom Typ MailMsgEventHandler. Das bedeutet, dass die Empfänger der entsprechenden Ereignismeldung eine Methode bereitstellen müssen, deren Prototyp zum MailMsgEventHandler-Delegierten passt.
Viertens definieren Sie eine geschützte virtuelle Methode, die für den Versand der Ereignismeldung an die angemeldeten Empfänger zuständig ist. Die Methode OnMailMsg wird aufgerufen, sobald eine neue E-Mail eintrifft. Diese Methode erhält ein initialisiertes MailMsgEventArgs-Objekt, in dem die zusätzlichen Informationen über das Ereignis zu finden sind. Diese Methode sollte zuerst überprüfen, ob sich überhaupt irgendwelche Objekte für dieses Ereignis angemeldet haben. Ist das der Fall, sollte das Ereignis natürlich gemeldet werden.
Ein Typ, der MailManager als Basistyp benutzt, kann die Methode OnMailMsg überschreiben. Das gibt dem abgeleiteten Typ die Kontrolle über die Meldung der Ereignisse. Der abgeleitete Typ kann mit neuen Ereignismeldungen so umgehen, wie er es für richtig hält. Im Normalfall wird der abgeleitete Typ die OnMailMsg-Methode des Basistyps aufrufen, damit die angemeldeten Objekte die entsprechenden Ereignismeldungen erhalten. Allerdings kann sich der abgeleitete Typ bei Bedarf auch dafür entscheiden, das Ereignis gar nicht weiterzumelden.
Fünftens definieren Sie eine Methode, die für die Umsetzung der Eingangsdaten in die gewünschte Ereignismeldung sorgt. Ihr Typ muss eine Methode haben, die in irgendeiner Form Eingaben annimmt und diese Eingaben in eine Ereignismeldung umsetzt. In diesem Beispiel wird als Hinweis darauf, dass eine neue E-Mail beim MailManager eingetroffen ist, die Methode SimulateArrivingMsg aufgerufen. SimulateArrivingMsg akzeptiert Informationen über die E-Mail und baut ein neues MailMsgEventArgs-Objekt zusammen, wobei die Informationen über die E-Mail an den Konstruktor übergeben werden. Anschließend wird die eigene virtuelle OnMailMsg-Methode von MailManager aufgerufen. Dadurch wird das MailManager-Objekt formal über die neue E-Mail informiert. Normalerweise wird bei dieser Gelegenheit ein Ereignis gemeldet, was zur Benachrichtigung aller interessierten Objekte führt. Allerdings kann ein Typ, der sich von MailManager ableitet, dieses Verhalten überschreiben.
Sehen wir uns nun etwas genauer an, was die Definition des MailMsg-Ereignisses eigentlich bedeutet. Wenn der Compiler den Quelltext analysiert, stößt er irgendwann auf die Zeile, in der das Ereignis definiert wird:

public event MailMsgEventHandler MailMsg; 

Der C#-Compiler übersetzt diese eine Codezeile in drei Konstrukte, wie in Listing L 2 beschrieben. Das erste Konstrukt ist einfach ein Feld, das im Typ definiert wird. Dieses Feld stellt eine Referenz auf den Kopf einer verketteten Liste dar, in der die Delegierten erfasst werden, die über das Ereignis informiert werden möchten. Das Feld wird mit null initialisiert, was nichts Anderes bedeutet, als dass sich noch keine Interessenten für die Ereignismeldung eingetragen haben. Sobald sich ein Interessent für das Ereignis anmeldet, enthält das Feld eine Referenz auf eine Instanz des MailMsgEventHandler-Delegierten. In der betreffenden MailMsgEventHandler-Instanz gibt es wieder einen Zeiger auf einen weiteren MailMsgEventHandler-Delegierten. Das Ende der Liste wird durch eine null gekennzeichnet. Ein Empfänger bekundet nun sein Interesse an dem Ereignis, indem er einfach eine Instanz des Delegiertentyps in die verkettete Liste einträgt. Interessiert ihn das Ereignis nicht mehr, streicht er den Delegierten wieder aus der Liste.

L2 Der Compiler generiert aus einer Ereignisdefinition drei Konstrukte

// 1. Ein privates Delegiertenfeld, das mit null 
//    initialisiert wird 
private MailMsgEventHandler MailMsg = null; 
// 2. Eine öffentliche add_-Methode. 
//    Mit ihr bekunden die Objekte ihr Interesse an 
//    dem Ereignis 
MethodImplAttribute(MethodImplOptions.Synchronized)] 
public void add_MailMsg(MailMsgEventHandler handler) { 
   MailMsg = (MailMsgEventHandler) 
      Delegate.Combine(MailMsg, handler); 
} 
// 3. Eine öffentliche remove_-Methode. 
//    Ermöglicht den Objekten, sich wieder aus der 
//    Liste der Empfänger zu streichen 
[MethodImplAttribute(MethodImplOptions.Synchronized)] 
public void remove_Click (MailMsgEventHandler handler) { 
   MailMsg = (MailMsgEventHandler) 
      Delegate.Remove(MailMsg, handler); 
} 

Vermutlich ist Ihnen bereits aufgefallen, dass das Ereignisfeld (in diesem Fall MailMsg) privat ist, selbst wenn die ursprüngliche Quelltextzeile das Ereignis als öffentlich definiert. Damit soll verhindert werden, dass das Feld von anderen Codeteilen, die nicht zum Typ gehören, versehentlich oder absichtlich geändert wird. Nur der MailManager erfährt es, wenn eine neue E-Mail eintrifft. Also kann nur er sinnvoll entscheiden, wann die Ereignismeldungen abzuschicken sind. Wäre das Feld öffentlich, könnte jeder beliebige Codeabschnitt jederzeit ein Ereignis melden, selbst wenn gar keines eingetreten ist.
Beim zweiten Konstrukt, das der C#-Compiler generiert, handelt es sich um eine Methode, mit der andere Objekte ihr Interesse an dem Ereignis anmelden können. Der C#-Compiler generiert den Namen automatisch, indem er dem Feldnamen (MailMsg) ein "add_" voranstellt. Außerdem generiert der C#-Compiler auch den Code automatisch, der in dieser Methode zu finden ist. Der Code ruft immer die statische Combine-Methode von System.Delegate auf, die eine Instanz des Delegierten in die verkettete Liste einträgt und den neuen Kopf der verketteten Liste zurückgibt.
Das dritte und letzte Konstrukt, das der C#-Compiler generiert, ist eine Methode, die es einem Objekt erlaubt, sich auch wieder aus der Liste der Interessenten zu streichen. Auch hier konstruiert der C#-Compiler den Namen der Funktion automatisch, indem er dem Feldnamen (MailMsg) ein "remove_" voranstellt. Der Code in dieser Methode ruft immer die statische Methode Remove des Delegierten auf, die den Eintrag des Delegierten aus der verketteten Liste entfernt und den neuen Kopf der Liste zurückgibt.
Für die add- und remove-Methoden wurde das MethodImplAttribute-Attribut angegeben. Genauer gesagt, diese Methoden wurden als synchronisiert gekennzeichnet. Dadurch werden sie thread-sicher. Nun können sich mehrere Interessenten gleichzeitig an- oder abmelden, ohne die Liste ungewollt zu zerstören.
In meinem Beispiel sind die add- und remove-Methoden öffentlich, weil das Ereignis in der ursprünglichen Quelltextzeile als öffentliches Ereignis deklariert wurde. Wäre es als geschütztes Ereignis deklariert worden, hätte der Compiler auch die add- und remove-Methoden als protected deklariert. Wenn Sie also ein Ereignis in einem Typ definieren, geht aus der Zugänglichkeit des Ereignisses hervor, welcher Code Interesse am Ereignis anmelden kann. Nur der Typ selbst kann aber das Ereignis melden.
Der Compiler wirft nicht nur die drei gezeigten Konstrukte aus, sondern trägt die Ereignisdefinition auch noch in die Metadaten des verwalteten Moduls ein. Dieser Eintrag enthält einige Flags, den dazugehörigen Delegiertentyp und Bezüge auf die add- und remove-Zugriffsmethoden. Sinn dieser Informationen ist es, einen Bezug zwischen dem abstrakten Konzept eines Ereignisses und den Zugriffsmethoden herzustellen. Compiler und andere Werkzeuge können auf die Metadaten zurückgreifen. Wahrscheinlich sind diese Informationen auch über die System.Reflection.EventInfo-Klasse zugänglich. Die CLR selbst (common language runtime) benutzt diese Metadaten aber nicht und verlangt zur Laufzeit nur die Zugriffsmethoden.

Entwurf eines Empfängertyps

Damit liegt der schwerste Teil der Arbeit eindeutig hinter Ihnen. In diesem Abschnitt geht es nun darum, wie man einen Typ definiert, der die Ereignismeldungen eines anderen Typs auswertet. Beginnen möchte ich diese Betrachtung mit dem Fax-Typ aus Listing L3.

L3 Der Fax-Typ

class Fax { 
   // Übergibt das MailManager-Objekt an den Konstruktor 
   public Fax(MailManager mm) { 
      // Lege eine Instanz vom MailMsgEventHandler-Dele- 
      // gierten an, die sich auf unsere FaxMsg-Methode 
      // bezieht. Melde unsere Methode beim MailMsg- 
      // Ereignis vom MailManager an. 
      mm.MailMsg += new MailManager.MailMsgEventHandler(FaxMsg); 
   } 
   // Diese Methode ruft der MailManager auf, wenn er das 
   // Fax-Objekt über den Eingang einer E-Mail infor- 
   // mieren möchte. 
   private void FaxMsg( 
      Object sender, MailManager.MailMsgEventArgs e) { 
      // 'sender' bezieht sich auf den MailManager, für 
      //  den Fall, dass wir mit ihm kommunizieren müssen 
      // 'e' bezieht sich auf zusätzliche Informationen, 
      // die uns der MailManager über das Ereignis 
      // geben möchte. 
      // Normalerweise würde dieser Code die E-Mail als 
      // Fax weiterschicken. Dieses Beispiel zeigt die 
      // Daten einfach auf der Konsole an. 
      Console.WriteLine("Faxing mail message:"); 
      Console.WriteLine( 
         "   To: {0}\n   From: {1}\n   Subject: {2}\n   Body: {3}\n", 
         e.from, e.to, e.subject, e.body); 
   } 
   public void Unregister(MailManager mm) { 
      // Baue eine Instanz des MailMsgEventHandler-Dele- 
      // gierten, die sich auf die FaxMsg-Methode bezieht 
      MailManager.MailMsgEventHandler callback =  
         new MailManager.MailMsgEventHandler(FaxMsg); 
      // Nun streiche mich aus der MailMsg-Liste vom 
      // MailManager. 
      mm.MailMsg -= callback; 
   } 
}

Bei ihrem Start wird eine E-Mail-Anwendung zuerst ein MailManager-Objekt anlegen und in einer Variablen eine Referenz auf dieses Objekt festhalten. Dann wird die Anwendung ein Fax-Objekt anlegen und dabei eine Referenz auf das MailManager-Objekt als Argument übergeben. Im Fax-Konstruktor entsteht ein neues mailManager.mailMsgEventHandler-Delegiertenobjekt. In der Variablen callback wird eine Referenz auf dieses Objekt gespeichert. Das neue Delegiertenobjekt ist eine Hülle für die Methode FaxMsg des Typs Fax. Sie werden feststellen, dass die Methode FaxMsg den Ergebnistyp void und dieselben beiden Parameter hat, die vom Delegierten MailMsgEventHandler des MailManagers definiert werden. Das ist erforderlich, damit sich der Code kompilieren lässt.
Nach dem Zusammenbau des Delegierten meldet das Fax-Objekt mit folgender Zeile sein Interesse am MailMsg-Ereignis vom MailManager an:

mm.MailMsg += callback

Da der C#-Compiler von Haus aus mit Ereignissen umgehen kann, übersetzt er den Operator += in die folgende Codezeile, mit der die Objektreferenz in die Liste der Interessenten aufgenommen wird:
mm.add_MailMsg(callback};
Falls Sie mit einer Programmiersprache arbeiten, die Ereignisse nicht von Haus aus versteht, können Sie die Delegierten mit Hilfe der Zugriffsmethoden immer noch explizit beim Ereignis eintragen. Unter dem Strich ist das Ergebnis dasselbe - nur der Quelltext sieht anders aus. Für den Eintrag des Delegierten in die Liste der Interessenten ist natürlich die add-Methode zuständig.
Sobald der MailManager das Ereignis meldet, wird die FaxMsg-Methode des Fax-Objekts aufgerufen. Die Methode erhält beim Aufruf eine Referenz auf das MailManager-Objekt. Meistens wird dieser Parameter ignoriert, aber er ist von Nutzen, wenn das Fax-Objekt in Reaktion auf die Nachricht auf Felder oder Methoden des MailManager-Objekts zugreifen muss. Der zweite Parameter ist eine Referenz auf ein MailMsgEventArgs-Objekt. Dieses Objekt enthält alle zusätzlichen Informationen, von denen der MailManager annimmt, dass sie für den Empfänger der Nachricht von Nutzen sein könnten.
Mit dem MailMsgEventArgs-Objekt hat die FaxMsg-Methode leichten Zugang auf die folgenden Angaben über die eingetroffene E-Mail: den Absender, den Empfänger der Mail, das Thema und den eigentlichen Text der Mail. Ein echtes Fax-Objekt würde diese Informationen irgendwohin faxen. In diesem Beispiel werden die Daten einfach im Konsolenfenster angezeigt.
Auch wenn es etwas ungewöhnlich ist, kann ein Objekt sein Interesse an den Ereignismeldungen von anderen Objekten wieder zurückziehen. Der Code in der Unregister-Methode des Fax-Objekts zeigt, wie sich das Objekt wieder aus der Interessentenliste austrägt (Listing L3). Diese Methode ist praktisch mit dem Fax-Konstruktor identisch. Der einzige Unterschied liegt im Operator -=, der statt += eingesetzt wird. Wenn der Compiler auf den Operator -= stößt, mit dem sich ein Delegierter aus der Liste der Meldungsempfänger streichen will, generiert er dafür einen Aufruf der entsprechenden remove-Methode:
mm.remove_MailMsg(callback};
Falls Sie mit einer Programmiersprache arbeiten, die Ereignisse nicht von Haus aus kennt, können Sie den Delegierten wieder mit dem expliziten Aufruf der remove-Methode aus der Interessentenliste streichen. Die remove-Methode durchsucht die verkettete Liste nach einem Delegierten, der in seiner Aufgabe als Stellvertreter dieselbe Methode vertritt wie der angegebene Delegierte. Wird sie fündig, entfernt sie den vorhandenen Delegierten aus der Liste. Findet sie keinen passenden Stellvertreter der aufzurufenden Funktion, wird aber keine Fehlermeldung ausgelöst. Die Liste bleibt unverändert. Das ist alles.
Übrigens schreibt C# gnadenlos vor, dass Sie die Einträge in der Interessentenliste mit den Operatoren -= und += vornehmen. Falls Sie versuchen, die add- oder remove-Methode explizit aufzurufen, beschwert sich der Compiler mit einem "cannot explicitly call operator or accessor".
Sie finden das MailManager-Beispielprogramm wie üblich auf der Begleit-CD dieses Hefts. Es umfasst die Quelltexte für die Typen MailManager, Fax und Pager, wobei sich die Implementierung von Pager weitgehend an Fax anlehnt.