Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Vorwort zur 5. Auflage
1 Allgemeine Einführung in .NET
2 Grundlagen der Sprache C#
3 Klassendesign
4 Vererbung, Polymorphie und Interfaces
5 Delegates und Ereignisse
6 Weitere .NET-Datentypen
7 Weitere Möglichkeiten von C#
8 Auflistungsklassen (Collections)
9 Fehlerbehandlung und Debugging
10 LINQ to Objects
11 Multithreading und die Task Parallel Library (TPL)
12 Arbeiten mit Dateien und Streams
13 Binäre Serialisierung
14 Einige wichtige .NET-Klassen
15 Projektmanagement und Visual Studio 2010
16 XML
17 WPF – Die Grundlagen
18 WPF-Containerelemente
19 WPF-Steuerelemente
20 Konzepte der WPF
21 Datenbindung
22 2D-Grafik
23 ADO.NET – verbindungsorientierte Objekte
24 ADO.NET – Das Command-Objekt
25 ADO.NET – Der SqlDataAdapter
26 ADO.NET – Daten im lokalen Speicher
27 ADO.NET – Aktualisieren der Datenbank
28 Stark typisierte DataSets
29 LINQ to SQL
30 Weitergabe von Anwendungen
Stichwort

Buch bestellen
Ihre Meinung?

Spacer
<< zurück
Visual C# 2010 von Andreas Kühnel
Das umfassende Handbuch
Buch: Visual C# 2010

Visual C# 2010
geb., mit DVD
1295 S., 49,90 Euro
Rheinwerk Computing
ISBN 978-3-8362-1552-7
Pfeil 5 Delegates und Ereignisse
Pfeil 5.1 Delegates
Pfeil 5.1.1 Einführung in das Prinzip der Delegates
Pfeil 5.1.2 Vereinfachter Aufruf eines Delegates
Pfeil 5.1.3 Multicast-Delegate
Pfeil 5.1.4 Anonyme Methoden
Pfeil 5.2 Ereignisse eines Objekts
Pfeil 5.2.1 Ereignis in einer Ereignisquelle ergänzen
Pfeil 5.2.2 Behandlung eines Ereignisses im Ereignisempfänger
Pfeil 5.2.3 Allgemeine Betrachtungen der Ereignishandlerregistrierung
Pfeil 5.2.4 Wenn der Ereignisempfänger ein Ereignis nicht behandelt
Pfeil 5.2.5 Ereignisse mit Übergabeparameter
Pfeil 5.2.6 Ereignisse in der Vererbung
Pfeil 5.2.7 Hinter die Kulissen geblickt
Pfeil 5.2.8 Änderungen im Projekt »GeometricObjects«


Galileo Computing - Zum Seitenanfang

5.2 Ereignisse eines Objekts Zur nächsten ÜberschriftZur vorigen Überschrift

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.


Galileo Computing - Zum Seitenanfang

5.2.1 Ereignis in einer Ereignisquelle ergänzen Zur nächsten ÜberschriftZur vorigen Überschrift

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.


Galileo Computing - Zum Seitenanfang

5.2.2 Behandlung eines Ereignisses im Ereignisempfänger Zur nächsten ÜberschriftZur vorigen Überschrift

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 Taste Tabulator-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 Tabulator-Taste bewirkt das automatische Erzeugen des Ereignishandlers nach der weiter oben beschriebenen Namenskonvention.


Abbildung 5.1 Automatisches Erzeugen des Ereignishandlers


Galileo Computing - Zum Seitenanfang

5.2.3 Allgemeine Betrachtungen der Ereignishandlerregistrierung Zur nächsten ÜberschriftZur vorigen Überschrift

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.


Galileo Computing - Zum Seitenanfang

5.2.4 Wenn der Ereignisempfänger ein Ereignis nicht behandelt Zur nächsten ÜberschriftZur vorigen Überschrift

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();
  }
}


Galileo Computing - Zum Seitenanfang

5.2.5 Ereignisse mit Übergabeparameter Zur nächsten ÜberschriftZur vorigen Überschrift

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.

Galileo Computing - Zum Seitenanfang

5.2.6 Ereignisse in der Vererbung Zur nächsten ÜberschriftZur vorigen Überschrift

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.


Galileo Computing - Zum Seitenanfang

5.2.7 Hinter die Kulissen geblickt Zur nächsten ÜberschriftZur vorigen Überschrift

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());
  }
}


Galileo Computing - Zum Seitenanfang

5.2.8 Änderungen im Projekt »GeometricObjects« topZur vorigen Überschrift

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.




Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen. >> Zum Feedback-Formular
<< zurück
  Zum Katalog
Zum Katalog: Visual C# 2010

Visual C# 2010
Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Katalog: Professionell entwickeln mit Visual C# 2012






 Professionell
 entwickeln mit
 Visual C# 2012


Zum Katalog: Windows Presentation Foundation






 Windows Presentation
 Foundation


Zum Katalog: Schrödinger programmiert C++






 Schrödinger
 programmiert C++


Zum Katalog: C++ Handbuch






 C++ Handbuch


Zum Katalog: C/C++






 C/C++


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo




Copyright © Rheinwerk Verlag GmbH 2010
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern