So werden Ereignisse in .NET gemeldet und behandelt
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
Diesen Artikel können Sie hier lesen dank freundlicher Unterstützung der Zeitschrift:
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.

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.