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.
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:
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:
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:
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.
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:
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:
- Call
RaiseProperty
method for a property.
- Get all the dependent properties for that property.
- Call
RaiseProperty
on them as well.
- 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.