Click here to Skip to main content
15,867,835 members
Articles / Desktop Programming / WPF

WPF: The calculated property dependency problem

Rate me:
Please Sign up or sign in to vote.
4.18/5 (6 votes)
30 Apr 2012CPOL4 min read 43.3K   13   14
In this article I'm going to speak about another nuance of the MVVM pattern; namely binding to calculated properties and updating the view when calculations change. I'm also crazy enough to propose a solution.

Introduction

Model-View-ViewModel (MVVM) is one of the new patterns (well maybe old now!) used to separate UI layers from model layers in applications. It's ubiquitously used in WPF and takes advantage of the extensive data binding support offered. While it's a wonderful separation pattern it certainly does have it drawbacks. For example, capturing non routed events on the view model. Developers have found numerous workarounds for issues like this including using attached properties and event-to-command wrappers just to name two of them.  In this article I'm going to speak about another nuance of the MVVM pattern; namely binding to calculated properties and updating the view when calculations change. I'm also crazy enough to propose a solution.

Background 

Step 1: Let's look into the abyss shall we!  

Below you will find a very simple ViewModel. Let's assume that a View is bound to it and the RaisePropertyChanged method is implemented correctly. Let's also assume that a WPF TextBlock on the View is bounded to the Name property.  

C#
public class ViewModel: INotifyPropertyChanged
{
    private string name = String.Empty;
    private DateTime birthDate;
        
    public string Name
    {
        get
        {
           return name;
        }

        set
        {
            name = value;
            RaisePropertyChanged("Name");
        }
    }
}

If I now change the Name property in code, the TextBlock's Text will change since I called the RaisePropertyChanged method which raises the PropertyChanged event. Indeed this is the ideal case and is how view model property binding and notification works. 

Step 2: Let's climb down into the abyss

Let's modify our view model as follows:

C#
public class ViewModel: INotifyPropertyChanged
{
    private string name = String.Empty;
    
    public string Name
    {
        get
        {
           return name;
        }

        set
        {
            name = value;
            RaisePropertyChanged("Name");
        }
    }

    public DateTime BirthDate
    {
        get
        {
           return birthDate;
        }

        set
        {
            birthDate= value;
            RaisePropertyChanged("BirthDate");
        }
    }


    public int Age
    {
        get
        {
           return DateTime.Today.Year-BirthDate.Year;
        }    
    }
}

Let's assume that in the second ViewModel a TextBlock on the View is bounded to Age, a calculated property. Careful readers will notice that when BirthDate is updated, the TextBlock on the view bound to Age will not update. This is because the view does not know that Age has changed (we never told it). Age also does not have a backing property! A solution to this issue is deceptively simple:

C#
public class ViewModel: INotifyPropertyChanged
{
    private string name = String.Empty;
    
    public string Name
    {
        get
        {
           return name;
        }

        set
        {
            name = value;
            RaisePropertyChanged("Name");
        }
    }

    public DateTime BirthDate
    {
        get
        {
           return birthDate;
        }

        set
        {
            birthDate= value;
            RaisePropertyChanged("BirthDate");
            RaisePropertyChanged("Age");
        }
    }

    public int Age
    {
        get
        {
           return DateTime.Today.Year-BirthDate.Year;
        }    
    }

}

By calling RaisePropertyChanged("Age") in the setter of BirthDate the view will query Age and hence retrieve the correct value.  

While this solution works in this extremely simple case it will soon become a nightmare when we have numerous calculated properties with no setters. Calculated properties may even depend on each other! This will result in calling the RaisePropertyChanged method all over your code just to get the view and the view model to sync. Suddenly we are in the abyss and it's dark.

Step 3: Staring into the abyss (and not being afraid)

At this point the abyss is telling us to accept our fate (the solution above) and move on. But that's not who we are. We are warriors with sharp swords (c sharp, that is).

We need a solution to this issue which we can reuse across different view models. The last thing we need is to litter our code with property changed notification calls. What we need is a way to tell our view model that if a certain property changes then we should also raise a property changed event of any property that depends on it. We need a kind of parent-child property thing-y.

Step 4: Fighting the abyss (does that even make sense?)

Let's modify our view model as follows: 

C#
public class ViewModel: INotifyPropertyChanged
{
    private string name = String.Empty;
    
    public string Name
    {
        get
        {
           return name;
        }

        set
        {
            name = value;
            RaisePropertyChanged("Name");
        }
    }

    [DependentProperties("Age")]
    public DateTime BirthDate
    {
        get
        {
           return birthDate;
        }

        set
        {
            birthDate= value;
            RaisePropertyChanged("BirthDate");
            RaisePropertyChanged("Age");
        }
    }

    public int Age
    {
        get
        {
           return DateTime.Today.Year-BirthDate.Year;
        }    
    }
}

Notice that I have annotated the Birthdate property with an attribute called DependentProperties. I have also removed the RaisePropertyChanged method call from the setter of BirthDate.

Using the mechanism we can say that whenever BirthDate is updated the property changed event of Age should also be raised. The view will be notified that Age has 'changed' and query it. The view and the view model will be in sync.

Benefits  

  • A higher level of abstraction when dealing with calculated properties
  • Less code to write
  • Less code to maintain 
  • No 'tracking down' of which properties depend on each other.
  • A solution to the calculated property dependency problem in WPF.

Step 5: Punching the abyss in the gut (aka the implementation)!

Let's describe the DependentProperties attribute class.

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

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

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

It derives from Sytem.Attribute and has a simple Properties field. This field holds all the dependent properties (properties that need to be re-queried when the annotated property value changes).

Now let's look at who consumes the DependentProperties attribute.

The magic is in the RaiseProperty procedure:

C#
public class ViewModelBase: INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void RaiseProperty(string propertyName, List<string> calledProperties = null)
    {
        RaisePropertyChanged(propertyName);

        if (calledProperties == null)
        {
            calledProperties = new List<string>();
        }
     
        calledProperties.Add(propertyName);
      
        PropertyInfo pInfo =  GetType().GetProperty(propertyName);

       if (pInfo != null)
       {
           foreach (DependentPropertiesAttribute ca in 
             pInfo.GetCustomAttributes(false).OfType<dependentpropertiesattribute>())
           {
               if (ca.Properties != null)
               {
                   foreach (string prop in ca.Properties)
                   {
                       if (prop != propertyName && !calledProperties.Contains(prop))
                       {
                           RaiseProperty(prop, calledProperties);
                       }
                   }
               }
           }
       }
    }

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

Step 5: Making the abyss beg for mercy

The algorithm, although it looks complex, it quite simple:

  1. Call RaiseProperty method for a property.
  2. Get all the dependent properties for that property.
  3. Call RaiseProperty on them as well.
  4. Avoid stack overflow exceptions from properties who depend on each other either directly or indirectly by keeping a list of called properties.

Step 6: Climbing out from the abyss

At the this you survived the abyss. You are much stronger than it!

Points of Interest 

Although I feel that this solution is elegant it still relies on reflection to get the dependent properties. Also a list of called properties must be maintained per top level RaiseProperty call. This solution can be used when performance is not an issue. I believe the small performance hit due to reflection far outweighs the complexity of dealing with dependant and calculated properties. You have the option of also using a hybrid approach. For example, for calculated and dependent properties call the RaiseProperty method. For properties that do not need this just call the general RaisePropertyChanged event.

The solution can also be optimized to cache the dependent properties whereby avoiding the reflection call.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Finance Industry
United States United States
Currently pursuing 'Programming Nirvana' (The ineffable ultimate in which one has attained disinterested wisdom and compassion as it relates to programming)

Respected Technologies
1. Confusor (https://confuser.codeplex.com/)
2. Power Threading (http://www.wintellect.com/Resources/visit-the-power-threading-library)
3. EDI Parsers (http://www.rdpcrystal.com)


Acknowledgements:

Microsoft Certified Technologist for WPF and .Net 3.5 (MCTS)
Microsoft Certified Technologist for WCF and .Net 3.5 (MCTS)
Microsoft Certified Application Developer for .Net (MCAD)
Microsoft Certified Systems Engineer (MCSE)
Microsoft Certified Professional (MCP)

Sun Certified Developer for Java 2 Platform (SCD)
Sun Certified Programmer for Java 2 Platform (SCP)
Sun Certified Web Component Developer (SCWCD)

CompTIA A+ Certified Professional

Registered Business School Teacher for Computer Programming and Computer Applications (2004)
(University of the State of New York Education Department)

Graduated from University At Stony Brook

Comments and Discussions

 
QuestionBug Fix: wrong method called Pin
Member 1474272312-Feb-20 4:27
Member 1474272312-Feb-20 4:27 
QuestionWhy not use PropertyChanged? Pin
tc6924-Jan-18 12:56
tc6924-Jan-18 12:56 
QuestionDependency Attribute should be reversed Pin
Member 1068684323-Mar-17 7:39
Member 1068684323-Mar-17 7:39 
Questiongetting rid of the attributes as well using a runtime dependency graph and code injection - an idea? Pin
saephoed8-Jan-14 7:58
saephoed8-Jan-14 7:58 
Hello FatCatProgrammer

I just read your article and, while reading, began to wonder if it would be possible to approach the RaisePropertyChanged like the following (I'm not an expert in c#, so maybe the terminology is poor):

- at prog. start/assm load/just before creating first instance of a class/...
- use MethodInfo, GetILAsByteArray and stuff to identify properties without the event call and such with
- put all these in a graph (maybe using QuickGraph) and evaluate appropriately for every prop
- modify the found set of props using code-injection (explicitely add the call or if that's possible, add the attributes)
--> an exposable transformation function could be used to derive the event names from the props and their context so that the user still had some sort of control

What do you think?

Greetings
QuestionClarification Request Pin
Joseph S. Keller27-Sep-13 6:31
Joseph S. Keller27-Sep-13 6:31 
QuestionI like were u going Pin
Sk8tz30-Apr-12 7:22
professionalSk8tz30-Apr-12 7:22 
AnswerRe: I like were u going Pin
FatCatProgrammer1-May-12 7:32
FatCatProgrammer1-May-12 7:32 
QuestionAlmost perfect... Pin
Paulo Zemek28-Apr-12 7:25
mvaPaulo Zemek28-Apr-12 7:25 
GeneralRe: Almost perfect... - Agreed Pin
crashedapp28-Apr-12 15:02
crashedapp28-Apr-12 15:02 
AnswerRe: Almost perfect... Pin
FatCatProgrammer28-Apr-12 17:26
FatCatProgrammer28-Apr-12 17:26 
GeneralRe: Almost perfect... Pin
Paulo Zemek29-Apr-12 3:13
mvaPaulo Zemek29-Apr-12 3:13 
GeneralRe: Almost perfect... Pin
FatCatProgrammer29-Apr-12 8:47
FatCatProgrammer29-Apr-12 8:47 
GeneralRe: Almost perfect... Pin
yotta1-Jul-15 16:37
professionalyotta1-Jul-15 16:37 
AnswerDoing the Reverse Attribute Pin
thesly2-Apr-17 1:25
thesly2-Apr-17 1:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.