Events in C#

Oft ist es notwendig, dass ein Quellcode ausgeführt wird, wenn ein Button geklickt, ein Programm gestartet oder ein Status erreicht wurde. In vielen Fällen liegt es hierbei an “fremden” Objekten, dass etwas passiert ist, dann soll jedoch entsprechend darauf reagiert werden. Eine Nachricht muss von diesem “fremden” Objekt empfangen werden, wenn das entsprechende Ereignis eintritt. Dieses Ereignis/Event ist der springende Punkt, es kann vom eigenen Programmcode (im Algorithmus oder durch Interaktion mit Benutzer oder Netzwerk) oder auch vom Betriebssystem ausgehen.

Das Betriebssystem und das Framework stellen sehr viele vorgefertigte Events zur Verfügung, welche nur abonniert werden müssen. Bei der Arbeit mit Windows.Forms wird das Ereignis des Button.Click beispielsweise häufig verwendet. Dieses wird z.B. von einem Button button1 wie folgt von der eigenen Klasse (welche auf das Ereignis reagieren soll) abonniert:


this.button1.Click += new System.EventHandler(this.button1_Click);

Dabei wird eine Methode einem Delegate (Funktionszeiger) zugewiesen. Diese Methode heißt im Beispiel button1_Click, sie soll ausgeführt werden, wenn der button1 geklickt wird.

private void button1_Click(object sender, EventArgs e)
{
\\ tu etwas
}

Die Methode ist die Event-Reaktion. Wird das Event ausgeführt, also der Button geklickt, führt ein Funktionszeiger, der auf diese Methode zeigt, diese aus. Die Methode hat zwei Parameter (Pflichtparameter bei vom Framework vordefinierten Events), auf welche später weiter eingegangen wird.

Eventhandling

Tritt ein Objekt ein Event los, werden alle abonnierenden Objekte benachrichtigt. Bei Button.Click ist der Button das publizierende Objekt. Die Abonnenten können nach der Benachrichtigung, dass ein Event eingetreten ist, entsprechend reagieren. Wie die empfangenden Objekte auf das Ereignis reagieren, interessiert den Event-Publizierer nicht. Dabei wird eine 1-zu-n Beziehung angewandt, denn ein Publizierer kann sehr viele Abonnenten benachrichten.

Ein eigenes Event definieren

Nachfolgend soll ein kleines Programm beschrieben werden, welches von einem Startwert zu einem Endwert zählt. Dabei wird über einen Parameter eine ArrayList an Zahlen übergeben, welche als kritisch eingestuft werden. Stößt die Zählschleife auf diese Zahlen, soll sie sofort (per Event) Alarm schlagen! Wie abonnierende Objekte darauf dann reagieren, ist der Zählschleife ganz egal, nur Alarmschlagen liegt in ihrem Aufgabenbereich.


public void Count(int start, int end, ArrayList items)
{
   for (int i = start; i <= end; i++)
   {
      if (items.Contains(i))      // Ein Item erwischt?
      {
         // mach was, aber nicht hier!!
      }
   }
}

Um ein Event-Handling (darf beruhigt als Ereignis-Behandlung bezeichnet werden) einzurichten, ist ein EventHandler notwendig. Dieser ist nichts weiter als ein Delegate (Funktionszeiger), welcher auf eine oder mehrere Methoden zeigt. Das Delegate überbrückt die Distanz zwischen Event-Sender und den entkoppelten Event-Empfängern. Im folgenden Beispiel wird ein EventHandler namens CountEventHandler definiert, er hat zwei Parameter vom Typ object bzw. InfoEventArgs.


// Delegate für die Abonnenten
public delegate void CountEventHandler(object Instanz, InfoEventArgs EventInfo);

Die beiden Parameter sind nicht unbedingt für eine Event-Behandlung notwendig, haben jedoch ihren Zweck. Vordefinierte Events des Frameworks sind nach gleichem/ähnlichem Schema eingerichtet. Der erste Parameter vom Typ object verweist auf den Absender (Ereignisauslöser). So kann der Empfänger wissen, welches Objekt das Ereignis auslöste bzw. bei welchem es ausgelöst wurde (dies kann z.B. wichtig sein, wenn mehrere EventHandler auf die gleiche Methode verweisen, also eine n-zu-n Beziehung umgesetzt wurde).

Der zweite Parameter vom Typ InfoEventArgs ist ein spezieller Informationslieferant. Er soll verschiedene Auskünfte über das Event geben.
I.d.R. (spezielle Events ausgenommen) wird statt des Parameters vom Typ InfoEventArgs ein Parameter vom Typ EventArgs übergeben. Dieser Parameter wird bei der Event-Reaktion eine wichtige Rolle spielen.

Das Delegate muss natürlich noch deklariert werden:


// Deklaration des Delegates
public CountEventHandler ItemMatch;

Nun existiert ein Delegate vom Typ CountEventHandler mit dem Instanz-Namen ItemMatch. Dieses soll Abonnenten benachrichtigen, wenn eine kritische Zahl erreicht wurde.

InfoEventArgs ist eine, eigens für dieses Event neuverfasste, Definition von der Klasse EventArgs aus dem .NET-Framework. Dabei wird hier eine eigene Klasse geschrieben, die von der Klasse EventArgs erbt, denn die vorgegebene Klasse EventArgs stellt für dieses Beispiel eine Information zu wenig zur Verfügung.

public class InfoEventArgs : EventArgs
{
   public readonly int number;

   public InfoEventArgs(int number)
   {
      this.number = number;
   }
}

Zweck dieser Eigendefinition ist die Bereitstellung der Zahl, welche für das Event (nämlich dem Alarm) ausschlaggebend war. Diese Zahl soll der Ereignisauslöser bereitstellen und der Ereignis-Abonnent erhalten können, jedoch wirklich nur erhalten, nicht verändern (daher readonly).

Sind keine maßgeschneiderten Event-Informationen notwendig, kann die vorgefertigte EventArgs Klasse verwendet werden (sie darf bei eigenen Events auch ganz weggelassen werden, wenn keine Event-Informationen benötigt werden und die Abonnenten-Benachrichtigung alleine ausreicht). Dann muss eine Instanz der Klasse EventArgs deklariert und instanziiert werden:

System.EventArgs eventInfo = new System.EventArgs();

Da hier jedoch die eigene Definition benutzt wird, wird stattdessen die vorherig beschriebene InfoEventArgs Klasse deklariert.

// Instanzierung der Klasse für die Event-Informationen
InfoEventArgs eventInfo;

Die eigentliche Zählschleife wird um einige Anweisungen erweitert. Wenn eine kritische Zahl erreicht wurde, also die Bedingung items.Contains(i) erfüllt ist, soll Alarm geschlagen werden. Dazu wird eine Instanz der InfoEventArgs erstellt und dieser über den Konstruktor-Parameter die kritische Zahl übergeben. Dann werden alle Abonnenten benachrichtigt. Gibt es keine Abonnenten, ist das Delegate mit dem Wert Null ausgewiesen, so dass (dank der if-Bedingung) die Benachrichtigung nicht versucht wird. Die Benachrichtigung geschieht durch das Delegate als Stellvertreter für die Methoden der Abonnenten. Diese Methoden, auf welche das Delegate zeigt, müssen zwei Parameter empfangen, wie es bereits bei der Delegate-Definition festgeschrieben wurde.

Zu einem den Parameter vom Typ object, welcher das eigene Objekt (referenziert durch das Schlüsselwort this), zum anderen den Parameter, welcher Auskunft über das Event selbst geben soll, das Objekt eventInfo.


// Zählschleife
public void Count(int start, int end, ArrayList items)
{
   for (int i = start; i <= end; i++)
   {
      if (items.Contains(i))      // Ein Item erwischt?
      {
         // Event-Information bereitstellen
         eventInfo = new InfoEventArgs(i);

         if (ItemMatch != null)
         {
            // Action! Benachrichtigung der Abonnenten!
            ItemMatch(this, eventInfo);
         }
      }
   }
}

Das Event ist nun vollständig eingerichtet und wird vom Objekt publiziert, d.h. es kann von anderen Objekten (die Zugriff auf das Objekt haben) abonniert werden.

Einen Abonnenten einrichten:

Nun wird eine neue, eigene Klasse Alarm definiert, welche Alarm schlagen soll. Diese Klasse hat einen Konstruktor, der das Objekt als Parameter empfängt, welches das Event anbietet. In diesem Beispiel ist es das Objekt der Klasse Counter, das zur Laufzeit instanziiert werden soll.


public class EventAlarm
{
   public EventAlarm(Counter counter)    // Konstruktor
   {
   }

   public void Alarm(int item)
   {
      Console.WriteLine("Die kritische Zahl " + item.ToString() + " wurde gefunden!");
   }
}

Wenn die Instanz der Klasse Counter das Event lostritt, soll die Methode Alarm der Klasse EventAlarm aufgerufen werden. Dazu wird eine weitere Methode implementiert:

public void ItemMatched(object sender, InfoEventArgs e)
{
   this.Alarm(e.number);
}

Diese Methode ItemMatched ruft die Methode Alarm auf. Nun spielen die Parameter vom Typ object bzw. InfoEventArgs wieder eine Rolle. Diese Parameter müssen in die Signatur der Methode, denn der EventHandler (Delegate) soll auf diese Methode zeigen. Der EventHandler setzt diese Parameter (wie in der Definition des EventHandlers festgelegt) voraus.

Der Parameter e vom Typ InfoEventArgs bietet Zugriff auf die Zahl, die zum Event führte (siehe dazu die Klassen-Definition InfoEventArgs). Der Parameter darf natürlich beliebig benannt werden, Events des .NET Frameworks nennen ihn standardmäßig jedoch auch e.

Damit diese Methode ItemMatched auch im Falle eines Events aufgerufen wird, muss das Objekt der Klasse EventAlarm das Event mit dieser Methode abonnieren, dies geschieht im Konstruktor wie folgt:

public EventAlarm(Counter counter)
{
   // Der Funktionszeiger soll auch auf die Funktion "ItemMatched" zeigen
   counter.ItemMatch += new Counter.CountEventHandler(ItemMatched);
}

Das Objekt counter der Klasser Counter stellt den EventHandler ItemMatch zur Verfügung. Eine neue Referenz auf eine Methode wird mittels new-Operator eingerichtet, der Methodenname wird als Parameter (als Referenz) übergeben. Da bekannt ist, dass es nur einen einzigen Abonnent gibt, dürfte auf die Zuweisung mit “+=” verzichtet werden. Sobald es jedoch weitere Abonnenten gibt, würden diese überschrieben werden (und als Abonnenten verloren gehen), daher sollte immer die “+=” Operatorkombination verwendet werden.

Das Event ist nun mit einem Abonnent vollständig eingerichtet.

Nachfolgend der gesamte Quellcode im Zusammenhang:

/// <summary>
    /// Start-Klasse
    /// </summary>
    public class Program
    {
        public static void Main()
        {
            Program program = new Program();
        }

        private Program()
        {
            Counter counter = new Counter();
            EventAlarm eventAlarm = new EventAlarm(counter);

            ArrayList items = new ArrayList();
            items.AddRange(new object [] { 2, 50, 88});

            counter.Count(0, 100, items);
        }
    }

    /// <summary>
    /// Zusätzliche Event-Information in Klasse lagern
    /// Erbt von EventArgs, welche Standard-Informationen zur Verfügung stellt
    /// </summary>
    public class InfoEventArgs : EventArgs
    {
        public readonly int number;

        public InfoEventArgs(int number)
        {
            this.number = number;
        }
    }

    /// <summary>
    /// Event-Publizierer
    /// </summary>
    public class Counter
    {
        // Delegate für die Abonnenten
        public delegate void CountEventHandler(object Instanz, InfoEventArgs EventInfo);

        // Deklaration des Delegates
        public CountEventHandler ItemMatch;

        // Instanzierung der Klasse für die Event-Informationen
        InfoEventArgs eventInfo;

        public Counter()    // Konstruktor
        {

        }

        // Zählschleife
        public void Count(int start, int end, ArrayList items)
        {
            for (int i = start; i <= end; i++)
            {
                if (items.Contains(i))      // Ein Item erwischt?
                {
                    // Event-Information bereitstellen
                    eventInfo = new InfoEventArgs(i);

                    if (ItemMatch != null)
                    {
                        // Action! Benachrichtigung der Abonnenten!
                        ItemMatch(this, eventInfo);
                    }
                }
            }
        }
    }

    /// <summary>
    /// Diese Klasse aboniert das Event und schlägt Alarm, wenn es eintritt
    /// </summary>
    public class EventAlarm
    {
        public EventAlarm(Counter counter)
        {
            // Der Funktionszeiger soll auch auf die Funktion "ItemMatched" zeigen
            counter.ItemMatch += new Counter.CountEventHandler(ItemMatched);
        }

        public void Alarm(int item)
        {
            Console.WriteLine("Die kritische Zahl " + item.ToString() + " wurde gefunden!");
        }

        public void ItemMatched(object sender, InfoEventArgs e)
        {
            this.Alarm(e.number);
        }
    }

Ausgabe:

[ad#Google Adsense]

Benjamin Aunkofer ist Wirtschaftsingenieur und -informatiker. Er arbeitet in den interdisziplinären Themengebieten als Dozent an der HTW Berlin und engagiert sich als Unternehmer insbesondere für Big Data und Industrie 4.0.