Click here to Skip to main content
16,020,628 members
Articles / Desktop Programming / WPF

INotifyPropertyChanged Propagator

Rate me:
Please Sign up or sign in to vote.
4.80/5 (13 votes)
9 Jan 2017CPOL6 min read 41.4K   94   28   21
Free and elegant way to propagate changes of dependent properties when dependency changed (including dynamically changeable nested properties)

Attention

Instead of this code I strongly recommend to use Stephen's Cleary Nito.CalculatedProperties library: https://github.com/StephenCleary/CalculatedProperties.  

Turned out Mr Cleary developed and published his library about the same time as I created my code so I wasn't aware of it. It is far more flexible and elegant than my solution, it's extremely simple and useful yet not so many people know about it.

Introduction

Here is the free and elegant way to propagate changes of dependent properties when dependency changed (including dynamically changeable nested properties).

Background

Most popular and naive implementation of INotifyPropertyChanged looks something like this:

C#
public decimal TotalPrice
{
    get { return UnitPrice * Qty; }
}
    public int Qty
{
    get { return _qty; }
    set
    {
        _qty = value;
        RaisePropertyChanged(() => Qty);
        RaisePropertyChanged(() => TotalPrice);
    }
}
public decimal UnitPrice
{
    get { return _unitPrice; }
    set
    {
        _unitPrice = value;
        RaisePropertyChanged(() => UnitPrice);
        RaisePropertyChanged(() => TotalPrice);
    }
}

It has one well known design flaw: independent properties know their dependencies (Qty and UnitPrice in this example know that TotalPrice depends on them).
It's not a big problem for small apps however in complex view model hierarchies, bookkeeping code grows fast and things get out of control pretty quickly.

Wouldn't it be cool to have something like Microsoft Excel sheet instead? You just type a formula depending on other cells while those cells have no idea what depends on them.

Existing Solutions

This problem has been recognized by the community and number of possible solutions already exist. Why do we need another one? Well, let's check what we already have:

UpdateControls. Very promising and working solution. However it requires to abandon INotifyPropertyChanged along with good old {Binding} extensions and converters which is not easy in legacy code. Plus one should have some courage to give it a try on new commercial projects.

Fody (successor of Notify Property Weaver). Here we are stepping into the realm of IL-rewriters. Fody is free, fast and good. Unfortunately, it has a big limitation - namely it analyzes dependencies only in limits of one class, i.e., it won't propagate calculated property if it depends on a property of nested/child view model.

PostSharp. Also IL-rewriting solution, it has the same benefits that Fody plus it perfectly handles nested dependencies. Hooray! Unfortunately INotifyPropertyChanged implementation is a part of PostSharp Ultimate which is expensive (589 EUR as of 27 May 2014).

This list is not full obviously, there are other possible solutions with other or similar limitations. However, it looks like only PostSharp solves the problem to the full extent and unfortunately, it's not free.

Introducing PropertyChangedPropagator

Long story short, it's better to explain how to use it by example (pay attention to statements in bold):

C#
public int Qty
{
    get { return _qty; }
    set { Set(ref _qty, value); }
}

public decimal UnitPrice
{
    get { return _unitPrice; }
    set { Set(ref _unitPrice, value); }
}

public decimal TotalPrice
{
    get
    {
        RaiseMeWhen(this, has => has.Changed(_ => _.UnitPrice, _ => _.Qty));
        return UnitPrice * Qty;
    }
}

public decimal ExchTotalPrice
{
    get
    {
        RaiseMeWhen(this, has => has.Changed(_ => _.TotalPrice, _ => _.ExchangeRate));
        RaiseMeWhenDynamic(ExchangeRate, has => has.Changed(_ => _.Rate));
        return TotalPrice * ExchangeRate.Rate;
    }
}

public ExchangeRateViewModel ExchangeRate
{
    get { return _exchangeRate; }
    set { Set(ref _exchangeRate, value); }
}

As you can see, the initial example has been turned inside out - Qty and UnitPrice properties (and ExchangeRate.Rate property of nested view model) don't know if there is anything depending on them, they just mind to fire their own notifications. And calculated properties say explicitly "yes, I'm depending on this and that, please raise me when some of those have changed".

How does RaiseMeWhen know which property to propagate? It uses [CallerMemberName] attribute:

C#
public void RaiseMeWhen<T>(T dep, Action<IRaiseMeWhenHas<T>> has, 
    [CallerMemberName] string propertyName = null) ...   

Though my solution does violate "don't repeat yourself" principle (as well as initial naive example), it encourages to declare dependencies just where they belong - in getters of dependent properties.

Advanced Details

The trickiest part here is RaiseMeWhenDynamic. It should be used if some property depends on a property of nested view model and this nested view model itself is a subject to change at runtime. Sounds complicated, but in fact quite simple. In the attached example, we can choose one of ExchangeRateViewModel nested view models and change Rate property of currently active object.

A drawback of dynamic dependencies is that if there are several nested dependencies of the same type, then it's required to add some string hint to distinguish one dynamic dependency from another (fortunately this situation is rare). Consider the pseudo code:

C#
public string CurrentContactName
{
    get
    {
// PrimaryContact and SecondaryContact are of same type so have to use hints
        RaiseMeWhenDynamic(PrimaryContact,
        "Primary", has => has.Changed(_ => _.Name));
        RaiseMeWhenDynamic(SecondaryContact,
        "Secondary", has => has.Changed(_ => _.Name));
        return (PrimaryContact ?? SecondaryContact).Name;
    }
}

One of undeservedly little-known features of INotifyPropertyChanged is to fire PropertyChanged event with string.Empty property name which means "hey, the whole object changed, refresh all properties". RaiseMeWhen allows to do just that if invoked from constructor (NOTE: It doesn't work for dynamic dependencies). Another aggressive approach is to raise notification when any property of nested view model changed. The code below demonstrates both features:

public MyViewModel(NestedViewModel nested)
{
    NestedVm = nested;
    RaiseMeWhen(NestedVm, has => has.ChangedAny());
} 

Usage of PropertyChangePropagator might look like declarative code but there is no IL rewriting and the implementation is perfectly imperative. Which means that all propagation subscriptions are lazily created (or dynamically modified in case of dynamic subscriptions) only if getter of dependent property is invoked. In human words - if nobody reads the calculated property, then it won't be propagated. This "feature-like bug" is usually not a problem in real life (after all, you declare properties for views to read them), but makes unit testing a bit more tricky. :-) Check out the source code for details.

The implementation is also thread-safe and uses weak subscriptions, so it's safe to declare dependencies on long-living publishers as subscribers will be eligible for garbage collection when they go out of scope. There is also an implementation trick to optimize memory usage.

I highly recommend to download and run the source code to see more details.

Performance

The solution includes performance tests which gives 1.5-2x times slower results than naive implementation. Analogue in PostSharp is 9 times slower on my machine, I didn't include it in the attached code though because of license dependency. In general, performance will depend on actual complexity of your view models' hierarchy.

Subscription callbacks created lazily on first getter invocation and then cached. In case of dynamic dependencies, subscriptions will be recreated on subsequent read after reference to the dynamic child view model changed.

Lambda expression trees to specify dependencies are themselves part of "has" callback's body and therefore constructed only once during subscription:

C#
RaiseMeWhen(this, has => has.Changed(_ => _.TotalPrice, _ => _.ExchangeRate));

Propagation handlers made static to optimize memory footprint, i.e., same instances of generated methods are used for multiple view models of the same type. See StaticPropertyChangedHandlersCache class for implementation details.

How to Add This to My Project?

Easy! Copy PropertyChangedPropagator.cs and WeakEventListener.cs and check BindableBaseEx.cs to get the idea how to include RaiseMeWhen* methods in your base view model class.

It works equally well in WPF and WinRT (didn't try Silverlight, but must be good too).

Possible Improvements

It would be good to add support of ObservableCollection<T> and to find some way to work with multiple dynamic dependencies of the same type without string hints.

Summary

PropertyChangedPropagator can be used as a free and easy solution to propagate property dependencies. It works nicely with legacy code and can be adopted gradually. However if you already have PostSharp Ultimate licence (or spare money for it), then I'd recommend to use it as it's a more mature solution with official support.

History

  • 5 June 2014; Added Performance section
  • 27 May 2014: Initial version

License

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


Written By
Software Developer
New Zealand New Zealand
Passionate .NET developer since times of C# 1.0
Originally from Belarus, moved to New Zealand in 2013

Comments and Discussions

 
GeneralMy vote of 4 Pin
Evgeny Bestfator27-Apr-17 21:27
professionalEvgeny Bestfator27-Apr-17 21:27 
GeneralRe: My vote of 4 Pin
Nick Dunets2-Jul-17 22:38
Nick Dunets2-Jul-17 22:38 
QuestionGOOD JOB Pin
mikor-soft16-Feb-17 22:48
mikor-soft16-Feb-17 22:48 
GeneralMy vote of 5 Pin
Assil10-Jan-17 8:10
professionalAssil10-Jan-17 8:10 
QuestionDownload Pin
Member 78373512-Sep-15 10:47
Member 78373512-Sep-15 10:47 
AnswerRe: Download Pin
Shadowcouncil10-Jan-16 10:03
Shadowcouncil10-Jan-16 10:03 
GeneralMy vote of 5 Pin
D V L15-Feb-15 19:19
professionalD V L15-Feb-15 19:19 
GeneralSimilar projects Pin
ncdahlquist24-Nov-14 8:47
ncdahlquist24-Nov-14 8:47 
GeneralRe: Similar projects Pin
Nick Dunets14-Nov-16 16:02
Nick Dunets14-Nov-16 16:02 
QuestionNot bad. Pin
Pete O'Hanlon27-May-14 2:52
mvePete O'Hanlon27-May-14 2:52 
AnswerRe: Not bad. Pin
Nick Dunets27-May-14 10:01
Nick Dunets27-May-14 10:01 
GeneralRe: Not bad. Pin
William E. Kempf30-May-14 9:37
William E. Kempf30-May-14 9:37 
GeneralRe: Not bad. Pin
Nick Dunets4-Jun-14 12:16
Nick Dunets4-Jun-14 12:16 
GeneralRe: Not bad. Pin
wkempf5-Jun-14 2:30
wkempf5-Jun-14 2:30 
GeneralRe: Not bad. Pin
Nick Dunets5-Jun-14 10:37
Nick Dunets5-Jun-14 10:37 
QuestionNice, I quite like this Pin
Sacha Barber27-May-14 2:30
Sacha Barber27-May-14 2:30 
AnswerRe: Nice, I quite like this Pin
Pete O'Hanlon27-May-14 7:47
mvePete O'Hanlon27-May-14 7:47 
AnswerRe: Nice, I quite like this Pin
Nick Dunets27-May-14 10:09
Nick Dunets27-May-14 10:09 
QuestionCan you post the code ? Pin
melnac27-May-14 0:02
melnac27-May-14 0:02 
Hi,
Can you post the code ?

Thanks in advance.
AnswerRe: Can you post the code ? Pin
Nick Dunets27-May-14 9:59
Nick Dunets27-May-14 9:59 

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.