5.2 Ereignisse eines Objekts 

Die meisten Objekte, mit denen wir es täglich zu tun haben, reagieren auf Anstöße von außen: Ein Auto kann hupen und fahren, eine Person kann gehen und sprechen. Äußere Anstöße können – projiziert auf den Programmentwurf – Methodenaufrufen gleichgesetzt werden. Ein Client verwaltet beispielsweise ein Objekt vom Typ Car und ruft die Methode Fahren auf. Daraufhin setzt sich das Auto in Bewegung – zumindest aus dem programmiertechnischen Blickwinkel heraus.
Die Erfahrung des täglichen Lebens zeigt aber auch, dass Objekte auf diese Anstöße ihrerseits selbst reagieren können. Stellen Sie sich vor, Sie würden in einem Mietshaus wohnen und die Lautstärke der Stereoanlage zu hoch drehen. Wenn Sie Glück haben, werden Ihre Nachbarn das stillschweigend akzeptieren, möglicherweise werden Sie aber auch soeben das Klingeln an der Wohnungstür vernehmen und einem vielleicht freundlich, vielleicht auch verärgert um mehr Ruhe bittenden Nachbarn begegnen.
Ein Methodenaufruf ist das Verfahren, mit dem ein Client einem Objekt einen Anstoß gibt, damit dieses eine bestimmte Verhaltensweise zeigt. Die Konsequenz eines Methodenaufrufs könnte sein, dass das Objekt seinerseits bei seinem Aufrufer eine Reaktion auslöst.
Diese Reaktion lässt sich ebenfalls programmiertechnisch erfassen: Sie wird als Ereignis oder auch mit dem englischen Begriff Event bezeichnet. Ereignisse spielen eine herausragende Rolle bei der Programmierung grafischer Benutzeroberflächen. Ereignisse lassen sich so abstrahieren, dass sie als Nachrichtenverkehr zwischen einer Ereignisquelle und einem Ereignisempfänger angesehen werden können. Eine Ereignisquelle könnte beispielsweise die Schaltfläche in einem Windows-Fenster sein. Sobald der Anwender mit der Maus auf die Schaltfläche klickt, wird ein Ereignis ausgelöst, auf das der Ereignisempfänger reagieren kann, aber nicht reagieren muss.
Hinweis |
Die Richtung eines klassischen Methodenaufrufs geht immer vom Aufrufer zum Objekt. Das Objekt führt danach die Methode aus. Die Richtung eines Ereignisses ist genau entgegengesetzt: Sie geht vom Objekt zurück zum Aufrufer der Objektmethode, der auch als Ereignisempfänger bezeichnet wird. Aus diesem Blickwinkel betrachtet, ruft ein Objekt als Ereignisquelle eine ihm bekannte Methode im Client, dem Ereignisempfänger, auf. |
Der Zeitpunkt der Ereignisauslösung ist im Code eines Objekts festgelegt. Das Besondere an einem Event ist, dass der Ereignisempfänger auf die Auslösung individuell reagieren kann, sie unter Umständen sogar einfach ignoriert.
5.2.1 Ereignis in einer Ereignisquelle ergänzen 

Die theoretische Betrachtung eines Ereignisses soll nun in ein praktisches Beispiel umgesetzt werden. Erinnern wir uns dazu zunächst an die aktuelle Implementierung der Eigenschaftsmethode Radius in der Circle-Klasse:
Public virtual double Radius {
get{return _Radius;}
set{
if(value >= 0)
_Radius = value;
else
Console.Write("Unzulässiger Wert.");
}
}
Uns interessiert insbesondere der set-Accessor und dort wiederum das Verhalten der Methode, wenn der Eigenschaft ein negativer Wert übergeben wird. Nach dem derzeitigen Stand führt das zu einem Nachrichtenhinweis auf der Konsole.
Der Code funktioniert tadellos, unterliegt jedoch einer Einschränkung: Der Client muss die Nachricht entgegennehmen – ob er will oder nicht. Anstatt an der Konsole immer nur dieselbe gleichlautende Meldung anzuzeigen, könnte das Circle-Objekt im Client eine Methode aufrufen und ihm damit signalisieren, dass die Wertübergabe nicht korrekt war. Der Client kann als Ereignisempfänger auf das Ereignis reagieren, indem in ihm eine bestimmte Methode ausgeführt wird.
Bisher hatten wir es immer mit der Aufrufrichtung zu tun, die von einem Client zu einem Objekt verläuft. Ein Ereignis dreht diese Richtung um. Um die Wirkungsweise der Ereignisse besser zu verstehen, wollen wir nun die Klasse Circle um ein solches Ereignis erweitern, das wir InvalidRadius nennen. Der Programmablauf bis zu einer eventuellen Ereignisauslösung würde wie folgt aussehen:
1. | Der Client erzeugt ein Objekt der Klasse Circle und weist der Eigenschaft Radius einen unzulässigen Wert zu. |
2. | In der Eigenschaftsmethode Radius wird der übergebene Wert geprüft, die Unzulässigkeit festgestellt und das Ereignis InvalidRadius ausgelöst – mit der Folge, dass im Client nach einer Methode gesucht wird, die das Ereignis behandelt. |
3. | Erklärt sich der Client bereit, das Ereignis zu behandeln, wird im Client die dem Ereignis zugeordnete Methode ausgeführt. |
Kommen wir nun zu den Details der Ereignisimplementierung in der Ereignisquelle. Jedes Ereignis muss in der Klassendefinition bekannt gegeben werden. Die allgemeine Syntax einer Ereignisdefinition lautet wie folgt:
[<Zugriffsmodifizierer>] event <Delegate-Typ> <Event-Bezeichner>;
Dem optionalen Zugriffsmodifizierer (der Standard ist private) folgt das Schlüsselwort event, und dahinter wird der Typ des Ereignisses bekannt gegeben, der ein Delegate ist. Weil ein Delegate den Zeiger auf eine Methode mit einer bestimmten Parameterliste und einem bestimmten Rückgabetyp beschreibt, wird damit gleichzeitig die ereignisbehandelnde Methode im Client spezifiziert. Abgeschlossen wird die Deklaration mit dem Bezeichner des Ereignisses.
Hinweis |
Delegates, die den Ereignissen als Typvorgabe dienen, haben im .NET Framework per Konvention das Suffix EventHandler. |
Unsere Anwendung müssen wir demnach um eine Delegate-Definition ergänzen, und wir müssen in der Klasse Circle einen Event vom Typ dieses Delegates deklarieren, um der selbst gestellten Anforderung zu genügen:
// ---------- Delegate ----------
public delegate void InvalidRadiusEventHandler();
public class Circle : IDisposable {
// ---------- Ereignis ----------
public event InvalidRadiusEventHandler InvalidRadius;
...
}
Der Ausgangspunkt unserer Überlegungen war, bei einer unzulässigen Zuweisung an die Eigenschaft Radius eines Circle-Objekts das Ereignis InvalidRadius auszulösen. Die Ereignisauslösung muss genau zu dem Zeitpunkt erfolgen, wenn die Überprüfung des Übergabewertes zu einer Ablehnung geführt hat. Die Auslösung selbst ist trivial, wir brauchen dazu nur den Namen des Ereignisses anzugeben, in unserem Beispiel also:
InvalidRadius();
Diese Anweisung ersetzt in der Eigenschaft Radius die Konsolenausgabe im else-Zweig des set-Accessors:
public virtual double Radius {
get {return _Radius;}
set {
if(value >= 0)
_Radius = value;
else
// Ereignis auslösen
InvalidRadius();
}
}
Übergibt der Client der Eigenschaft Radius nun einen Wert, der der Bedingung
Radius < 0
entspricht, wird das Delegate aktiv und sucht im Aufrufer nach einer parameterlosen Methode ohne Rückgabewert.
5.2.2 Behandlung eines Ereignisses im Ereignisempfänger 

Wie sich der Ereignisempfänger verhält, ob er die Ereignisauslösung ignoriert oder darauf reagiert, bleibt ihm selbst überlassen. Es ist eine Option, die wahrgenommen werden kann oder auch nicht.
In Kenntnis der Tatsache, dass ein Circle-Objekt ein Ereignis auslösen kann, wenn der Eigenschaft Radius ein unzulässiger Wert übergeben wird, entwickeln wir zunächst eine Methode, die bei der Auslösung des Ereignisses InvalidRadius ausgeführt werden soll. Solche Methoden werden auch als Ereignishandler bezeichnet. Da der Typ dieses Ereignisses ein parameterloses Delegate ist, muss die Parameterliste unserer Methode natürlich leer sein.
public class Program { static void Main(string[] args) { Circle kreis = new Circle(); ... } public static void kreis_InvalidRadius() { Console.WriteLine("Unzulässiger negativer Radius."); } }
Hinweis |
Es ist nicht nur unter .NET üblich, einem Ereignishandler nach einem bestimmten Muster einen Bezeichner zu geben. Dabei wird zuerst der Objektname angegeben, gefolgt von einem Unterstrich und dem sich anschließenden Ereignisbezeichner, also Objektname_Ereignisname Sie können selbstverständlich von dieser Konvention abweichen. Die vom Visual Studio automatisch generierten Ereignishandler folgen alle diesem Namensmuster, dem allerdings Grenzen gesetzt sind, wie Sie gleich noch sehen werden. |
Wir können dem Objekt kreis nun einen Radius von beispielsweise -1 zuweisen, aber die Methode kreis_InvalidRadius würde daraufhin nicht ausgeführt. Woher soll das Objekt auch wissen, welche Methode im Client ausgeführt werden soll, wenn das Ereignis InvalidRadius ausgelöst wird? Es könnten schließlich x-beliebig viele parameterlose Methoden in der Clientklasse definiert sein und prinzipiell als Ereignishandler in Frage kommen. Außerdem ist dem Circle-Objekt auch der Bezeichner der aufzurufenden Methode völlig unbekannt.
Wir müssen per Anweisung den von uns bereitgestellten Ereignishandler an das Ereignis InvalidRadius des Objekts binden. Dazu übergeben wir dem Ereignis des Objekts mit dem +=-Operator eine Instanz des Delegates InvalidRadiusEventHandler, wobei wir den Bezeichner des Handlers angeben:
kreis.InvalidRadius +=
new InvalidRadiusEventHandler(kreis_InvalidRadius);
Natürlich ist auch die Kurzform
kreis.InvalidRadius += kreis_InvalidRadius;
erlaubt. Die einzige Bedingung ist, dass die dem Konstruktor genannte Methode den vom Delegate festgelegten Kriterien hinsichtlich Parameterliste und Rückgabewert genügt. Unser Testcode in Main könnte nun wie folgt lauten:
public void Main(string[] args) {
Circle kreis = new Circle();
kreis.InvalidRadius += kreis_InvalidRadius;
kreis.Radius = -1;
Console.ReadLine();
}
Dazu noch der Ereignishandler:
public void kreis_InvalidRadius () { Console.WriteLine("Unzulässiger negativer Radius."); }
Wenn wir Code ausführen, der versucht, der Eigenschaft Radius den ungültigen Wert -1 zuzuweisen, wird der Client durch die Auslösung des Ereignisses InvalidRadius und den Aufruf des Handlers kreis_InvalidRadius über die ungültige Zuweisung benachrichtigt.
Hinweis |
Ein Tipp am Rande. Sie brauchen sich nicht die Mühe zu machen, das Delegate des Ereignisses zu instanziieren und anschließend den Ereignishandler manuell anzugeben. Stattdessen können Sie Visual Studio 2010 die Arbeit überlassen. Achten Sie einmal darauf, dass Ihnen nach der Eingabe des +=-Operators angeboten wird, die |
Abbildung 5.1 Automatisches Erzeugen des Ereignishandlers
5.2.3 Allgemeine Betrachtungen der Ereignishandlerregistrierung 

Einen Ereignishandler können Sie auch mehrfach an das Ereignis eines Objekts binden, z. B. so:
Circle kreis = new Circle(); kreis.InvalidRadius += kreis_InvalidRadius; kreis.InvalidRadius += kreis_InvalidRadius; kreis.InvalidRadius += kreis_InvalidRadius; ...
Ob das sinnvoll ist, wollen wir an dieser Stelle nicht diskutieren. Aber es geht. Sie können aber auch mehrere verschiedene Ereignishandler bei einem Ereignis registrieren; Sie sind nicht nur auf einen Handler beschränkt.
Circle kreis = new Circle(); kreis.InvalidRadius += kreis_InvalidRadius; kreis.InvalidRadius += RadiusError; ...
Analog zum Binden eines Ereignisses mit dem +=-Operator an eine Methode im Ereignisempfänger können Sie mit dem -=-Operator diese Bindung zu einem beliebigen Zeitpunkt wieder lösen. Mit
Circle kreis = new Circle(); kreis.InvalidRadius += kreis_InvalidRadius; kreis.InvalidRadius -= kreis_InvalidRadius; ...
weist das Objekt keine gültige Registrierung mehr auf. Es wird also nichts passieren, falls das Ereignis ausgelöst wird.
Ereignishandler sind nicht nur von einem Objekt nutzbar, sondern können von mehreren Objekten gleichzeitig benutzt werden. Mit
Circle kreis1 = new Circle(); Circle kreis2 = new Circle(); kreis1.InvalidRadius += kreis_InvalidRadius; kreis2.InvalidRadius += kreis_InvalidRadius;
wird der Ereignishandler sowohl für das Objekt kreis1 als auch für das Objekt kreis2 genutzt. Sie können sogar noch einen Schritt weitergehen: Der Ereignishandler ist natürlich auch nicht einen bestimmten Typ verpflichtet. Sie können den Ereignishandler für jedes x-beliebige Objekt mit jedem x-beliebigen Ereignis verwenden – vorausgesetzt, der Typ des Ereignisses stimmt mit der Parameterliste und dem Rückgabewert des Ereignishandlers überein.
5.2.4 Wenn der Ereignisempfänger ein Ereignis nicht behandelt 

Clientseitig muss das von einem Objekt ausgelöste Ereignis nicht zwangsläufig an einen Ereignishandler gebunden werden. Legt man keinen Wert darauf, kann das Ereignis auch unbehandelt im Sande verlaufen, es findet dann keinen Abnehmer.
Sehen wir uns in der Klasse Circle noch einmal die Eigenschaft Radius mit dem Ereignisauslöser an:
public virtual double Radius {
get{return _Radius;}
set{
if(value >= 0)
_Radius = value;
else
InvalidRadius();
}
}
Die Implementierung ist noch nicht so weit vorbereitet, dass der Aufrufer das Ereignis ignorieren könnte. Wenn nämlich mit
Circle kreis = new Circle(); kreis.Radius = -2;
fälschlicherweise ein unzulässiger negativer Wert zugewiesen wird und das Ereignis im potenziellen Ereignisempfänger nicht behandelt wird, kommt es zur Laufzeit zu einem Fehler des Typs System.NullReferenceException, da der Aufruf nur null zurückliefert.
Vor der Auslösung eines Ereignisses sollte daher in der Ereignisquelle zuerst geprüft werden, ob der Ereignisempfänger überhaupt die Absicht hat, auf das Ereignis zu reagieren. Mit einer if-Anweisung lässt sich das sehr einfach feststellen:
public virtual double Radius {
get { return _Radius; }
set {
if (value >= 0)
_Radius = value;
else
if (InvalidRadius != null)
InvalidRadius();
}
}
5.2.5 Ereignisse mit Übergabeparameter 

Der Auslöser des Ereignisses
Werfen wir nun erneut einen Blick auf den Ereignishandler, der das Event InvalidRadius eines Circle-Objekts behandelt:
public void kreis_InvalidRadius() {
Console.WriteLine("Unzulässiger negativer Radius.");
}
Einer kritischen Betrachtung kann die Codierung nicht standhalten, denn wir müssen erkennen, dass der Handler keine Allgemeingültigkeit gewährleistet. Dafür gibt es zwei Gründe:
- Zweifelsfrei wäre es sehr angenehm, bereits im Ereignishandler den Anwender zu einer erneuten Eingabe des Radius aufzufordern. Allerdings ist die Referenz auf das auslösende Objekt im Ereignishandler nicht bekannt.
- Ein Ereignishandler kann von mehreren Objekten benutzt werden, auch solchen mit unterschiedlichem Typ. Allerdings besteht derzeit keine Möglichkeit, festzustellen, welches Objekt für den Aufruf des Ereignishandlers verantwortlich zeichnet.
Das Problem ist sehr einfach zu lösen, wenn der Ereignishandler einen Parameter bereitstellt, in dem das ereignisauslösende Objekt die Referenz auf sich selbst übergibt. Darauf kann dem Radius ein neuer Wert zugewiesen werden.
public void kreis_InvalidRadius(Circle sender) {
Console.WriteLine("Unzulässiger negativer Radius.");
Console.Write("Neueingabe: ");
sender.Radius = Convert.ToDouble(Console.ReadLine());
}
Jetzt ist der Ereignishandler so allgemein, dass er an das InvalidRadius-Ereignis jedes x-beliebigen Circle-Objekts gebunden werden kann. Eine Neueingabe des Radius wird immer dem ereignisauslösenden Objekt zugewiesen.
Diese Überlegung hat auch eine Änderung des Codes in der Klasse Circle zur Folge. Dazu ist zunächst die Definition des Delegates durch einen Parameter vom Typ Circle zu ergänzen:
public delegate void InvalidRadiusEventHandler(Circle c);
Bei der Auslösung des Ereignisses in der Eigenschaftsmethode Radius wird dem Event InvalidRadius mit this die Referenz auf das aktuelle Objekt übergeben, auf dem der Ereignishandler im Client Operationen ausführen kann.
public virtual double Radius {
get{return _Radius;}
set {
if(value >= 0)
_Radius = value;
else if(InvalidRadius != null)
InvalidRadius(this);
}
}
Jetzt haben wir einen Stand erreicht, der auch einer kritischen Analyse standhält: Das Ereignis InvalidRadius ist so allgemein definiert, dass im Client ein Ereignishandler ausreicht, um damit mehrere Circle-Objekte gleichzeitig behandeln zu können.
Ereignishandler im .NET Framework
Alle Ereignishandler im .NET Framework weisen zwei Parameter auf:
- Im ersten Parameter gibt sich das auslösende Objekt bekannt.
- Im zweiten Parameter werden ereignisspezifische Daten geliefert.
Den ersten Parameter haben wir im Abschnitt zuvor zwar schon behandelt, aber wir müssen eine kleine Nachbetrachtung anstellen und eine Änderung vornehmen. Grundsätzlich ist nämlich der erste Parameter immer vom Typ Object. Der Grund ist recht einfach, denn die Delegates, die den Ereignissen zugrunde liegen, sollen generell mehreren unterschiedlichen Ereignisdefinitionen zur Verfügung stehen, die durchaus auch von unterschiedlichen Objekten ausgelöst werden können. Im ersten Parameter einen spezifischen Typ anzugeben, würde zu deutlich mehr Delegate-Definitionen führen.
Im zweiten Parameter werden ereignisspezifische Daten geliefert. Wir wollen uns das am Beispiel der Klasse Circle exemplarisch verdeutlichen. Nach dem derzeitigen Entwicklungsstand können wir im Ereignishandler nicht feststellen, welcher Wert nicht akzeptiert worden ist. Vielleicht möchten wir aber diese Information dem Ereignishandler bereitstellen, damit beispielsweise die Konsolenausgabe
Ein Radius vom -22 ist nicht zulässig.
ermöglicht wird.
Zur Bereitstellung von ereignisspezifischen Daten werden spezielle Klassen benötigt, die prinzipiell von EventArgs abgeleitet sind. Damit lassen sich die Typen der zweiten Parameter auf eine gemeinsame Basis zurückführen. EventArgs dient seinerseits selbst einigen Ereignissen als Typvorgabe (beispielsweise allen Click-Ereignissen). Allerdings stellt EventArgs selbst keine eigenen Daten zur Verfügung und ist daher bei diesen Ereignissen mehr als Dummy anzusehen, um der allgemeinen Konvention zu folgen.
In unserem Fall könnte die Klasse für den zweiten Parameter wie folgt codiert sein:
public class InvalidRadiusEventArgs : EventArgs {
private double _Radius;
public double Radius {
get { return _Radius; }
}
public InvalidRadiusEventArgs(double radius) {
_Radius = radius;
}
}
Hinweis |
Üblicherweise werden die Klassen, die als Typvorgabe für die Objekte der zweiten Parameter im Eventhandler dienen, mit dem Suffix EventArgs ausgestattet. Häufig wird dem Suffix der Ereignisname vorangestellt. |
In unserem Fall wollen wir dem Ereignishandler nur den Wert des fehlgeschlagenen Zuweisungsversuchs mitteilen. Es reicht dazu aus, den Wert in einer schreibgeschützten Eigenschaft zu kapseln.
Jetzt wollen wir uns alle Änderungen ansehen, die sich aus unseren Überlegungen ergeben. Da wäre zunächst einmal die Anpassung des Delegates InvalidRadiusEventHandler, das nun im ersten Parameter den Typ Object vorschreibt und im zweiten ein Objekt vom Typ InvalidRadiusEventArgs.
public delegate void InvalidRadiusEventHandler(Object sender, InvalidRadiusEventArgs e);
Nun können wir auch die Eigenschaft Radius in der Klasse Circle überarbeiten:
public virtual double Radius {
get { return _Radius; }
set {
if (value >= 0)
_Radius = value;
else
{
if (InvalidRadius != null)
InvalidRadius(this, new InvalidRadiusEventArgs(value));
}
}
}
Der Ereignishandler muss nun natürlich entsprechend parametrisiert werden. Aber er gestattet uns nicht nur zu erfahren, welches Objekt für die Ereignisauslösung verantwortlich ist, sondern auch die Auswertung, welcher Wert nicht akzeptiert werden konnte.
void kreis_InvalidRadius(object sender, InvalidRadiusEventArgs e){
Console.WriteLine("Ein Radius von {0} ist nicht zulässig.", e.Radius);
Console.Write("Neueingabe: ");
((Circle)sender).Radius = Convert.ToDouble(Console.ReadLine());
}
Zusammenfassung
Fassen wir an dieser Stelle noch einmal alle Erkenntnisse hinsichtlich der Ereignishandler im .NET Framework zusammen:
- Ereignishandler liefern niemals einen Wert an den Aufrufer zurück, sie sind immer void und haben zwei Parameter.
- Der erste Parameter ist grundsätzlich immer vom Typ Object. Hier gibt sich der Auslöser des Events bekannt.
- Der zweite Parameter ist vom Typ EventArgs oder davon abgeleitet. Er stellt ereignisspezifische Daten zur Verfügung. In der Regel wird das Suffix EventArgs angehängt.
- Basierend auf diesen Punkten werden die Delegates definiert, die als Typvorgabe der Ereignisse dienen.
5.2.6 Ereignisse in der Vererbung 

Ereignisse können nur in der Klasse ausgelöst werden, in der sie definiert sind. Mit anderen Worten bedeutet das auch, dass Ereignisse nicht vererbt werden. In der Klasse GraphicCircle könnte nach dem derzeitigen Stand des Klassencodes niemals das Ereignis InvalidRadius ausgelöst werden.
Aus diesem Grund wird in der Klasse, in der ein Ereignis bereitgestellt wird, eine zusätzliche Methode definiert, in der das Ereignis ausgelöst wird. Üblicherweise sind diese Methoden geschützt, also protected. Sie definieren einen Parameter, bei dem es sich um den Typ EventArgs handelt. Es ist allgemeine Konvention im .NET Framework, dass die Methoden, die einzig und allein der Ereignisauslösung dienen, mit dem Präfix On gekennzeichnet werden, gefolgt vom Bezeichner des Ereignisses. Für unser Ereignis würde die Methode demnach wie folgt aussehen:
protected void OnInvalidRadius(InvalidRadiusEventArgs e) {
if (InvalidRadius != null)
InvalidRadius(this, e);
}
Das Ereignis wird also in der Klasse ausgelöst, in der es definiert ist. Andererseits vererbt sich die Methode aber an alle abgeleiteten Klassen, die nun durch den einfachen Methodenaufruf über einen Umweg die Auslösung des Ereignisses bewirken können.
5.2.7 Hinter die Kulissen geblickt 

Rufen wir uns zum Abschluss noch einmal in Erinnerung, wie wir einen Ereignishandler registrieren:
kreis.OnlnvalidRadius += new InvalidRadiusEventHandler(kreis_InvalidRadius);
Die Nutzung des Operators += mag seltsam anmuten; warum kann nicht auch einfach nur der Operator = benutzt werden? In diesem Zusammenhang stellt sich auch die Frage, warum ein Ereignis mit dem Schlüsselwort event deklariert werden muss? Da ein Ereignis vom Typ eines Delegates ist, könnte doch vermutlich auch auf die Angabe von event verzichtet werden, also:
public InvalidRadiusEventHandler;
Tatsächlich verbirgt sich hinter dem Schlüsselwort ein Mechanismus, der ähnlich wie eine gekapselte Eigenschaft aufgebaut ist. Unser Ereignis InvalidRadius wird, zusammen mit dem event-Schlüsselwort, im Hintergrund wie folgt umgesetzt:
private InvalidRadiusEventHandler _InvalidRadius; public event InvalidRadiusEventHandler InvalidRadius { add { _InvalidRadius += value; } remove { _InvalidRadius -= value; } }
Vergleichbar mit dem get- und set-Accessor einer Eigenschaftsmethode werden hinter event die beiden Zweige add und remove erzeugt. Das Delegate selbst bleibt in einem private-Feld verborgen.
Angenommen, wir würden tatsächlich auf die Angabe von event verzichten. Der Code würde zwar weiterhin fehlerfrei ausgeführt, aber er würde auch gestatten, die Aufrufliste mit
kreis.InvalidRadius = null;
zu löschen. Bei einer Deklaration des Ereignisses mit event ist das nicht möglich, denn event kapselt den direkten Zugriff.
Mit event wird, wie Sie oben gesehen haben, ein add- und ein remove-Accessor definiert. Mit den beiden Operatoren += und -= wird nur noch gesteuert, welcher der beiden ausgeführt werden soll. Die Entwicklungsumgebung wird einen Kompilierfehler ausgeben, wenn Sie stattdessen nur den einfachen Zuweisungsoperator = benutzen.
Tatsächlich können Sie sogar per Programmcode ein Ereignis mit den beiden Routinen add und remove nachbilden. Im folgenden Beispielprogramm wird das demonstriert. In der Klasse Demo ist das Ereignis OutOfCoffee definiert, aber ganz elementar. Außer Ihnen die Möglichkeit zu geben, ein wenig hinter die Kulissen eines Events zu schauen, vollbringt das Beispiel keine besonderen Leistungen.
// --------------------------------------------------------- // Beispiel: ...\Kapitel 5\EventDemonstration // --------------------------------------------------------- class Program { static void Main(string[] args) { Demo demo = new Demo(); demo.OutOfCoffee += new EventHandler(demo_OutOfCoffee); demo.TestMethod(); Console.ReadLine(); } // Ereignishandler static void demo_OutOfCoffee(object sender, EventArgs e) { Console.WriteLine("Im Ereignishandler von 'OutOfCoffee'"); } } class Demo { // Gekapseltes Delegate private EventHandler _OutOfCoffee; // Definition des Events public event EventHandler OutOfCoffee { add { _OutOfCoffee += value; } remove { _OutOfCoffee -= value; } } // ereignisauslösende Methode public void TestMethod() { if (_OutOfCoffee != null) this._OutOfCoffee(this, new EventArgs()); } }
5.2.8 Änderungen im Projekt »GeometricObjects« 

Sie wissen nun, wie Ereignisse definiert werden; Sie wissen, wie Ereignisse in der Vererbung behandelt werden und kennen die allgemeine Namenskonvention im .NET Framework. Ich habe Ihnen das alles anhand eines Ereignisses gezeigt, das ausgelöst wird, wenn der Versuch unternommen wird, einen ungültigen Radius zu übergeben. Sie dürfen daraus nicht den Schluss ziehen, dass im Fall eines Fehlers ein Ereignis ausgelöst werden sollte. Selbstkritisch betrachtet, muss ich im Fall unserer Klasse Circle sogar sagen, dass das Ereignis völlig unzureichend ist. Denken Sie nur daran, dass bei der Übergabe eines negativen Radius-Wertes an einen der parametrisierten Konstruktoren der Benutzer keine Möglichkeit hat, durch eine Neueingabe den Fehler zu korrigieren. Schlimmer noch: Er wird überhaupt nicht über das Missgeschick informiert. Zudem kommt noch hinzu, dass die Behandlung eines Ereignisses nicht verpflichtend ist.
In der Praxis ist daher die Auslösung eines Ereignisses im Falle eines Fehlers völlig unangebracht. Stattdessen sollte eine Ausnahme ausgelöst werden, die im Programmcode behandelt werden muss. Zur Ehrenrettung unserer Klasse Circle muss aber auch festgestellt werden, dass Sie im .NET Framework auf Methoden treffen werden, die beide Möglichkeiten vorsehen: Entweder wird ein Ereignishandler registriert, in dem auf den aufgetretenen Fehler reagiert wird, oder es wird eine Exception ausgelöst. Genauso werden wir später noch die Klasse Circle ergänzen, wenn wir uns mit den Ausnahmen (Exceptions) beschäftigen.
Wir wollen nun aber noch unser Projekt um zwei sehr typische Ereignisse in der Methode MoveXY erweitern. Es sind die Ereignisse Moving und Moved. Moving soll ausgelöst werden, bevor die Bezugspunktkoordinaten verschoben werden, Moved nach der Verschiebung.
Solche Ereignispärchen sind nicht untypisch im .NET Framework. Beispielsweise werden beim Schließen eines WPF-Fensters die Ereignisse Closing und Closed ausgelöst. Wird das Ereignis Closing im Code behandelt, besteht die Möglichkeit, das bereits eingeleitete Schließen des Fensters im buchstäblich letzten Moment noch abzubrechen. Soll das Fenster nicht geschlossen werden, muss die Eigenschaft Cancel des entsprechenden EventArgs-Objekts auf true gesetzt werden. Geschieht das nicht, wird Closed ausgelöst. Der Abbruch des Schließvorgangs ist dann nicht mehr möglich.
Ein ähnliches Verhalten sollen auch die beiden Ereignisse Moving und Moved zeigen. Eine eingeleitete Verschiebeoperation soll im Ereignishandler von Moving noch abgebrochen werden können, und zwar ebenfalls über eine Eigenschaft Cancel. Das Moved-Ereignis dient nur dazu, im EventArgs-Parameter die neuen Bezugspunktkoordinaten bereitzustellen.
Um diese Forderungen umzusetzen, wird das Projekt zuerst um die beiden Klassen ergänzt, die die EventArgs-Objekte der späteren Delegates beschreiben.
public class MovingEventArgs { public bool Cancel; } public class MovedEventArgs : EventArgs { // Felder private int _X; private int _Y; // Konstruktor public MovedEventArgs(int x, int y) { _X = x; _Y = y; } // Eigenschaftsmethoden public int X { get { return _X; } } public int Y { get { return _Y; } } }
MovingEventArgs ist am einfachsten implementiert. Da die Eigenschaft Cancel im Ereignishandler unter Umständen einen neuen Wert erhält, der ausgewertet werden muss, genügt uns die einfache Deklaration einer booleschen Variablen. MovedEventHandler liefert die neuen Koordinatenwerte. Diese sollen aber schreibgeschützt sein, wodurch die Definition von entsprechenden Eigenschaftsmethoden notwendig wird. Damit die neuen Werte des Bezugspunktes überhaupt in das Objekt gelangen, ist ein parametrisierter Konstruktor notwendig.
Mit diesen beiden Typen lassen sich die beiden notwendigen Delegates beschreiben:
public delegate void MovingEventHandler(Object sender, MovingEventArgs e); public delegate void MovedEventHandler(Object sender, MovedEventArgs e);
In der Klasse GeometricObject ist die MoveXY-Methode definiert, in der die beiden Ereignisse Moving und Moved ausgelöst werden sollen. Folglich gilt es, in dieser Klasse die beiden Methoden zu definieren. Dazu gehören auch die beiden geschützten Methoden, die die Ereignisauslösung kapseln, um die Ereignisse auch den ableitenden Klassen zugänglich zu machen.
In MoveXY steht auch der Code, der prüft, ob der Anwender die eingeleitete Verschiebung des Bezugspunktes abbrechen möchte. Dazu wird die Eigenschaft Cancel des MovingEventArgs-Objekts untersucht. Hat der Benutzer mit true kundgetan, doch nicht zu verschieben, wird MoveXY mit return beendet.
public abstract class GeometricObject { // Ereignisse public event MovingEventHandler Moving; public event MovedEventHandler Moved; // geschützte Methoden protected void OnMoving(MovingEventArgs e){ if (Moving != null) Moving(this, e); } protected void OnMoved(MovedEventArgs e){ if (Moved != null) Moved(this, e); } public virtual void MoveXY(int dx, int dy){ // Moving-Ereignis MovingEventArgs e = new MovingEventArgs(); OnMoving(e); if (e.Cancel == true) return; XCoordinate += dx; YCoordinate += dy; // Moved-Ereignis OnMoved(new MovedEventArgs(XCoordinate, YCoordinate)); } ... }
Anmerkung |
Sie finden den Code des Beispiels auf der Buch-DVD unter den Beispielen zu Kapitel 5. Darin wurde auch die Klasse Rectangle geändert, sodass auch ein Rectangle-Objekt ähnliche Ereignisse wie ein Circle-Objekt hat. Der einzig erwähnenswerte Unterschied ist der, dass anstatt des Ereignisses InvalidRadius die beiden Ereignisse InvalidWidth und InvalidLength dafür sorgen, den Anwender präzise über die Seite zu informieren, der ein ungültiges Maß übergeben werden sollte. |