A nicer way to handle dependent values on PropertyChanged

Here’s a contrived example of how PropertyChanged can get difficult. The property Total depends on Values A and B, and if any of them change, Total has to be read again.

public int Total
{
  get { return A + B; }
}

public int A
{
  get { return m_A; }
  set { m_A = value; RaisePropertyChanged("A"); RaisePropertyChanged("Total"); }
}

public int B
{
  get { return m_B: }
  set { m_B = value; RaisePropertyChanged("B"); RaisePropertyChanged("Total"); }
}

We can solve this by using a helper attribute, so that the new code looks like this. Notice the increased clarity and ease of adding more dependent variables:


[DependsOn("A")]
[DependsOn("B")]
public int Total
{
  get { return A + B; }
}

public int A
{
  get { return m_A; }
  set { m_A = value; RaisePropertyChanged("A"); }
}

public int B
{
  get { return m_B: }
  set { m_B = value; RaisePropertyChanged("B"); }
}

Here’s the supporting code

        internal Dictionary<string, IEnumerable<string>> m_DependentAttributes = new Dictionary<string, IEnumerable<string>>();
        private void SetupDependentAttributes()
        {
            foreach (var property in this.GetType().GetProperties())
            {
                string propertyName = property.Name;

                DependsOnAttribute[] attrs = (DependsOnAttribute[])property.GetCustomAttributes(typeof(DependsOnAttribute), true);
                foreach (var dependOnAttr in attrs)
                {
                    foreach (string dependsOnPropertyName in dependOnAttr.DependsOnPropertyNames)
                    {
                        if (!m_DependentAttributes.ContainsKey(dependsOnPropertyName))
                            m_DependentAttributes[dependsOnPropertyName] = new List<string>();
                        (m_DependentAttributes[dependsOnPropertyName] as List<string>).Add(propertyName);
                    }
                }

                // If a property is ObservableCollection, then CollectionChanged should also raise PropertyChanged.
                object obs = property.GetGetMethod().Invoke(this, null);
                if (obs != null && obs is System.Collections.Specialized.INotifyCollectionChanged)
                {
                    EventInfo collectionChanged = obs.GetType().GetEvent("CollectionChanged");
                    if (collectionChanged != null)
                    {
                        (obs as System.Collections.Specialized.INotifyCollectionChanged).CollectionChanged +=
                            (s, e) => { RaisePropertyChanged(propertyName); };
                    }
                }
            }
        }

        public void RaisePropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                if (m_DependentAttributes.ContainsKey(propertyName))
                    foreach (var dependentPropertyName in m_DependentAttributes[propertyName])
                    {
                        RaisePropertyChanged(dependentPropertyName);
                    }
            }
        }

    [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
    sealed class DependsOnAttribute : Attribute
    {
        // See the attribute guidelines at
        //  http://go.microsoft.com/fwlink/?LinkId=85236
        readonly string[] dependsOnPropertyNames;

        // This is a positional argument
        public DependsOnAttribute(params string[] dependsOnPropertyNames)
        {
            this.dependsOnPropertyNames = dependsOnPropertyNames;
        }

        public string[] DependsOnPropertyNames
        {
            get { return dependsOnPropertyNames; }
        }

        // This is a named argument
        public int NamedInt { get; set; }
    }

About this entry