Weak event patterns, część II

W poprzednim poście podałem prostą implementację zdarzeń opartych na słabych referencjach. Zachęcam najpierw do przeczytania poprzedniego wpisu bo bez niego będzie ciężko zrozumieć dzisiejszy post. Dziś implementacja zaproponowana na blogu Paul Stovell’a:

public sealed class WeakEventHandler<TEventArgs> where TEventArgs : EventArgs
{
   private readonly WeakReference _targetWeakReference;
   private readonly MethodInfo _handlerMethodInfo;

   public WeakEventHandler(EventHandler<TEventArgs> strongEventHandler)
   {
       _handlerMethodInfo = strongEventHandler.Method;
       _targetWeakReference = new WeakReference(strongEventHandler.Target, true);
   }

   public void Handler(object sender, TEventArgs e)
   {
       var target = _targetWeakReference.Target;

       if (target != null)
       {
           var handler =
               (Action<object, TEventArgs>)
               Delegate.CreateDelegate(typeof (Action<object, TEventArgs>), target, _handlerMethodInfo, true);
           
           if (handler != null)
           {
               handler(sender, e);
           }
       }
   }
}

Konstruktor przyjmuje jako parametr zwykły, klasyczny handler. Za pomocą EventHandler’a możemy odczytać na jakim obiekcie ma zostać on wykonany (target – wskaźnik na obiekt) oraz informacje o sygnaturze metody w postaci MethodInfo. Dzięki temu mamy informacje potrzebne do wykonania zdarzenia. MethodInfo dostarcza nam nazwę metody, parametry wejściowe oraz wyjściowe. Target natomiast wskazuje obiekt na którym ta metoda ma zostać wykonana. Innymi słowy wiemy co mamy wykonać (methodinfo) oraz gdzie (target).

Target jest oczywiście zapisany za pomocą słabej referencji. Publiczna metoda Handler odczytuje target i jeśli nie został on usunięty przez GC wtedy tworzy delegate, która wskazuje na zdarzenie. W celu stworzenia delegaty przekazujemy informacje o metodzie oraz obiekcie docelowym. W skrócie handler wywoła zdarzenie tylko wtedy gdy obiekt jeszcze nie został usunięty z pamięci.

Klasa jest dość generyczna – to jej główna zaleta. Możemy ją wykorzystać do dowolnych zdarzeń. Spróbujmy zatem użyć jej na przykładzie z poprzedniego posta:

public class SampleClass:INotifyPropertyChanged
{
   public event PropertyChangedEventHandler PropertyChanged;
   public void RefreshProperty(string propName)
   {
       if(PropertyChanged!=null)
           PropertyChanged(this,new PropertyChangedEventArgs(propName));
   }
}
public class ViewModel
{
    public ViewModel(SampleClass sampleClass)
    {
        sampleClass.PropertyChanged +=
            new WeakEventHandler<PropertyChangedEventArgs>(SampleClassPropertyChanged).Handler;
    }

    void SampleClassPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        // do something
    }   
}

// TEST
SampleClass sampleClass = new SampleClass();
ViewModel viewModel = new ViewModel(sampleClass);        

sampleClass.RefreshProperty(null);
viewModel = null;
GC.Collect();
sampleClass.RefreshProperty(null);

Warto zwrócić uwagę na sposób doczepiania zdarzenia:

sampleClass.PropertyChanged +=
            new WeakEventHandler<PropertyChangedEventArgs>(SampleClassPropertyChanged).Handler;

Najpierw tworzony jest WeakEventHandler, za pomocą konstruktora przekazywana jest referencja na silny handler a na końcu jako callback  jest przekazywana metoda Handler klasy WeakEventHandler. Z tego względu SampleClass ma referencje do WeakEventHandler a nie ViewModel.

Uruchamiając test przekonamy się, że SampleClassPropertyChanged zostanie wykonany tylko RAZ a nie dwa razy jakby było to w przypadku silnych referencji. Po wyzerowaniu viewModel, drugie wywołanie RefreshProperty nie spowoduje wykonanie zdarzenia.

Jakie wady ma powyższe rozwiązanie? Wciąż niestety w pamięci jest tzw. “sacrifice object”.  Rozwiązanie nadaje się w sytuacjach gdy rozmiar WeakEventHandler jest dużo mniejszy od ViewModel.