David
David

Reputation: 1976

Easily propagating notifications up to the UI in WPF

So I have seen some responses to similar questions as this, but I was wondering if a certain paradigm that I am thinking of is even possible in C#. First, I'll lay out the issue:

I have a MVVM application that I am developing in C#. The model has properties that change, and when a single property changes in the model, it often times affects multiple properties in the view-model. So the view-model listens for changes on the model. And the view listens for changes on the view-model.

In my view-model, I end up getting some code that looks like this:

private void OnModelPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
    string prop_name = e.PropertyName;
    if (prop_name.Equals("some_property_on_the_model"))
    {
        NotifyPropertyChanged("some_property_on_the_view_model");
        NotifyPropertyChanged("some_property_on_the_view_model");
        NotifyPropertyChanged("some_property_on_the_view_model");
        NotifyPropertyChanged("some_property_on_the_view_model");
        NotifyPropertyChanged("some_property_on_the_view_model");
    }
    else if (...)
    {
        ... etc ...
    }
}

This gets annoying because it just seems messy. And if I forget to edit this function after adding a new property to the view-model then it can easily lead to bugs.

So here is what I would like to do, but I don't know if this is possible. So I would like one of you to help me understand if it is possible or not.

It would be really cool if I could use C#'s "attributes" feature to take care of the property changed propagation.

So maybe something like this:

[ListenToModelProperty("some_property_on_the_model")]
[OnPropertyChanged("MyButtonVisibility")]
public Visibility MyButtonVisibility
{
    get
    {
        if (model.some_property_on_the_model == true)
        {
            return Visibility.Visible;
        }
        else
        {
            return Visibility.Hidden;
        }
    }
}

private void OnModelPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
    prop_name = e.PropertyName;

    foreach (var property in view_model)
    {
        var attributes = property.GetCustomAttributes(typeof(ListenToModelPropertyAttribute));
        var descriptions = attributes.Select(x => x.Description);
        if (descriptions.Contains(prop_name))
        {
            notification_to_make = property.GetCustomAttributes(typeof(OnPropertyChangedAttribute));
            string notification_string = notification_to_make[0].Description;
            NotifyPropertyChanged(notification_string);
        }
    }
}

Please note that the above code is not meant to be real code. It will definitely not compile and will not work. But I would like to see if something like the above is possible in C#. Is it possible to do something like this using attributes? Or is there a library out there that makes something like this possible?

Upvotes: 2

Views: 96

Answers (1)

David
David

Reputation: 1976

I have figured out how to do it! It is fairly simple. I will post the relevant code here, and those who are interested can find all the code at this github repository that I just made: https://github.com/davepruitt/model-subscribe

First, I created a custom attribute class. It is a simple class that takes an array of strings as a parameter to its constructor. This allows you to listen to multiple properties on the model for changes. It looks like this:

using System;
using System.Collections.Generic;

namespace TestPropagationOfPropertyChanges
{
    [AttributeUsage(AttributeTargets.All)]
    public class ListenForModelPropertyChangedAttribute : System.Attribute
    {
        public List<string> ModelPropertyNames = new List<string>();

        public ListenForModelPropertyChangedAttribute (string [] propertyNames)
        {
            ModelPropertyNames.AddRange (propertyNames);
        }
    }
}

I then created my model. For simplicity's sake, it only contains two properties. They are strings that store a "first name" and a "last name":

using System;
using System.ComponentModel;

namespace TestPropagationOfPropertyChanges
{
    public class Model : NotifyPropertyChangedObject
    {
        #region Constructors

        public Model ()
        {
        }

        #endregion

        #region Private data members

        private string _first = string.Empty;
        private string _last = string.Empty;

        #endregion

        #region Public properties

        public string FirstName 
        {
            get
            {
                return _first;
            }
            set
            {
                _first = value;
                NotifyPropertyChanged ("FirstName");
            }
        }

        public string LastName
        {
            get
            {
                return _last;
            }
            set
            {
                _last = value;
                NotifyPropertyChanged ("LastName");
            }
        }

        #endregion
    }
}   

The view-model, in this case, has a "full name" property. So it wants to listen to any changes that happen to the first or last name on the model, and then react to changes on either of those. I realize this isn't the best "real world" scenario in which this kind of system would be used, but it does help illustrate the concept. The first part of my view-model is below:

using System;

namespace TestPropagationOfPropertyChanges
{
    public class ViewModel : NotifyPropertyChangedObject
    {
        #region Private data members

        //This is public for testing purposes
        public Model _model = new Model();

        #endregion

        #region Constructors

        public ViewModel ()
        {
            _model.PropertyChanged += ReactToModelPropertyChanged;
        }

        #endregion

        #region Properties

        [ListenForModelPropertyChangedAttribute(new string [] {"FirstName", "LastName"})]
        public string FullName 
        {
            get
            {
                return _model.FirstName + _model.LastName;
            }
        }

Finally, the view-model finishes with the method that reacts to changes on the model. Normally, in a large and complex application, this method could contain a large if-else statement with lots of calls to NotifyPropertyChanged. Instead, we now just iterate through the properties of the view-model and see which ones subscribe to the model's property that was changed. See below:

void ReactToModelPropertyChanged (object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
    //Get the name of the property that was changed on the model
    string model_property_changed = e.PropertyName;

    //Get a System.Type object representing the current view-model object
    System.Type t = typeof(ViewModel);

    //Retrieve all property info for the view-model
    var property_info = t.GetProperties();

    //Iterate through each property
    foreach (var property in property_info) 
    {
        //Get the custom attributes defined for this property
        var attributes = property.GetCustomAttributes (false);
        foreach (var attribute in attributes) 
        {
            //If the property is listening for changes on the model
            var a = attribute as ListenForModelPropertyChangedAttribute;
            if (a != null) 
            {
                //If the property that was changed on the model matches the name
                //that this view-model property is listening for...
                if (a.ModelPropertyNames.Contains(model_property_changed))
                {
                    //Notify the UI that the view-model property has been changed
                    NotifyPropertyChanged (property.Name);
                }
            }
        }
    }
}

Overall, it works excellently, and is exactly what I needed. This code can easily be expanded upon to be even more functional for those interested.

Upvotes: 1

Related Questions