Das Weak Event Pattern in .NET 4.5

13. Mai 2013
Bei der Verwendung von Events in .Net gibt es zwei beteiligte Parteien: das Objekt, das das Event auslöst (Sender), und das Objekt, das auf das ausgelöste Event reagiert (Empfänger). Bei der Zuweisung des Eventhandlers an das Event in der Form

SenderObjekt.Event += EmpfaengerObjekt.Handler

wird eine sogenannte strong reference zwischen Sender und Empfänger erzeugt. Durch diese Beziehung wird die Lebensdauer des Empfänger-Objektes durch die Lebensdauer des Sender-Objektes beeinflusst (sofern der Eventhandler nicht explizit entfernt wird) – was nicht immer gewünscht ist und auch zu Memory Leaks führen kann: das Empfänger-Objekt bleibt am Leben, obwohl dies nicht beabsichtigt ist.

Eine mögliche Lösung hierfür ist das Weak Event Pattern, das ich in diesem Artikel näher vorstellen und besonders auf die neuen Möglichkeiten unter .NET 4.5 eingehen möchte.

Ausgangssituation

Zur Verdeutlichung des angesprochenen Verhaltens dient das folgende Beispiel. Es wird die Beziehung zwischen einer Verwaltung von Arbeitszeiten und mehreren Ansichten (z. B. Wochenansicht, Tagesansicht etc.) dargestellt. Wir haben eine TimeEntryCollection, in der die Zeiteinträge verwaltet werden. Wird ein neuer Zeiteintrag hinzugefügt, so wird das Event TimeEntryAdded ausgelöst, das alle Ansichten über den neuen Zeiteintrag benachrichtigt.

   1: public class TimeEntryCollection

   2: {

   3:     public event EventHandler<TimeEntryEventArgs> TimeEntryAdded;

   4:  

   5:     List<TimeEntry> timeEntries = new List<TimeEntry>();

   6:  

   7:     public void AddTimeEntry(DateTime start, DateTime end, string description, string user)

   8:     {

   9:         var entry = new TimeEntry(start, end, description, user);

  10:  

  11:         this.timeEntries.Add(entry);

  12:         OnNewTimeEntryAdded(entry);

  13:     }

  14:  

  15:     private void OnNewTimeEntryAdded(TimeEntry entry)

  16:     {

  17:         if (TimeEntryAdded != null)

  18:         {

  19:             TimeEntryAdded(this, new TimeEntryEventArgs(entry));

  20:         }

  21:     }

  22: }

Die TimeEntryEventArgs, die dem Event mitgegeben werden, beinhalten in meinem Beispiel den Zeiteintrag selbst und den Zeitpunkt, an dem das Event ausgelöst wurde.

   1: public class TimeEntryEventArgs : EventArgs

   2: {

   3:     public TimeEntry TimeEntry { get; private set; }

   4:     public DateTime TimeStamp { get; private set; }

   5:  

   6:     public TimeEntryEventArgs(TimeEntry entry)

   7:     {

   8:         this.TimeEntry = entry;

   9:         this.TimeStamp = DateTime.Now;

  10:     }

  11: }

Für die Empfänger-Objekte gibt es eine WeekView– und eine DayView-Klasse, die bei hinzugefügten Zeiteinträgen benachrichtigt werden. Diese beiden Klassen stellen verschiedene Ansichten dar – eine Wochenansicht und eine Tagesansicht. In diesen Klassen wird nur über die Konsole ausgegeben, dass die entsprechende Ansicht über den neuen Zeiteintrag informiert wurde. Um das Beispiel kompakt zu halten, wird hier auf die Verwendung eines Interfaces verzichtet und die gleiche Methode doppelt implementiert.

   1: public class WeekView

   2: {

   3:     public void TimeEntryAdded(object sender, TimeEntryEventArgs e)

   4:     {

   5:         // hier könnten Berechnungen für die Wochenarbeitszeit durchgeführt werden

   6:         Console.WriteLine(

   7:             string.Format("Wochenansicht:
Zeit: {0} - {1}
Beschreibung: {2}
Benutzer: {3}
",

   8:                 e.TimeEntry.Start,

   9:                 e.TimeEntry.End,

  10:                 e.TimeEntry.Description,

  11:                 e.TimeEntry.User));

  12:     }

  13: }

   1: public class DayView

   2: {

   3:     public void TimeEntryAdded(object sender, TimeEntryEventArgs e)

   4:     {

   5:         // hier könnten Berechnungen für die Tagesarbeitszeit durchgeführt werden

   6:         Console.WriteLine(

   7:             string.Format("Tagesansicht:
Zeit: {0} - {1}
Beschreibung: {2}
Benutzer: {3}
",

   8:                 e.TimeEntry.Start,

   9:                 e.TimeEntry.End,

  10:                 e.TimeEntry.Description,

  11:                 e.TimeEntry.User));

  12:     }

  13: }

Das Programm selbst hält eine TimeEntryCollection, erzeugt die zwei Ansichten-Objekte und fügt mehrere Zeiteinträge in die Liste ein. Vor dem Einfügen des letzten Eintrages wird jedoch die Tagesansicht auf null gesetzt – in einem realen Szenario vielleicht aufgrund eines Wechsels der Ansicht.

   1: class Program

   2: {

   3:     static void Main(string[] args)

   4:     {

   5:         TimeEntryCollection timeEntries = new TimeEntryCollection();

   6:  

   7:         WeekView weekView = new WeekView();

   8:         timeEntries.TimeEntryAdded += weekView.TimeEntryAdded;

   9:  

  10:         timeEntries.AddTimeEntry(DateTime.Parse("08.03.2013 08:00"), DateTime.Parse("08.03.2013 16:30"), "Office Day", "Max");

  11:  

  12:         DayView dayView = new DayView();

  13:         timeEntries.TimeEntryAdded += dayView.TimeEntryAdded;

  14:  

  15:         timeEntries.AddTimeEntry(DateTime.Parse("28.02.2013 08:00"), DateTime.Parse("28.02.2013 18:30"), "Basta 2013", "Max");

  16:  

  17:         Console.WriteLine("Tagesansicht auf NULL setzen.
");

  18:         dayView = null;

  19:         timeEntries.AddTimeEntry(DateTime.Parse("27.02.2013 09:00"), DateTime.Parse("27.02.2013 10:30"), "Meeting", "Max");

  20:         System.GC.Collect();

  21:         Console.WriteLine("Garbage Collector hat aufgeräumt.
");

  22:  

  23:         timeEntries.AddTimeEntry(DateTime.Parse("08.02.2013 08:00"), DateTime.Parse("08.02.2013 18:30"), "Kick-Off 2013", "Max");

  24:  

  25:         Console.ReadLine();

  26:     }

  27: }

Die Eventhandler werden auf die bekannte Art und Weise zugewiesen. Um sicherzustellen, dass nicht mehr benötigte Objekte durch den Garbage Collector aufgeräumt werden, wird dieser hier explizit aufgerufen. Das gezeigte Programm erzeugt folgende Ausgabe auf der Konsole:

image

Wie zu sehen ist, wird auch die Tagesansicht sowohl über den Zeiteintrag für den Kick-Off als auch das Meeting benachrichtigt, obwohl die Referenz vorher auf null gesetzt wurde und der Garbage Collector gelaufen ist – das Objekt bleibt also durch den hinzugefügten Eventhandler am Leben. Die einfachste Variante, die Beziehung zwischen Sender und Empfänger aufzulösen, ist sicherlich das Deregistrieren des Eventhandlers:

Fügen wir die folgende Zeile vor der Stelle ein, an der die Referenz auf null gesetzt wird, so wird auch der Eventhandler nicht mehr aufgerufen:

timeEntries.TimeEntryAdded –= dayView.TimeEntryAdded;

image

Doch wie bereits gesagt, ist dies nicht immer möglich (z. B. bei der Programmierung von eigenen Controls, die von anderen verwendet werden) – und dann kommt das Weak Event Pattern ins Spiel.

Das Weak Event Pattern – bisherige Vorgehensweise

Das Weak Event Pattern an sich existiert bereits seit .NET 3.0. Es wurde entworfen, um eine lose Kopplung zwischen Sender und Empfänger zu ermöglichen, sodass sich ein Empfänger an das Event eines Senders hängen kann, die Lebenszeit des Empfängers aber nicht durch die Lebenszeit des Senders bestimmt wird. Diese lose Kopplung zwischen den beiden Objekten wird als weak reference bezeichnet – daher auch der Name Weak Event Pattern.

Bis .NET 4.5 war die Implementierung des Weak Event Patterns eher aufwendig. Zunächst musste eine Manager-Klasse von WeakEventManager abgeleitet werden (normalerweise für jedes Event eine eigene Manager-Klasse). Die Klasse, die auf das Event reagieren wollte, musste dann das IWeakEventListener-Interface und dadurch die ReceiveWeakEvent-Methode implementieren. Die Verbindung zwischen Sender und Empfänger musste dann über die AddListener-Methode des Managers erfolgen. Und das alles nur, um die enge Kopplung zwischen Sender und Empfänger zu lösen. Schon aufwendig, oder?

Und so geht’s in .NET 4.5

Das dachte man sich anscheinend auch im Hause Microsoft, und hat mit .NET 4.5 eine generische Version des WeakEventManagers eingeführt (die bisherige Variante bleibt aber weiterhin als Möglichkeit bestehen). Die Klassen TimeEntryCollection, TimeEntry, TimeEntryEventArgs, DayView und WeekView müssen nicht geändert werden – und wir brauchen auch keinen eigenen WeakEventManager, denn es gibt nun einen generischen WeakEventManager mit folgende Signatur:

WeakEventManager<TEventSource, TEventArgs>

TEventSource ist der Typ des Senders, TEventArgs der Typ der Argumente des Events. Auf diesem Manager gibt es eine AddHandler-Methode mit folgender Signatur:

public static void AddHandler(
    TEventSource source,
    string eventName,
    EventHandler<TEventArgs> handler
)

Source ist das Sender-Objekt, eventName der Name des Events als String und handler die Eventhandler-Methode. Um diesen WeakEventManager zu verwenden, müssen wir nun lediglich das Setzen des Eventhandlers über eben diese AddHandler-Methode durchführen.

Somit sieht unser Programm nun folgendermaßen aus:

   1: class Program

   2: {

   3:     static void Main(string[] args)

   4:     {

   5:         TimeEntryCollection timeEntries = new TimeEntryCollection();

   6:  

   7:         WeekView weekView = new WeekView();

   8:         // Verknüpfen des Events mit dem Handler nun über den WeakEventManager

   9:         //timeEntries.TimeEntryAdded += weekView.TimeEntryAdded;

  10:         WeakEventManager<TimeEntryCollection, TimeEntryEventArgs>.AddHandler(timeEntries, "TimeEntryAdded", weekView.TimeEntryAdded);

  11:  

  12:         timeEntries.AddTimeEntry(DateTime.Parse("08.03.2013 08:00"), DateTime.Parse("08.03.2013 16:30"), "Office Day", "Max");

  13:  

  14:         DayView dayView = new DayView();

  15:         // Verknüpfen des Events mit dem Handler nun über den WeakEventManager

  16:         //timeEntries.TimeEntryAdded += dayView.TimeEntryAdded;

  17:         WeakEventManager<TimeEntryCollection, TimeEntryEventArgs>.AddHandler(timeEntries, "TimeEntryAdded", dayView.TimeEntryAdded);

  18:  

  19:         timeEntries.AddTimeEntry(DateTime.Parse("28.02.2013 08:00"), DateTime.Parse("28.02.2013 18:30"), "Basta 2013", "Max");

  20:         // Eventhandler wird nicht deregistriert

  21:         //timeEntries.TimeEntryAdded -= dayView.TimeEntryAdded;

  22:  

  23:         Console.WriteLine("Tagesansicht auf NULL setzen.
");

  24:         dayView = null;

  25:  

  26:         timeEntries.AddTimeEntry(DateTime.Parse("27.02.2013 09:00"), DateTime.Parse("27.02.2013 10:30"), "Meeting", "Max");

  27:         System.GC.Collect();

  28:         Console.WriteLine("Garbage Collector hat aufgeräumt.
");

  29:  

  30:         timeEntries.AddTimeEntry(DateTime.Parse("08.02.2013 08:00"), DateTime.Parse("08.02.2013 18:30"), "Kick-Off 2013", "Max");

  31:  

  32:         Console.ReadLine();

  33:     }

  34: }

Und das kommt in der Kommandozeile an:

image

Die Ausgabe zeigt, dass nun der Zeiteintrag für den Kick-Off nicht mehr in der Tagesansicht erscheint – denn diese gibt’s ja nicht mehr. Aber auch hier wurde explizit der Garbage Collector aufgerufen. Der Eintrag für das Meeting wurde vor dem Aufruf des Garbage Collectors eingefügt und somit wurde auch die Tagesansicht noch benachrichtigt. Dies gilt es in der Praxis zu berücksichtigen, denn der Eventhandler wird trotzdem aufgerufen, wenn der Garbage Collector noch nicht aufgeräumt hatte.

Fazit

Mit .NET 4.5 wurde die Verwendung des Weak Event Patterns deutlich vereinfacht, und macht es somit auch attraktiver, benutzt zu werden. Die generische Variante ist nicht ganz so effizient wie eine speziell abgeleitete Variante, es ist aber deutlich weniger Code erforderlich. Sollte die Performance also nicht an erster Stelle stehen, so ist die neue generische Variante eine gute und einfache Möglichkeit, das Weak Event Pattern einzusetzen.

Wichtig: das Weak Event Pattern verhindert nicht, dass unnütze Events behandelt werden. Es verhindert lediglich, dass Objekte durch Event-Registrierungen am Leben erhalten werden, obwohl dies nicht notwendig ist.

Wer sich umfassender informieren möchte, dem seien folgende Seiten empfohlen: