Avi
Avi

Reputation: 16176

Dependecy properties that depend on other properties

Class C implements INotifyPropertyChanged.

Assume the C has Length, Width and Area propreties, where Area = Length * Width. A change in either might cause a change in area. All three are bound, i.e. the UI expects all three to notify of changes in their values.

When either Length or Width change, their setters call NotifyPropertyChanged.

How should I treat the calculated Area property? Currently the pattern I can think of is detecting in NotifyPropertyChanged whether the changed property is either Length or Width and, if such is the case, initiate an addional PropertyChanged notification for Area. This, however, requires that I maintain inside NotifyPropertyChanged the dependencies graph, which I feel is an anti-pattern.

So, my question is: How should I code dependency properties that depend on other dependency properties?

edit: People here suggested that Length and Width also call NotifyPropertyChanged for Area. Again, I think this is an anti-pattern. A property (IMHO) shouldn't be aware of who depends on it, as shouldn't NotifyPropertyChanged. Only the property should be aware of who it depends on.

Upvotes: 2

Views: 4384

Answers (5)

maxence51
maxence51

Reputation: 1054

Here is a possible implementation of an attribute:

public class DependentPropertiesAttribute : Attribute
{
    private readonly string[] properties;

    public DependentPropertiesAttribute(params string[] dp)
    {
        properties = dp;
    }

    public string[] Properties
    {
        get
        {
            return properties;
        }
    }
}

Then in the Base View Model, we handle the mechanism of calling property dependencies:

public class ViewModelBase : INotifyPropertyChanged
{
    public ViewModelBase()
    {
        DetectPropertiesDependencies();
    }

    private readonly Dictionary<string, List<string>> _dependencies = new Dictionary<string, List<string>>();

    private void DetectPropertiesDependencies()
    {
        var propertyInfoWithDependencies = GetType().GetProperties().Where(
        prop => Attribute.IsDefined(prop, typeof(DependentPropertiesAttribute))).ToArray();

        foreach (PropertyInfo propertyInfo in propertyInfoWithDependencies)
        {
            var ca = propertyInfo.GetCustomAttributes(false).OfType<DependentPropertiesAttribute>().Single();
            if (ca.Properties != null)
            {
                foreach (string prop in ca.Properties)
                {
                    if (!_dependencies.ContainsKey(prop))
                    {
                        _dependencies.Add(prop, new List<string>());
                    }

                    _dependencies[prop].Add(propertyInfo.Name);
                }
            }
        }
    }

    protected void OnPropertyChanged(params Expression<Func<object>>[] expressions)
    {
        expressions.Select(expr => ReflectionHelper.GetPropertyName(expr)).ToList().ForEach(p => {
            RaisePropertyChanged(p);
            RaiseDependentProperties(p, new List<string>() { p });
        });

    }

    public event PropertyChangedEventHandler PropertyChanged = delegate { };

    protected virtual void RaisePropertyChanged(string propertyName)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    protected void RaiseDependentProperties(string propertyName, List<string> calledProperties = null)
    {
        if (!_dependencies.Any() || !_dependencies.ContainsKey(propertyName))
            return;

        if (calledProperties == null)
            calledProperties = new List<string>();

        List<string> dependentProperties = _dependencies[propertyName];

        foreach (var dependentProperty in dependentProperties)
        {
            if (!calledProperties.Contains(dependentProperty))
            {
                RaisePropertyChanged(dependentProperty);
                RaiseDependentProperties(dependentProperty, calledProperties);
            }
        }
    }
}

Finally we define dependencies in our ViewModel

[DependentProperties("Prop1", "Prop2")]
public bool SomeCalculatedProperty
{
    get
    {
        return Prop1 + Prop2;
    }
}

Upvotes: 0

Bj&#246;rn
Bj&#246;rn

Reputation: 3418

Here is an article describing how to create a custom attribute that automatically calls PropertyChanged for properties depending on another property: http://www.redmountainsw.com/wordpress/2012/01/17/a-nicer-way-to-handle-dependent-values-on-propertychanged/

The code will look like this:

[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"); }
}

I haven't tried it myself but I like the idea

Upvotes: 3

Peter Porfy
Peter Porfy

Reputation: 9030

Then you should raise twice, in Length and Width property setters. One for the actual property and one for the Area property.

for example:

private int _width;
public int Width
{
    get { return _width; }
    set
    {
        if (_width == value) return;
        _width = value;
        NotifyPropertyChanged("Width");
        NotifyPropertyChanged("Area");
    }
}

People here suggested that Length and Width also call NotifyPropertyChanged for Area. Again, I think this is an anti-pattern. A property (IMHO) shouldn't be aware of who depends on it, as shouldn't NotifyPropertyChanged. Only the property should be aware of who it depends on.

This is not an anti-pattern. Actually, your data encapsulated inside this class, so this class knows when and what changed. You shouldn't know outside of this class that Area depends on Width and Length. So the most logical place to notify listeners about Area is the Width and Length setter.

A property (IMHO) shouldn't be aware of who depends on it, as shouldn't NotifyPropertyChanged.

It does not break encapsulation, because you are in the same class, in the same data structure.

An extra information is that knockout.js (a javascript mvvm library) has a concept which accessing this problem: Computed Observables. So I believe this is absolutely acceptable.

Upvotes: 0

Avi
Avi

Reputation: 16176

This issue kept on bugging me, so I re-opened it.

First, I'd like to appologize for anyone taking my "anti-pattern" comment personally. The solutions offered here were, indeed, how-it's-done in WPF. However, still, IMHO they're bad practices caused, deficiencies in ther framework.

My claim is that the information hiding guide dictates that when B depeneds on A, A should not be aware of B. For exmaple, when B derives from A, A should not have code saying: "If my runtime type is really a B, then do this and that". Simiarily, when B uses A, A should not have code saying: "If the object calling this method is a B, then ..."

So it follows that if property B depends on property A, A shouldn't be the one who's responsible to alert B directly.

Conversely, maintaining (as I currently do) the dependencies graph inside NotifyPropertyChanged is also an anti-pattern. That method should be lightweight and do what it name states, not maintain dependency relationships between properties.

So, I think the solution needed is through aspect oriented programming: Peroperty B should use an "I-depend-on(Property A)" attribute, and some code-rewriter should create the dependency graph and modify NotifyPropertyChanged transparently.

Today, I'm a single programmer working on a single product, so I can't justify dvelving with this any more, but this, I feel, is the correct solution.

Upvotes: 4

Martin Liversage
Martin Liversage

Reputation: 106826

When the Length or Width properties are changed you fire PropertyChanged for Area in addition to firing it for either Length or Width.

Here is a very simple implementation based on backing fields and the method OnPropertyChanged to fire the PropertyChanged event:

public Double Length {
  get { return this.length; }
  set {
    this.length = value;
    OnPropertyChanged("Length");
    OnPropertyChanged("Area");
  }
}

public Double Width {
  get { return this.width; }
  set {
    this.width = value;
    OnPropertyChanged("Width");
    OnPropertyChanged("Area");
  }
}

public Double Area {
  get { return this.length*this.width; }
}

Doing it like this is certainly not an anti-pattern. That is exactly the pattern for doing it. You as the implementer of the class knows that when Length is changed then Area is also changed and you encode it by raising the appropriate event.

Upvotes: 0

Related Questions