Pages

Thursday, October 10, 2013

How to subscribe on property changes without events

In my previous post I already explained how to implement INotifyPropertyChanged and INotifyPropertyChanging properly. At least one proper way. If you missed it, you might want to read it before this post, because this one based heavily on it. Let’s assume we want to use it as base class for our ViewModels and Models as well. In our really simple example we have a model called Person. This class is inherited from our base ViewModel (implemented in my prev. post), named  ViewModelBase.

public class Person : ViewModelBase
{
   private string _name;
   public string Name
   {
     get { return _name; }
     set { this.SetAndRaisePropertyChanged(() => this.Name, ref _name, value); }
   }

   private DateTime _birthDate;
   public string BirthDate
   {
     get { return _birthDate; }
     set { this.SetAndRaisePropertyChanged(() => this.BirthDate, ref _birthDate, value); }
   }
}
  
And a 'manager' class, our MainViewModel, also inherited from ViewModelBase. Person is bound to UI, user can create a new one, modify and existing one, it doesn't matter. Anything happens with our class Person we want to receive a notification in our MainViewModel. We can extend our base class with an OnPropertyChanged protected method, like OnPropertyChanging, but those are protected and we are in another class. We can use the PropertyChanged event, but there are two issues with it: first it is a hard reference, we need to manage subscriptions (subscribe and unsubscribe), otherwise the garbage collector (GC) won't dispose our class. Second issue: we'll get every PropertyChanged event only with a property name. So we had to check every time, whether this property is our property using 'magic' string. Furthermore we always had to keep it in our heads, when we rename or refactor a property we also need to change those compare strings. So, might be working, but not a good idea. Something similar would be nice:

this.SubscribeOnPropertyChanged(() => this.Person, p => p.Name, OnNameChanged);

Ok, let's see how to achieve this. Create a new method in our ViewModelBase class. First a class selector (which is actually a property or field of our MainViewModel), then a property selector for the selected class, and an Action to execute in case of any changes. The trick is: we pick the class and store it in our external Action reference collection.

private readonly List<PropertySubscription> OnChangedActions = 
    new List<PropertySubscription>();
 
protected void SubscribeOnPropertyChanged<TClass, TProperty>(
    Expression<Func<TClass>> classSelector, 
    Expression<Func<TClass, TProperty>> propertySelector, Action onPropertyChanged)
   where TClass : ViewModelBase
{
   var propertyName = ExtractPropertyName(propertySelector.Body as MemberExpression);
   var targetClass = classSelector.Compile();

   targetClass().OnChangedActions.Add( 
       new PropertySubscription(propertyName, onPropertyChanged));
}

PropertySubscription is really just a metaclass to store external subscriptions. You can use a simply KeyValuePair<string, Action> instead. To avoid redundant code, ExtractPropertyName must be extracted into two methods. Simply create a new method with the same name and copy the last few lines from the original one into the new one:

private string ExtractPropertyName(MemberExpression me)
{
   if ((me != null) && !string.IsNullOrEmpty(me.Member.Name))
   {
      return me.Member.Name;
   }

   throw new ArgumentException("Property could not be found.");
}

Ok, so we store it, but it does not do anything. We need to execute those actions whenever a property change occurs. Create a method which filters our collection on property name and execute every subscription action. Don't forget to store actions into a local variable inside the iteration to avoid "Access to modified closure".

private void ExecuteExternalSubcriptions(string propertyName)
{
   var actions = this.OnChangedActions.Where(s => s.PropertyName == propertyName);

   foreach (var action in actions)
   {
      var local = action;
      if (local.OnChangedAction != null)
      {
         local.OnChangedAction();
      }
   }
}

There is only one step left, add this method to our RaisePropertyChanged method. The new one is something like:

private void RaisePropertyChanged(string propertyName)
{
   var handler = this.PropertyChanged;
   if (handler != null)
   {
      handler(this, new PropertyChangedEventArgs(propertyName));
   }

   this.ExecuteExternalSubcriptions(propertyName);
}

From now on, every time a property has been changed, we also check external subscriptions and if we found any, execute them after property changed event. Thanks to this little extension in the ViewModelBase we can easily execute custom actions outside our class without strong references. Since we reference in a (probably) short living class to a long living class GC can dispose our object or class without problem. Furthermore developing is also much better and easier, working with expressions does not let us to compile with typo and we can refactor or rename our properties without to worry about magic strings in our code.

I can't believe you are still reading :) To appreciate your patient I give you one more thing: subscriptions to any property change. As you can see, our previous method allows us to subscribe to one particular property change. But sometimes we need all of them. Subscribing to all of them would be silly, so let's just add one more method, without property selector.

private readonly object dummy = new object();
 
protected void SubscribeOnPropertyChanged<TClass>(
    Expression<Func<TClass>> classSelector, Action onPropertyChanged)
   where TClass : ViewModelBase
{
   this.SubscribeOnPropertyChanged(classSelector, x => x.dummy, onPropertyChanged);
}

But what is 'dummy and why? Like I said before, I really hate to build logic on strings, but to keep our code simply and effective we want to use the same collection. But our collection stores PropertySubscriptions, where property name is obligatory. Best way is to create a private field and use it as property in our subscription. What we need it filter out this property before we iterate through our actions. So, let's change our ExecuteExternalSubscriptions method:

private void ExecuteExternalSubscriptions(string propertyName)
{
   var actions = this.OnChangedActions.Where(
       s => s.PropertyName == propertyName || 
       s.PropertyName == this.ExtractPropertyName(() => this.dummy));

   foreach (var action in actions)
   {
      var local = action;
      if (local.OnChangedAction != null)
      {
         local.OnChangedAction();
      }
   }
}

And how it works? Here is a tiny code snippet:

public class MainWindowViewModel : ViewModelBase
{      
  private Person Person = new Person();

  public MainWindowViewModel()
  {
   // to subscribe on Person's name changes
   this.SubscribeOnPropertyChanged(() => this.Person, p => p.Name, OnNameChanged);

   // to subscribe on any property changes in Person clsas
   this.SubscribeOnPropertyChanged(() => this.Person, OnPersonChanged);
  }

  private void OnNameChanged()
  { }

  private void OnPersonChanged()
  { }
}



No comments:

Post a Comment