Creating a Model Base – Part III: Subscribing can make all the difference

In this post I am going to introduce the idea of subscribing to property change events in our Model Base.

Note: Please read Part I before continuing here.
Note: Please read Part II before continuing here.

The purpose of this is to cover a potentially annoying situation:

Scenario: Let’s say you have a ViewModel, and inside that you have another ViewModel acting as a property like this:


    private MyCustomObject _customObjectInstance;

    public MyCustomObject CustomObjectInstance
    {
        get
        {
            return _customObjectInstance;
        }

        set
        {
            SetValue("CustomObjectInstance", ref _customObjectInstance, ref value);
        }
    }

Okay, now you want your UI bound to the current ViewModel to change every time CustomObjectInstance.MeterReading changes.

Q: (Panic moment) So, how do I do that without breaking my wonderful abstraction?
A: Implementing an ability to Subscribe to a nested property. All it takes is a little reflection and patience.

The idea is that we tell that property to fire a specific Action whenever it is changed in our ViewModelBase.

The first thing we will need to add is a private list of subscriptions:


/// <summary>
/// Subscription list.
/// </summary>
private readonly List<Tuple<string, Action<object>>> _subscriptions;

Instead of creating yet another custom object, I decided to use a Tuple because they are convenient.

  • The string value will serve as the name of the property you want to subscribe to.
  • The Action will serve as the Action you wish to call when the property is changed. The object parameter allows you to return an object if needed.

Next, we make sure to create a new instance of _subscriptions in the Constructor:


/// <summary>
/// Default constructor.
/// </summary>
protected ViewModelBase()
{
    _subscriptions = new List<Tuple<string, Action<object>>>();
}

Now we will expose a Subscribe method.

        /// <summary>
        /// Subscribes an action to a specific property that will be called
        /// during that property's OnPropertyChanged event.
        /// </summary>
        /// <param name="propertyName"></param>
        /// <param name="onChange"></param>
        public void Subscribe(string propertyName, Action<object> onChange)
        {
            // Verify property
            var propInfo = this.GetType().GetProperty(propertyName);

            // If valid, add to subscription pool.
            if (propInfo != null)
            {
                _subscriptions.Add(
                    new Tuple<string, Action<object>>(propertyName, onChange));
            }
            else
            {
                // Invalid property name provided.
                throw new Exception(
                    "Property "" + propertyName + "" could not be " +
                    "found for type "" + this.GetType().ToString() + ""!");
            }
        }

This idea here is fairly simple:

  • We pass our property name and intended Action that will fire OnPropertyChanged.
  • If the property name is not valid, we will throw an exception to ensure we didn’t pass invalid information into our Subscribe method.

Q: Alright, now we have a nice Tuple-list full of property names and Actions. Now what?
A: Glad you asked. Here comes the hard part:


        /// <summary>
        /// Processes existing subscriptions matching the provided property name.
        /// </summary>
        /// <param name="propertyName"></param>
        private void ProcessSubscriptions(string propertyName)
        {
            // Get matching subscriptions
            var subList =
                (from p in _subscriptions
                 where p.Item1 == propertyName
                 select p).ToList();

            // Check if any matches were found.
            if (subList.Any())
            {
                // Process actions
                foreach (var sub in subList)
                {
                    // Evaluate action
                    var onChange = sub.Item2;
                    if (onChange != null)
                    {
                        // Get property value by name
                        var propInfo = this.GetType().GetProperty(propertyName);
                        var propValue = propInfo.GetValue(this, null);

                        // Invoke action
                        onChange(propValue);
                    }
                }
            }
        }

ProcessSubscriptions does the following:

  • Looks up a specific property by name in _subscriptions and gets a list of all entries that are registered for that property.
  • Loop: If a specific entry has a valid Action assigned to it, it will use reflection to get that property value and pass it to the action (as out object parameter mentioned earlier).

So, the last piece is making sure ProcessSubscriptions is fired when the property has been changed. And that is as easy as augmenting our trusted OnPropertyChanged method:


        /// <summary>
        /// Calls the PropertyChanged event
        /// </summary>
        /// <param name="propertyName"></param>
        /// <param name="onChanged"></param>
        protected void OnPropertyChanged(string propertyName, Action onChanged = null)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                // Call handler
                handler(this, new PropertyChangedEventArgs(propertyName));

                // Subscriptions
                ProcessSubscriptions(propertyName);

                // On changed
                if (onChanged != null)
                {
                    onChanged();
                }
            }
        }

No worries if your specific property being changed is without entries. ProcessSubscriptions only acts on what is present in _subscriptions, so no entries means it just moves on.

Here is how you would use it in your parent ViewModel:


CustomObjectInstance.Subscribe("MeterReading", obj => MyActionThatDoesStuff());

Now, every time CustomObjectInstance.MeterReading is updated, the MyActionThatDoesStuff Action will be called allowing you to always have the latest values from your nested properties.

Here is our new ViewModelBase in it’s entirety:


    /// <summary>
    /// Extends the INotifyPropertyChanged interface to the class properties.
    /// </summary>
    public abstract class ViewModelBase : INotifyPropertyChanged
    {
        #region Members

        /// <summary>
        /// Subscription list.
        /// </summary>
        private readonly List<Tuple<string, Action<object>>> _subscriptions;

        #endregion

        #region Constructors

        /// <summary>
        /// Default constructor.
        /// </summary>
        protected ViewModelBase()
        {
            _subscriptions = new List<Tuple<string, Action<object>>>();
        }

        #endregion

        #region Methods

        /// <summary>
        /// To be used within the "set" accessor in each property.
        /// This invokes the OnPropertyChanged method.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="name"></param>
        /// <param name="value"></param>
        /// <param name="newValue"></param>
        /// <param name="onChanged"></param>
        protected void SetValue<T>(string name, ref T value, ref  T newValue,
            Action onChanged = null)
        {
            if (newValue != null)
            {
                if (!newValue.Equals(value))
                {
                    value = newValue;
                    OnPropertyChanged(name, onChanged);
                }
            }
            else
            {
                value = default(T);
            }
        }

        #endregion

        #region INotifyPropertyChanged

        /// <summary>
        /// The PropertyChanged event handler.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Calls the PropertyChanged event
        /// </summary>
        /// <param name="propertyName"></param>
        /// <param name="onChanged"></param>
        protected void OnPropertyChanged(string propertyName, Action onChanged = null)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                // Call handler
                handler(this, new PropertyChangedEventArgs(propertyName));

                // Subscriptions
                ProcessSubscriptions(propertyName);

                // On changed
                if (onChanged != null)
                {
                    onChanged();
                }
            }
        }

        /// <summary>
        /// Subscribes an action to a specific property that will be called
        /// during that property's OnPropertyChanged event.
        /// </summary>
        /// <param name="propertyName"></param>
        /// <param name="onChange"></param>
        public void Subscribe(string propertyName, Action<object> onChange)
        {
            // Verify property
            var propInfo = this.GetType().GetProperty(propertyName);

            // If valid, add to subscription pool.
            if (propInfo != null)
            {
                _subscriptions.Add(
                    new Tuple<string, Action<object>>(propertyName, onChange));
            }
            else
            {
                // Invalid property name provided.
                throw new Exception(
                    "Property "" + propertyName + "" could not be " +
                    "found for type "" + this.GetType().ToString() + ""!");
            }
        }
        
        /// <summary>
        /// Clears the subscriptions.
        /// </summary>
        public void ClearSubscriptions()
        {
            _subscriptions.Clear();
        }

        /// <summary>
        /// Processes existing subscriptions matching the provided property name.
        /// </summary>
        /// <param name="propertyName"></param>
        private void ProcessSubscriptions(string propertyName)
        {
            // Get matching subscriptions
            var subList =
                (from p in _subscriptions
                 where p.Item1 == propertyName
                 select p).ToList();

            // Check if any matches were found.
            if (subList.Any())
            {
                // Process actions
                foreach (var sub in subList)
                {
                    // Evaluate action
                    var onChange = sub.Item2;
                    if (onChange != null)
                    {
                        // Get property value by name
                        var propInfo = this.GetType().GetProperty(propertyName);
                        var propValue = propInfo.GetValue(this, null);

                        // Invoke action
                        onChange(propValue);
                    }
                }
            }
        }

        #endregion
    }

I hope this has been helpful for you.

 
Comments

Trackbacks for this post

Leave a Reply