Die Problematik von Memoryleaks durch Eventhandler wurde mir in den vergangenen Tagen wieder deutlich vor Augen geführt.
Ausgangssituation:
Ein Silverlight 4 Client, welcher das MVVM (Model View ViewModel) implementiert, benötigt im ViewModel die Benachrichtigung wenn sich in einer ObservableCollection des Models etwas verändert hat (Add, Remove). Die einfachste Möglichkeit ist, sich am CollectionChanged Event zu abonieren.
Das funktioniert alles prima. Doch nun navigiert der Benutzer wo anders hin und die View sowie das ViewModel müssen freigegeben werden. Werden Sie aber nicht! Warum? Ganz einfach. Die View hat durch die Bindings Referenzen auf das ViewModel. Das Model ist dem Ganzen übergeordnet und muss überleben. Das Problem ist, dass das Model durch die Event Registrierung eine Referenz auf das ViewModel in Form eines Callbacks hält.
Aus diesem Grund wird View und ViewModel erst mit der Freigabe des Models freigegeben. Navigiert der Benutzer nun einige Male hin und her dann wird dieser Effekt mit Sicherheit in einer OutOfMemoryException enden, da die Views meist recht speicherintensiv sind.
Weisen wir nun als Erstes den Sachverhalt mit einer Konsolenapplikation nach:
static void Main()
{
var model = new TestModel();
var viewModel = new TestViewModel();
model.TestEvent += viewModel.ViewModelEventHandler;
model.RaiseEvent();
viewModel = null;
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
model.RaiseEvent();
model.RaiseEvent();
}
public class TestModel
{
public event EventHandler TestEvent;
public void RaiseEvent()
{
if (TestEvent != null) TestEvent(this, EventArgs.Empty);
}
}
public class TestViewModel
{
public void ViewModelEventHandler(object sender, EventArgs e)
{
Console.WriteLine("Event fired!");
}
}
Auf der Konsole wird 3x “Event fired!” stehen, da das ViewModel trotz Freigabe der Instanz und explizitem Aufruf des Garbage Collectors nicht freigegeben wird. Es versteht sich von selbst, dass die Aufrufe der GC Klasse hier nur zu Anschauungszwecken dienen und nicht in produktivem Code verwendet werden.
Das Problem lässt sich recht einfach mit diesem Code Snippet lösen.
public class WeakEventHandler where TEventArgs : EventArgs
{
public WeakEventHandler(object eventSource, string eventName, object eventDestination, string eventHandlerName)
{
var weakSourceReference = new WeakReference(eventSource);
var weakDestiantionReference = new WeakReference(eventDestination);
EventHandler = delegate(object sender, TEventArgs e)
{
if (weakDestiantionReference.IsAlive)
{
//Invoke EventHandler if object is still alive.
weakDestiantionReference.Target.GetType().GetMethod(eventHandlerName).Invoke(weakDestiantionReference.Target, new[] { sender, e });
}
else if(weakSourceReference.IsAlive)
{
//Deregister the Event because source is not alive anymore.
weakSourceReference.Target.GetType().GetEvent(eventName).GetRemoveMethod().Invoke(weakSourceReference.Target, new[] { EventHandler });
}
};
}
public EventHandler EventHandler { get; private set;}
}
Die Registrierung des Events sieht dann wie folgt aus:
model.TestEvent += new WeakEventHandler(model, "TestEvent", viewModel, "ViewModelEventHandler").EventHandler;
Der Unterschied besteht darin, dass ein Eventhandler dieser Wrapper Klasse registriert wird. Der Verweis auf das eigentliche Objekt wird über eine WeakReference gehalten. Die Klasse fungiert dabei als Router und checkt beim Aufrufen des EventHandlers, ob das eigenliche Zielobjekt – in diesem Fall das ViewModel – noch lebt. Wurde das ViewModel bereits freigegeben, wird der eigene Eventhandler deregistriert und auch diese Klasse wird freigegeben. Es gibt jedoch 2 Haken an dieser Lösung:
1. Solange das Event nicht nach der Freigabe des ViewModels ausgelöst wird, wird die Wrapper Klasse nicht freigegeben. View und ViewModel tangiert dies nicht. Da die Klasse sehr schmal ist, wird man damit leben können.
2. Die Eventregistrierung ist nicht typisiert. Sowohl der Eventname als auch der Handlername werden als Strings mitgegeben. Ich persönlich finde das zwar hässlich, habe aber keine andere Möglichkeit gefunden, da man zwar den Handlernamen über eine LinqExpression typisiert übergeben kann aber bei dem Event gelingt dies nicht.
Fazit:
Die aufgezeigte Lösung schafft Abhilfe für das Problem, hat aber auch ihre Tücken. Am Saubersten ist die EventHandler, sobald man die Instanz nicht mehr benötigt, manuell abzuhängen. Allerdings erfordert dies eine Menge Disziplin und man muss auf anonyme Methoden verzichten.

