5.2 Ereignisse eines Objekts
Ein klassischer Methodenaufruf geht immer in Richtung vom Aufrufer zum Objekt, das daraufhin die Methode ausführt. Man könnte in diesem Fall auch von einer Client-Server-Konstellation sprechen, wobei der Aufrufer der Client ist, das aufgerufene Objekt der Server. Da der Client den Typ des Servers kennt, kann er die Methode auch namentlich angeben, z. B.:
class Program{
static void Main(string[] args) {
Circle kreis = new Circle();
kreis.Move(-100, 200);
}
}
Hier ist die Klasse Program der Client, das Circle-Objekt der Server. Das ist so weit noch sehr einfach. Gehen wir nun einen Schritt weiter. Stellen wir uns vor, aus Move heraus soll der Methodenaufrufer (also der Client) davon in Kenntnis gesetzt werden, dass die Verschiebung erfolgreich verlaufen ist. Dann müsste in der Methode Move ein Methodenaufruf codiert werden, der eine Methode im Client – bezogen auf unser Codefragment also in der Klasse Program – adressiert. Ein solcher Methodenaufruf wäre hinsichtlich der Aufrufrichtung genau entgegengesetzt der Richtung des klassischen Methodenaufrufs – nämlich vom Client zum Server. Genau das ist ein Ereignis, im Englischen auch als Event bezeichnet. Ein Ereignis ist somit nichts anderes als ein Methodenaufruf. Im Allgemeinen spricht man bei einem solchen Methodenaufruf auch vom »Auslösen eines Ereignisses«.
Ereignisse spielen eine herausragende Rolle bei der Programmierung grafischer Benutzeroberflächen (GUIs) und lassen sich so abstrahieren, dass sie den Nachrichtenverkehr zwischen einer Ereignisquelle und einem Ereignisempfänger beschreiben. Bezogen auf unsere Annahme würde das Circle-Objekt die Ereignisquelle sein, die Klasse Program der Ereignisempfänger.
Nun stellt sich eine Frage: Wenn die Auslösung eines Ereignisses einem Methodenaufruf gleichgesetzt werden kann, welche Methode im Client wird dann ausgeführt? Zunächst einmal können wir festhalten, dass der potenzielle Ereignisempfänger auf ein Ereignis nicht reagieren muss – es ist eine Option. Dann muss man weitblickend auch feststellen, dass es unterschiedliche Clients, also Ereignisempfänger, geben kann: Im Codefragment oben ist es die Klasse Program, es könnte aber auch eine Klasse namens Demo sein oder ein Auto-Objekt oder wer auch immer. Als logische Konsequenz können wir auch nicht den Bezeichner der Methode kennen, die als Reaktion auf die Auslösung eines Ereignisses aufgerufen werden soll.
Merken Sie etwas? Hatten wir nicht eine ähnliche Situation im Zusammenhang mit dem Delegaten kennengelernt? Erinnern wir uns an die folgende Anweisung im Beispielprogramm SimpleDelegate:
double result = calculate(input1, input2);
Die Variable calculate beschreibt einen Delegaten. Zur Entwicklungszeit ist nicht bekannt, welche Methode zur Laufzeit bei der Ausführung dieser Anweisung aufgerufen wird: Es konnte Add sein oder Subtract. Im Grunde genommen kann die Situation der Variablen calculate mit der beim Auslösen eines Ereignisses verglichen werden: Beide haben keine Kenntnis von der Methode, die daraufhin ausgeführt wird. Folgerichtig muss ein Ereignis ein Delegat sein.
Genug der Vorrede. Lassen Sie uns nun das Ganze an einem konkreten Beispiel erfahren. Es wird nicht im Zusammenhang mit der Methode Move stehen (das werden wir am Ende des Kapitels aber noch machen), sondern zunächst einen anderen Ansatzpunkt haben.
5.2.1 Ereignisse bereitstellen
Erinnern wir uns dazu zunächst an die aktuelle Implementierung der Eigenschaftsmethode Radius in der Circle-Klasse:
public virtual int Radius {
get{return _Radius;}
set{
if(value >= 0)
_Radius = value;
else
Console.Write("Unzulässiger negativer Radius.");
}
}
Listing 5.7 Aktuelle Implementierung der Eigenschaft »Radius« in der Klasse »Circle«
Uns interessiert nunmehr der set-Accessor und dort wiederum dessen Verhalten, wenn der Eigenschaft ein negativer Wert übergeben wird. Nach dem derzeitigen Stand führt das zu einer Benachrichtigung an der Konsole. Der Code funktioniert tadellos, unterliegt jedoch einer Einschränkung: Der Client muss die Nachricht entgegennehmen – ob er will oder nicht.
In der Praxis ist das Auslösen eines Ereignisses beim Auftreten eines Fehlers auch keine optimale Lösung, da eine Reaktion auf ein ausgelöstes Ereignis immer nur eine Option ist. Stattdessen sollte eine Ausnahme (Exception) »geworfen« werden, die behandelt werden muss. Zur Ehrenrettung unserer Idee muss an dieser Stelle aber auch festgestellt werden, dass es im .NET Framework Methoden gibt, die im Fehlerfall beide Alternativen anbieten: Entweder reagiert der Client auf ein Ereignis oder er behandelt die Exception. Genauso werden wir später noch die Klasse Circle ergänzen, wenn wir uns in Kapitel 7 mit den Ausnahmen beschäftigen.
Ein weiterer schwerwiegender Nachteil des bisherigen Lösungsansatzes: In einem GUI-Projekt, also beispielsweise einer WinForm- oder WPF-Anwendung, wird das Konsolenfenster überhaupt nicht geöffnet. Unsere augenblickliche Lösung ist also vollkommen indiskutabel.
Besser wäre es, wenn das Circle-Objekt stattdessen im Client eine Methode aufrufen würde, um damit zu signalisieren, dass die Wertübergabe nicht akzeptabel war. Mit anderen Worten: Wir wollen in dieser Situation ein Ereignis auslösen. Der Client kann dann als Ereignisempfänger auf das Ereignis reagieren.
Unser Ziel sei es nun, die Anweisung
Console.Write("Unzulässiger negativer Radius.");
durch eine Ereignisauslösung zu ersetzen. Das Ereignis wollen wir InvalidMeasure nennen.
Der Programmablauf bis zu einer eventuellen Ereignisauslösung würde wie folgt aussehen:
- Der Client erzeugt ein Objekt der Klasse Circle und weist der Eigenschaft Radius einen unzulässigen Wert zu.
- In der Eigenschaftsmethode Radius wird der übergebene Wert geprüft und im Fall der Unzulässigkeit das Ereignis InvalidMeasure ausgelöst. Das hat zur Folge, dass im Client nach einer Methode gesucht wird, die das Ereignis behandelt, also darauf reagiert.
- Erklärt sich der Client bereit, das Ereignis zu behandeln, wird im Client eine dem Ereignis zugeordnete Methode ausgeführt.
Kommen wir nun zu den Details der Ereignisimplementierung in der Ereignisquelle (hier: Circle). 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. Dabei handelt es sich immer um einen Delegaten. Weil ein Delegat den Zeiger auf eine Methode mit einer bestimmten Parameterliste und einem bestimmten Rückgabetyp beschreibt, wird damit gleichzeitig die Signatur der ereignisbehandelnden Methode im Client vorgeschrieben. Abgeschlossen wird die Deklaration mit dem Bezeichner des Ereignisses.
Unsere Anwendung müssen wir daher noch um eine Delegatdefinition ergänzen und in der Klasse Circle einen Event vom Typ dieses Delegaten deklarieren, um der selbst gestellten Anforderung zu genügen:
// Delegate
public delegate void InvalidMeasureEventHandler();
public class Circle : GeometricObject, IDisposable {
// Ereignis
public event InvalidMeasureEventHandler InvalidMeasure;
[...]
}
Listing 5.8 Definition des Events »InvalidMeasure« in der Klasse »Circle«
Delegates, die den Ereignissen als Typvorgabe dienen, haben im .NET Framework per Konvention das Suffix EventHandler.
Ausgangspunkt unserer Überlegungen war, bei einer unzulässigen Zuweisung an die Eigenschaft Radius eines Circle-Objekts das Ereignis InvalidMeasure auszulösen. Die Ereignisauslösung erfolgt, wenn die Überprüfung des Übergabewertes zu einer Ablehnung geführt hat. Die Ereignisauslösung selbst ist trivial, wir brauchen dazu nur den Namen des Ereignisses anzugeben. Diese Anweisung ersetzt in der Eigenschaft Radius die Konsolenausgabe im else-Zweig des set-Accessors:
public virtual int Radius {
get {return _Radius;}
set {
if(value >= 0)
_Radius = value;
else
// Ereignis auslösen
InvalidMeasure();
}
}
Listing 5.9 Ereignisauslösung in der Eigenschaft »Radius«
Übergibt der Client der Eigenschaft Radius nun einen Wert, der der Bedingung
Radius < 0
entspricht, wird der Delegat aktiv und sucht im Aufrufer nach einer parameterlosen Methode ohne Rückgabewert.
5.2.2 Die Reaktion auf ein ausgelöstes Ereignis
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 InvalidMeasure ausgeführt werden soll. Solche Methoden werden auch als Ereignishandler bezeichnet. Da der Typ unseres Ereignisses InvalidMeasure ein parameterloser Delegat ist, muss die Parameterliste unserer Methode natürlich leer sein.
public class Program {
static void Main(string[] args) {
Circle kreis = new Circle();
[...]
}
// Ereignishandler
public static void kreis_InvalidMeasure() {
Console.WriteLine("Unzulässiger negativer Radius.");
}
}
Listing 5.10 Bereitstellen eines Ereignishandlers
Es ist ü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 von Visual Studio automatisch generierten Ereignishandler folgen diesem Namensmuster.
Wir können dem Objekt kreis nun einen Radius von beispielsweise »-1« zuweisen, aber die Methode kreis_InvalidMeasure würde daraufhin nicht ausgeführt. (Ganz im Gegenteil sogar: Es tritt eine Ausnahme auf. Aber dem Phänomen widmen wir uns später noch.) Woher soll das Objekt kreis auch wissen, welche Methode bei der Ereignisauslösung im Client ausgeführt werden soll? Es könnten schließlich x-beliebig viele parameterlose void-Methoden im Ereignisempfänger definiert sein und prinzipiell als Ereignishandler in Frage kommen.
Um das Objekt entsprechend in Kenntnis zu setzen, müssen wir den von uns bereitgestellten Ereignishandler an das Ereignis InvalidMeasure des Objekts binden. Dazu übergeben wir dem Ereignis des Objekts mit dem +=-Operator eine Instanz des Delegates InvalidMeasureEventHandler mit Angabe des Handlers:
kreis.InvalidMeasure += new InvalidMeasureEventHandler(kreis_InvalidMeasure);
Dieser Vorgang wird als das »Abonnieren eines Ereignisses« oder auch als »Registrieren eines Ereignishandlers« bezeichnet. Natürlich ist auch die Kurzform
kreis.InvalidMeasure += kreis_InvalidMeasure;
erlaubt. Die einzige Bedingung ist, dass die dem Konstruktor bekannt gegebene Methode den vom Delegaten festgelegten Kriterien hinsichtlich der Parameterliste und des Rückgabewerts genügt. Unser Code in Main könnte nun wie folgt lauten:
public void Main(string[] args) {
Circle kreis = new Circle();
kreis.InvalidMeasure += kreis_InvalidMeasure;
kreis.Radius = -1;
Console.ReadLine();
}
Listing 5.11 Code, um den Ereignishandler zu testen
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 InvalidMeasure und den Aufruf des Handlers kreis_InvalidMeasure über die ungültige Zuweisung benachrichtigt.
Ein Tipp am Rande. Sie brauchen sich nicht die Mühe zu machen, den Delegaten des Ereignisses zu instanziieren und anschließend den Ereignishandler manuell anzugeben. Stattdessen können Sie Visual Studio 2012 die Arbeit überlassen. Achten Sie einmal darauf, dass Ihnen nach der Eingabe des +=-Operators angeboten wird, die -Taste zu drücken (siehe Abbildung 5.1). Nutzen Sie das Angebot, wird der Typ des Ereignisses automatisch instanziiert. Ein zweites Drücken der -Taste bewirkt das automatische Erzeugen des Ereignishandlers nach der oben beschriebenen Namenskonvention.
Abbildung 5.1 Automatisches Erzeugen des Ereignishandlers
5.2.3 Allgemeine Betrachtungen der Ereignishandler-Registrierung
In Abschnitt 5.1.4 habe ich die Multicast-Delegates und den +=-Operator beschrieben. Da ein Ereignis immer vom Typ eines Delegaten ist, gelten die Regeln hinsichtlich der Multicast-Delegates natürlich auch für Ereignisse. So können Sie beispielsweise mehrere verschiedene Ereignishandler für ein Ereignis abonnieren:
Circle kreis = new Circle();
kreis.InvalidMeasure += kreis_InvalidMeasure;
kreis.InvalidMeasure += RadiusError;
[...]
Listing 5.12 Mehrere Ereignishandler registrieren
Analog zum Binden eines Ereignishandlers mit dem +=-Operator können Sie mit dem -=-Operator diese Bindung zu jedem beliebigen Zeitpunkt wieder lösen. Mit
Circle kreis = new Circle();
kreis.InvalidMeasure += kreis_InvalidMeasure;
kreis.InvalidMeasure -= kreis_InvalidMeasure;
[...]
Listing 5.13 Ereignishandler deregistrieren
weist das Objekt keinen registrierten Ereignishandler mehr auf. Es wird also beim Auslösen des Events nichts passieren.
Ereignishandler sind nicht nur von einem Objekt nutzbar, sondern können von mehreren Objekten gleichermaßen verwendet werden. Mit
Circle kreis1 = new Circle();
Circle kreis2 = new Circle();
kreis1.InvalidMeasure += kreis_InvalidMeasure;
kreis2.InvalidMeasure += kreis_InvalidMeasure;
Listing 5.14 Bei mehreren Objekten denselben Ereignishandler registrieren
wird der Ereignishandler sowohl vom Objekt kreis1 als auch vom Objekt kreis2 benutzt. Sie können sogar noch einen Schritt weiter gehen: Der Ereignishandler ist natürlich auch nicht einem bestimmten Typ verpflichtet. Sie können den Ereignishandler für jedes x-beliebige Objekt und hier für jedes x-beliebige Ereignis verwenden – vorausgesetzt, der Typ des Ereignisses stimmt mit der Parameterliste und dem Rückgabewert des Ereignishandlers überein.
Beachten Sie bitte, dass auf ein ausgelöstes Ereignis erst nach dem Abonnieren des Events reagiert werden kann. In Konsequenz bedeutet das aber auch, dass das Ereignis InvalidMeasure noch nicht behandelt werden kann, wenn wir einem Konstruktor einen negativen Wert für die Eigenschaft Radius übergeben.
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 int Radius {
get{return _Radius;}
set{
if(value >= 0)
_Radius = value;
else
InvalidMeasure();
}
}
Die Implementierung ist noch nicht so weit vorbereitet, dass der potenzielle Ereignisempfänger 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 Ereignisempfänger nicht behandelt wird, kommt es zur Laufzeit zu einer Ausnahme des Typs NullReferenceException, weil das Ereignis keinen Abnehmer findet.
Vor der Auslösung eines Events muss daher in der Ereignisquelle 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 int Radius {
get { return _Radius; }
set {
if (value >= 0)
_Radius = value;
else if (InvalidMeasure != null)
InvalidMeasure();
}
}
Listing 5.15 Vollständiger Code zur Ereignisauslösung
5.2.5 Ereignisse mit Übergabeparameter
Bekanntgabe des Ereignisauslösers
Werfen wir noch einmal einen Blick auf den Ereignishandler, der den Event InvalidMeasure eines Circle-Objekts behandelt:
public void kreis_InvalidMeasure() {
Console.WriteLine("Unzulässiger negativer Radius.");
}
Einer kritischen Betrachtung kann der Code nicht standhalten, denn wir müssen erkennen, dass der Handler bisher nur allgemeingültig ist, da er keine Möglichkeit bietet, das auslösende Objekt zu identifizieren. Deshalb können wir auch nicht innerhalb des Ereignishandlers den Radius neu festlegen, was doch durchaus erstrebenswert wäre.
Das Problem ist sehr einfach zu lösen, indem der Ereignishandler einen Parameter bereitstellt, der die Referenz auf das ereignisauslösende Objekt beschreibt. Mit dem Parameter ist es dann möglich, dem Radius einen neuen Wert zuzuweisen.
public void kreis_InvalidMeasure(Circle sender) {
Console.Write("Unzulässiger negativer Radius. Neueingabe: ");
sender.Radius = Convert.ToInt32(Console.ReadLine());
}
Listing 5.16 Übergabe des ereignisauslösenden Objekts an den Ereignishandler
Jetzt ist der Ereignishandler so konstruiert, dass innerhalb des Handlers auf das auslösende Objekt zugegriffen werden kann. Wir nutzen den Parameter, um dem Radius einen neuen, dann hoffentlich akzeptablen Wert zuzuweisen.
Diese Überlegung hat auch weitere Änderungen zur Folge. Zunächst einmal muss die Definition des Delegates entsprechend geändert werden:
public delegate void InvalidMeasureEventHandler(Circle sender);
Listing 5.17 Änderung der Delegatdefinition
Das ist nicht die einzige Änderung. Auch die Klasse Circle muss noch angepasst werden, denn jetzt muss das Ereignis dem Ereignishandler auch ein Argument übergeben, mit dem die Referenz auf das auslösende Objekt beschrieben wird. Da sich der Code innerhalb des auslösenden Objekts befindet, kann das Objekt die Referenz auf sich selbst mit this angeben.
public virtual int Radius {
get{return _Radius;}
set {
if(value >= 0)
_Radius = value;
else if(InvalidMeasure != null)
InvalidMeasure(this);
}
}
Listing 5.18 Berücksichtigung der Delegatänderung aus Listing 5.17
Jetzt haben wir einen Stand erreicht, der auch einer kritischen Analyse standhält: Das Ereignis InvalidMeasure ist insgesamt so definiert, dass mit einem Ereignishandler mehrere Circle-Objekte gleich behandelt werden können.
Ereignishandler im .NET Framework
Obwohl wir nun im Ereignishandler das ereignisauslösende Objekt eindeutig identifizieren können, haben wir noch nicht den Stand erreicht, den alle Ereignishandler im .NET Framework haben. Denn alle Ereignishandler im .NET Framework weisen nicht nur einen, sondern zwei Parameter auf:
- Im ersten Parameter gibt sich das auslösende Objekt bekannt.
- Im zweiten Parameter werden ereignisspezifische Daten bereitgestellt.
Den ersten Parameter haben wir im letzten Abschnitt zwar schon behandelt, aber wir müssen noch eine kleine Nachbetrachtung anstellen. Grundsätzlich ist nämlich der erste Parameter immer vom Typ Object. Der Grund ist recht einfach, denn die den Ereignissen zugrunde liegenden Delegates sollen prinzipiell mehreren unterschiedlichen Ereignissen zur Verfügung stehen, die auch von unterschiedlichen Typen ausgelöst werden können.
In einem zweiten Parameter werden immer ereignisspezifische Daten geliefert. Wir wollen uns dies am Beispiel der Klasse Circle verdeutlichen.
Nach dem derzeitigen Entwicklungsstand können wir im Ereignishandler nicht feststellen, welche Zuweisung an Radius nicht akzeptiert worden ist. Vielleicht möchten wir aber diese Information dem Ereignishandler bereitstellen, damit beispielsweise die Konsolenausgabe
Ein Radius von -22 ist nicht zulässig.
ermöglicht wird.
Zur Bereitstellung von ereignisspezifischen Daten werden spezielle Klassen benötigt, die 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 den Click-Ereignissen). Allerdings stellt EventArgs keine eigenen Daten zur Verfügung und ist daher als Dummy anzusehen, um der allgemeinen Konvention zu entsprechen, dass alle Ereignishandler zwei Parameter haben.
In unserem Beispiel könnte die Klasse für den zweiten Parameter wie folgt codiert sein:
public class InvalidMeasureEventArgs : EventArgs
{
private int _InvalidMeasure;
public int InvalidMeasure {
get { return _InvalidMeasure; }
}
public InvalidMeasureEventArgs(int invalidMeasure) {
_InvalidMeasure = invalidMeasure;
}
}
Listing 5.19 Bereitstellung einer »EventArgs«-Klasse
Ü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 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.
Sehen wir uns nun alle Änderungen an, die sich aus unseren Überlegungen ergeben. Da wäre zunächst einmal die Anpassung des Delegaten InvalidMeasureEventHandler, der nun im ersten Parameter den Typ Object vorschreibt und im zweiten ein Objekt vom Typ InvalidMeasureEventArgs.
public delegate void InvalidMeasureEventHandler(Object sender,
InvalidMeasureEventArgs e);
Listing 5.20 Endgültige Definition des Delegaten »InvalidMeasureEventHandler«
Nun müssen wir auch die Eigenschaft Radius in der Klasse Circle anpassen:
public virtual int Radius {
get { return _Radius; }
set {
if (value >= 0)
_Radius = value;
else if (InvalidMeasure != null)
InvalidMeasure(this, new InvalidMeasureEventArgs(value));
}
}
Listing 5.21 Berücksichtigung des Delegaten aus Listing 5.20
Der Ereignishandler muss natürlich ebenfalls entsprechend parametrisiert werden. Er gestattet uns nun nicht nur, zu erfahren, welches Objekt für die Ereignisauslösung verantwortlich ist, sondern auch die Auswertung, welcher Wert nicht akzeptiert werden konnte.
Anmerkung
Selbstverständlich können Sie in der EventArgs-Klasse die Eigenschaft auch mit einem set-Accessor ausstatten. Das könnte aber zu einer Verwirrung im Ereignishandler führen, sollte mit
e.Radius = 10;
ein neuer Wert festgelegt werden. Der Grund ist recht einfach: In der Eigenschaft Radius der Klasse Circle wird diese Zuweisung nicht ausgewertet, sie verpufft im Nirwana. Weiter
unten, in Abschnitt 5.3, werden Sie bei der Ergänzung des Projekts GeometricObjectsSolution einen Fall kennenlernen, bei dem der set-Zweig in einer Eigenschaft des EventArgs-Objekts von Bedeutung ist.
void kreis_InvalidMeasure(object sender, InvalidMeasureEventArgs e){
Console.Write("Ein Radius von {0} ist nicht zulässig.", e.InvalidMeasure);
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. Dieser Parameter hat das Suffix EventArgs.
Nach diesen Vorgaben werden auch die Delegaten 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 InvalidMeasure ausgelöst werden.
Aus diesem Grund wird in der Klasse, in der ein Ereignis bereitgestellt wird, grundsätzlich eine zusätzliche Methode definiert, in der das Ereignis ausgelöst wird. Üblicherweise sind diese Methoden geschützt, also protected. Es ist eine allgemeine Konvention im .NET Framework, dass diese Methoden, die einzig und allein der Ereignisauslösung dienen, mit dem Präfix »On« gekennzeichnet werden, gefolgt vom Bezeichner des Events. Die OnXxx-Methoden definieren in der Regel genau einen Parameter. Bei diesem handelt es sich in der Regel um den ereignisspezifischen EventArgs-Typ.
Für unser Ereignis InvalidMeasure würde die Methode wie folgt aussehen:
protected virtual void OnInvalidMeasure(InvalidMeasureEventArgs e) {
if (InvalidMeasure != null)
InvalidMeasure(this, e);
}
Listing 5.22 Methode, die ein Ereignis ableitenden Klassen zur Verfügung stellt
Die OnXxx-Methode wird von allen abgeleiteten Klassen geerbt. Weil die Methode in der Klasse definiert ist, in der auch das Ereignis bereitgestellt wird, bewirkt der Aufruf dieser Methode in der abgeleiteten Klasse auch die Auslösung des Events. Der Modifikator virtual gestattet zudem, in der Ableitung die geerbte Methode polymorph zu überschreiben, um möglicherweise typspezifische Anpassungen im Kontext der Ereignisauslösung vorzunehmen. Dieser Fall ist gar nicht selten im .NET Framework.
5.2.7 Hinter die Kulissen des Schlüsselworts »event« geblickt
Rufen wir uns zum Abschluss noch einmal in Erinnerung, wie wir ein Ereignis definieren:
public event InvalidMeasureEventHandler InvalidMeasure;
Es stellt sich die Frage, warum ein Ereignis mit dem Schlüsselwort event deklariert werden muss. Da ein Ereignis vom Typ eines Delegaten ist, könnte doch vermutlich auch auf die Angabe von event verzichtet werden, also:
public InvalidMeasureEventHandler InvalidMeasure;
Tatsächlich verbirgt sich hinter dem Schlüsselwort ein Mechanismus, der ähnlich wie eine Eigenschaft aufgebaut ist. Unser Ereignis InvalidMeasure wird, zusammen mit dem event-Schlüsselwort, implizit wie folgt umgesetzt:
private InvalidMeasureEventHandler _InvalidMeasure;
public event InvalidMeasureEventHandler InvalidMeasure
{
add { _InvalidMeasure += value; }
remove { _InvalidMeasure -= value; }
}
Listing 5.23 Das Schlüsselwort »event« hinter den Kulissen
Durch das Schlüsselwort event werden die beiden Zweige add und remove implizit erzeugt. Der eigentliche Delegat bleibt in einem private-Feld verborgen. Nehmen wir an, wir würden auf die Angabe von event verzichten. Der Code wäre dann zwar syntaktisch nicht zu beanstanden, aber er würde auch gestatten, die Aufrufliste mit
kreis.InvalidMeasure = null;
zu löschen. Bei der Definition eines Ereignisses mit event ist das nicht möglich, denn event kapselt den direkten Zugriff.
Mit event wird implizit ein add- und ein remove-Accessor bereitgestellt. Mit den beiden Operatoren »+=« und »-=« wird bei der Registrierung eines Ereignishandlers gesteuert, welcher der beiden Zweige ausgeführt werden soll. Die Entwicklungsumgebung wird einen Kompilierfehler ausgeben, wenn Sie stattdessen nur den einfachen Zuweisungsoperator »=« benutzen.
Sie können 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 – ohne event anzugeben. Außer Ihnen die Möglichkeit zu geben, hinter die Kulissen eines Events zu schauen, vollbringt das Beispiel ansonsten 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.DoSomething();
Console.ReadLine();
}
// Ereignishandler
static void demo_OutOfCoffee(object sender, EventArgs e) {
Console.WriteLine("Im Ereignishandler von 'OutOfCoffee'");
}
}
class Demo {
// gekapselter Delegate
private EventHandler _OutOfCoffee;
// Definition des Events
public event EventHandler OutOfCoffee {
add { _OutOfCoffee += value; }
remove { _OutOfCoffee -= value; }
}
// Ereignisauslösende Methode
public void DoSomething() {
if (_OutOfCoffee != null)
this._OutOfCoffee(this, new EventArgs());
}
}
Listing 5.24 Fundamentale Implementierung eines Ereignisses
5.2.8 Die Schnittstelle »INotifyPropertyChanged«
Im Zusammenhang mit den Ereignissen sollten wir an dieser Stelle auch eine besondere Schnittstelle berücksichtigen. Es handelt sich dabei um INotifyPropertyChanged. Das Interface schreibt der implementierenden Klasse das Ereignis PropertyChanged vor. Per Vorgabe soll das Ereignis ausgelöst werden, nachdem sich eine Eigenschaft geändert hat, also im set-Zweig. INotifyPropertyChanged kommt eine besondere Bedeutung insbesondere im Zusammenhang mit neueren Technologien zu. In der WPF beispielsweise informiert dieser Event die datenbindenden Komponenten, dass die Anzeige des Eigenschaftswerts aktualisiert werden muss. Wir kommen darauf im Kontext dieser Thematik noch zu sprechen.
Natürlich wollen wir die Schnittstelle auch in den beiden Klassen Circle und Rectangle benutzen, um eine Änderung an Radius, Length oder Width zu signalisieren. Um die Schnittstelle zu implementieren, sollten wir zuerst noch mit
using System.ComponentModel;
den Namespace bekannt geben, in dem das Interface definiert ist. Dann können wir das Interface problemlos implementieren und sollten auch daran denken, eine entsprechende OnXxx-Methode bereitzustellen. Dabei ist zu berücksichtigen, dass das EventArgs-Objekt einen Parameter vom Typ string definiert, dem wir den Bezeichner der geänderten Eigenschaft übergeben. Das Interface INotifyPropertyChanged wird von der Klasse GeometricObject implementiert, damit die beiden abgeleiteten Klassen Circle und Rectangle gleichermaßen davon profitieren können.
public class GeometricObject : INotifyPropertyChanged
{
// Ereignis der Schnittstelle 'INotifyPropertyChanged'
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
[...]
}
Listing 5.25 Bereitstellung des Interfaces »INotifyPropertyChanged«
So ausgerüstet, kann nun die Eigenschaft Radius von diesem Ereignis profitieren:
public virtual int Radius {
get { return _Radius; }
set {
if (value >= 0) {
_Radius = value;
OnPropertyChanged("Radius");
}
else
OnInvalidMeasure(new InvalidMeasureEventArgs(value));
}
}
Listing 5.26 Änderung der Eigenschaft »Radius« aufgrund von Listing 5.25
In gleicher Weise sollten auch die Eigenschaften Length und Width von Rectangle und XCoordinate sowie YCoordinate in GeometricObject angepasst werden.
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.