Advanced  Services Events


Vom Delegate zum Event

Events sind spezialisierte Delegates. Hinter jedem Event steht immer ein Delegate. Man muß also zuerst ein Delegate haben, bevor man ein Event anlegen kann. Wie der Name schon vermuten läßt, werden Events für die Realisierung des sog. Observable-Observer-Patterns verwendet (auch Sender-Receiver-Pattern genannt). In den folgenden Beispielen realisieren wir das Eventhandling zunächst mit einem Delegate und verwenden schließlich im dritten Beispiel einen Event. Auf diese Weise wird klar, warum man Events eingeführt hat und nicht nur mit Delegates arbeitet.

Worterklärung: Der Observer ist eine Art Beobachter der Objekte beobachtet, die Observables.

Für alle Beispiele verwenden wir das folgende Delegate, das schon den passenden Namen trägt.

public delegate void CustomEventHandler(object sender);

Beispiel1: Eventhandling mit Add- und RemoveMethoden

Wir beginnen mit dem Receiver (Empfänger). Sein Aufbau ist einfach.

class Receiver
{
   public void OnEvent(object sender)
   {
      Console.WriteLine("Receiver: EventHandler called by " + sender);
   }
}

Er erhält eine Nachricht und reagiert darauf. In diesem Fall besteht die Reaktion aus einer einfachen Konsolausgabe. Unser Receiver braucht natürlich einen Sender, der ihn benachrichtigt. Das geht nur, wenn der Sender den Empfänger kennt, anders ausgedrückt, ein Receiver muß beim Sender registriert sein, erst dann kann er Nachrichten erhalten.

Der erste Sender enthält Add- und Removemethoden um Empfänger einzutragen oder zu entfernen. Außerdem noch eine Methode Notify(), die alle Empfänger benachrichtigt.

class Sender
{
  // delegate
   private CustomEventHandler receivers = null;

   public void Notify()
   {
      if (receivers != null)
      {
         receivers(this);
      }
   }

   public void AddReceiver(Receiver receiver)
   {
      this.receivers += receiver.OnEvent;
   }

   public void AddReceiver(CustomEventHandler handler)
   {
      this.receivers += handler;
   }

   public void RemoveReceiver(Receiver receiver)
   {
      this.receivers -= receiver.OnEvent;
   }

   public void RemoveReceiver(CustomEventHandler handler)
   {
      this.receivers += handler;
   }
}

Eine Main-Methode, die diese Konstellation verwendet.

public static void Main(string[] args)
{
   Sender1 sender = new Sender1();
   Receiver1 receiverA = new Receiver1();
   Receiver1 receiverB = new Receiver1();
   sender.addReceiver(receiverA);
   sender.addReceiver(receiverB.OnEvent);
   sender.NotifyHandler();

   Console.WriteLine("\nremove one receiver\n");
   sender.removeReceiver(receiverA);
   sender.NotifyHandler();
   Console.WriteLine();
}

Screenshot


Beispiel2: Eventhandling mit Properties

Wir verwenden die in C# üblichen Properties. Der Sender sieht dann folgendermaßen aus.

class Sender
{
   private CustomEventHandler receivers = null;

   public CustomEventHandler Receiver
   {
      get { return receivers; }
      set { receivers = value; }
   }

   public void NotifyHandler()
   {
      if (receivers != null)
      {
         receivers(this);
      }
   }
}

Am Receiver hat sich nichts geändert.

class Receiver
{
   public void OnEvent(object sender)
   {
      Console.WriteLine("Receiver: EventHandler called by " + sender);
   }
}

Die Main-Methode.

public static void Main(string[] args)
{
   Sender2 sender = new Sender2();
   Receiver2 receiverA = new Receiver2();
   Receiver2 receiverB = new Receiver2();
   sender.Receiver += receiverA.OnEvent;
   sender.Receiver += receiverB.OnEvent;

   sender.NotifyHandler();

   Console.WriteLine("\nremove one receiver\n");
   sender.Receiver -= receiverB.OnEvent;
   sender.NotifyHandler();
   Console.WriteLine();
}

Die Ausgabe ist identisch zum ersten Beispiel. Diese Variante hat aber im Gegensatz zum vorigen Beispiel unangenehme Nebeneffekte. Verwendet man die Property wie üblich in einer Zuweisung, so werden alle anderen Receiver gelöscht, was im folgenden gezeigt wird. Zudem sind über die Property mit Hilfe von Invoke alle Receiver direkt aufrufbar.

public static void Main(string[] args)
{
   Sender2 sender = new Sender2();
   Receiver2 receiverA = new Receiver2();
   Receiver2 receiverB = new Receiver2();
   sender.Receiver += receiverA.OnEvent;
   sender.Receiver += receiverB.OnEvent;

   sender.NotifyHandler();

   Console.WriteLine("\nremove one receiver\n");
   sender.Receiver -= receiverB.OnEvent;
   sender.NotifyHandler();
   Console.WriteLine();

   sender.Receiver = Events3.Method;
   sender.NotifyHandler();
}

//
static void Method(object noSender)
{
   Console.WriteLine("not called from a method of the sender");
}

Screenshot

Der folgende Screenshot zeigt, daß alle Delegatemethoden frei zugänglich sind:


Beispiel3: Eventhandling mit Events

Das obige Verhalten widerspricht natürlich der in der Objektorientierung üblichen Datenkapselung. Mit Hilfe von Events läßt sich dies vermeiden. Das Schlüsselwort event schränkt die Möglichkeiten einer Delegatevariablen soweit ein, daß die Datenkapselung wiederhergestellt ist. Das folgende Beispiel zeigt dies.

class Sender
{
   // dahinter steht eine automatisch erzeugte private Handlerinstanz !
   public event CustomEventHandler Receiver = null;

   public void NotifyHandler()
   {
      if (Receiver != null)
      {
         Receiver(this);
      }
   }
}

Ähnlich wie bei automatischen Properties erzeugt hier der Compiler im Hintergrund sinngemäß folgenden Code:

private CustomEventHandler myEvent;
public event Receiver
{
  add    { myEvent += value; }
  remove { myEvent -= value; }
}

Statt get und set gibt es also add und remove Accessors. Zudem wird die Sichtbarkeit drastisch eingeschränkt. Keine einzige der Methoden und Properties von System.Delegate ist von außen sichtbar und die einzigen von außen anwendbaren Operatoren sind += und -=. Damit ist dieses Beispiel vergleichbar mit dem ersten Beispiel, das Add- und Removemethoden bereitstellt.

Siehe hierzu auch stackoverflow eventhandler oder Troelsen Pro C# Pro C# 2010 and the .NET 4 Platform Apress 5.Edition p.421

Main demonstriert die Verwendung.

public static void Main(string[] args)
{
   // event CustomEventHandler ch;

   Sender3 sender = new Sender3();
   Receiver3 receiverA = new Receiver3();
   Receiver3 receiverB = new Receiver3();
   sender.Receiver += receiverA.OnEvent;
   sender.Receiver += receiverB.OnEvent;

   //sender.Receivers = receiverA.OnEvent;  // nicht mehr zulässig für Events

   sender.NotifyHandler();

   Console.WriteLine("\nremove one receiver\n");
   sender.Receiver -= receiverB.OnEvent;
   sender.NotifyHandler();
}

Wo kann man eine Variable vom Typ eines Events anlegen

das Schlüsselwort event schränkt auch in diesem Fall die Möglichkeiten deutlich ein. Im Gegensatz zu einer Delegatevariablen kann man Eventvariablen nur als Instanzvariablen in Klassen, aber auch in Interfaces anlegen.

Die Implementierung eines Interfaces, das eine Eventvariable enthält ist trivial.

interface IFace
{
   event CustomEventHandler handler;
}

class Xxx : IFace
{
   public event CustomEventHandler handler;
}