Arbeiten mit .NET: Grundlagen der OOP/ Ereignisse


ProblemBearbeiten

Hinweis: Dieses Beispiel hinkt ein wenig. Der Grund ist, dass wir noch nicht genug über Multithreading wissen, um hier ein nützliches Beispiel verwenden zu können. Dennoch veranschaulicht es das Prinzip der Ereignisse sehr gut. Und auch dem Hinken werden wir im Abschnitt Multithreading abhelfen.

Angenommen, wir würden eine sehr komplexe und lange dauernde Berechnung durchführen müssen und dringend auf das Ergebnis warten. Natürlich könnten wir nun in einer Schleife auf das Ergebnis lauern:

class Rechner
{
  // Dieses Flag zeigt an,
  // ob die Berechnung beendet ist.
  public bool m_Flag_Berechnung_Beendet; 

  // In dieser Variablen liegt am Ende das Ergebnis.
  public decimal m_Ergebnis;
 
  public void Berechne()
  {
    decimal zahl1 = 164m;
    decimal zahl2 =  26m;

    // Wir setzen das Flag zurück.
    m_Flag_Berechnung_Beendet = false;
    // Wir setzen das Ergebnis zurück.
    m_Ergebnis = 0;
    
    //
    // Hier findet eine sehr komplexe Berechnung statt...
    //

    // Am Ende der Berechnung übergeben wir der Klassenvariablen
    // m_Ergebnis das Ergebnis.
    // Symbolisch addieren wir es nur.
    m_Ergebnis = zahl1 + zahl2;

    // Abschließend setzen wir das Flag
    m_Flag_Berechnung_Beendet = true;
  }
}

class Client
{
  public void Berechne()
  {
    // Wir holen uns ein Objekt des Rechners
    Rechner rechner = new Rechner();

    // Wir beginnen die komplizierte Berechnung
    // der Addition zweier Zahlen.
    rechner.Berechne();

    // Während wir auf das Ergebnis warten,
    // kauen wir vor Langeweile an den Fingernägeln.
    while ( rechner.m_Flag_Berechnung_Beendet == false ) ; 
    
    Console.WriteLine( rechner.m_Ergebnis );
  }
}

Und wirklich, die meisten Programmiersprachen bieten hier kaum andere Möglichkeiten, als regelmäßig ein Flag abzufragen, wie es mit dem Status der Berechnung steht. Ist dieses Flag irgendwann true, wird unsere Sinnlosschleife verlassen und die nächste Zeile abgearbeitet.

Okay, manchmal ist sogar das einigermaßen sinnvoll. Aber was ist, wenn wir zwischenzeitlich andere Dinge erledigen wollen, während wir auf das Ergebnis warten? Warum kann uns der Rechner nicht einfach selbst darüber informieren, wann er mit seiner Berechnung fertig ist? Und das kann er sehr wohl. Denn dafür gibt es die Ereignisse.

EreignisseBearbeiten

Mit Hilfe von Ereignissen können Objekte mit ihrer Umwelt aktiv kommunizieren. In unserem Beispiel kann der Rechner sich bemerkbar machen und allen, die es wissen wollen, mitteilen, dass die Berechnung beendet ist. Dazu müssen wir unser Beispiel nur etwas umbauen.

Im ersten Schritt vereinfachen wir den Rechner. Wir entfernen zunächst einfach alle öffentlichen Felder, die bisher dem Zweck der Übertragung des Ergebnisses dienten. Die brauchen wir nicht.

class Rechner
{
  public void Berechne()
  {
    decimal zahl1 = 164m;
    decimal zahl2 =  26m;

    //
    // Hier findet eine sehr komplexe Berechnung statt...
    //

    // Am Ende der Berechnung übergeben wir der Klassenvariablen
    // m_Ergebnis das Ergebnis.
    // Symbolisch addieren wir es nur.
    zahl1 + zahl2;

  }
}

Außerdem fügen wir einen sogenannten Delegaten ein. Den legen wir einfach in die Klasse:

class Rechner
{
   public delegate void BerechnungBeendet(decimal ergebnis);

   // Hier folgt der restliche Programmcode...

Dann schreiben wir das Ereignis "OnBerechnungBeendet":

class Rechner 
{
   public delegate void BerechnungBeendet(decimal ergebnis);
   public event BerechnungBeendet OnBerechnungBeendet;
  
   // Hier folgt der restliche Programmcode...

Und zum Schluss legen wir noch fest, wann das Ereignis ausgelöst werden soll. Dazu schreiben wir in die Methode Berechne folgendes:

  public void Berechne()
  {
    decimal zahl1 = 164m;
    decimal zahl2 =  26m;

    //
    // Hier findet eine sehr komplexe Berechnung statt...
    //
 
    // Am Ende der Berechnung lösen wir das Ereignis aus
    // Symbolisch addieren wir nur.
    if ( OnBerechnungBeendet != null )
    {
      OnBerechnungBeendet( zahl1 + zahl2 );
    }
  }

Das war's schon. Mehr müssen wir nicht machen. Jetzt wird die Klasse Rechner alle Abonnenten des Ereignisses OnBerechnungBeendet informieren, wenn die Berechnung abgeschlossen ist. Wir müssen nur noch das Ereignis abonnieren. Und dazu erweitern wir die Klasse Client ein wenig...

class Client
{
  public void Berechne()
  {
    // Wir holen uns ein Objekt des Rechners
    Rechner rechner = new Rechner();

    // Wir abonnieren das Ereignis
    // Dazu erstellen wir ein neues Objekt 
    // des Delegaten "BerechnungBeendet" und 
    // fügen dieses dem Ereignis hinzu.
    rechner.OnBerechnungBeendet += new Rechner.BerechnungBeendet(rechner_OnBerechnungBeendet);

    // Wir beginnen die komplizierte Berechnung
    // der Addition zweier Zahlen.
    rechner.Berechne();
 
  }
}

... und fügen eine neue Methode hinzu:

class Client
{
  public void Berechne()
  {
    // Wir holen uns ein Objekt des Rechners
    Rechner rechner = new Rechner();

    // Wir abonnieren das Ereignis
    // Dazu erstellen wir ein neues Objekt 
    // des Delegaten "BerechnungBeendet" und 
    // fügen dieses dem Ereignis hinzu.
    rechner.OnBerechnungBeendet += new Rechner.BerechnungBeendet(rechner_OnBerechnungBeendet);

    // Wir beginnen die komplizierte Berechnung
    // der Addition zweier Zahlen.
    rechner.Berechne();
 
  }
 
  // ************************
  // Neue Methode hinzufügen:
  // ************************
  private void rechner_OnBerechnungBeendet(decimal ergebnis)
  {
    Console.WriteLine( ergebnis );
  }
}

Der Programmablauf erscheint bei der ersten Betrachtung etwas irritierend. Obwohl wir die Ausgabe in einer separaten Methode haben, die nirgendwo aufgerufen wird - und noch dazu private ist -, erscheint das Ergebnis auf dem Bildschirm.

Zur Erklärung müssen wir ein kleines bisschen ausholen, denn die einfache Erklärung "Der Delegat macht's möglich." reicht uns nicht wirklich aus...

Auch hier ist die Programmierung nichts anderes, als der Versuch, die alltägliche Realität in die Programmierung zu übernehmen: Ereignisse funktionieren ganz genauso, wie die Nachsendeliste der Post. Gibt es Post, und stehst du auf der Liste, wirst du informiert. Wenn nicht; dann nicht. Um aber auf die Nachsendeliste zu kommen, musst du die neue Adresse angeben.

Nichts anderes müssen wir machen: Wir tragen uns mit der neuen Adresse auf der Liste ein. Da aber die Post festlegt, wie das Format der Adresse auszusehen hat, müssen wir uns auch daran halten, wenn wir die Nachrichten wirklich bekommen wollen. In unserem Fall heißt das: Dieser Delegat (lies Verteiler)

delegate void BerechnungBeendet(decimal ergebnis);

akzeptiert nur Adressen (lies Methoden) als gültige Ziele, die den Rückgabe-Datentyp void haben und gleichzeitig einen Übergabe-Parameter des Datentyps decimal akzeptieren. In diesem Fall haben wir die Methode rechner_OnBerechnungBeendet in der Klasse Client als neue Zieladresse bestimmt,

void rechner_OnBerechnungBeendet(decimal ergebnis)

indem wir ihre Signatur den Anforderungen entsprechend formuliert und ihren Namen an den Delegaten übergeben haben. Es könnte aber jede beliebige Methode sein, vorausgesetzt, sie hält sich an die Signatur, die der Delegat bestimmt. Wie bei der echten Nachsendeliste darf die Methode (lies Adresse) ansonsten heißen, wie wir es möchten. Auch der Zugriffsmodifizierer ist dabei wahlfrei, obwohl wenn es nur selten Sinn macht, ihr andere Rechte als private zu geben.

Obwohl die Methode also private ist, verletzen wir auch keine Zugriffsrechte, denn der Delegat befindet sich innerhalb des Objekts der Klasse Client - wir haben ihn ja erst dort als Objekt erstellt -; darf also auch die privaten Methoden ansprechen.