Click here to Skip to main content
15,880,364 members
Articles / Desktop Programming / WPF

Simplifying (View Model-) Data Binding Using Expression-Trees

Rate me:
Please Sign up or sign in to vote.
4.88/5 (14 votes)
20 Sep 2010Ms-PL8 min read 33.8K   395   42   10
Want to get rid of messy "PropertyChanged"-subscriptions and too many OnPropertyChanged("...") to manage value relations? Then DataBinder is what you're looking for!

Introduction

In modern applications, everything should be dynamic. As soon as the user changes a textbox, he wants to see the results immediately. Thanks to the MVVM pattern, we are able to solve this tricky requirement using data binding. However, we still have to do much work to keep the data in our View Models and on the screen synchronized. Especially when dealing with composed values or relations to other View Models, our View Models need to deal with subscribing to PropertyChanged handlers of other View Models. Really?

Basic data binding (First example)

Example - the "old" way

Let's begin using a more or less real-world example: The user can change both the first name and last name of a person while his full name should automatically combine both the first name and last name.

A simple program illustrating this might look like the one in the screenshot:

FirstScreenshot.PNG

How could the fitting View Model look like? Well, maybe, quite similar to that one:

C#
public sealed class PersonViewModel
    : ViewModel
{
    private string _firstName;
    private string _lastName;

    public string LastName
    {
        get
        {
            return this._lastName;
        }
        set
        {
            if (this.LastName == value)
                return;

            this._lastName = value;
            this.OnPropertyChanged("LastName");
        }
    }
    public string FirstName
    {
        get
        {
            return this._firstName;
        }
        set
        {
            if (this.FirstName == value)
                return;

            this._firstName = value;
            this.OnPropertyChanged("FirstName");
        }
    }
    public string FullName
    {
        get
        {
            return String.Format("{1}, {0}", this.FirstName, this.LastName);
        }
    }
}

Refreshing FullName

However, the implementation still does not work as we want it to: FullName is only actualized once - if the user now changes FirstName or LastName, it won't be refreshed. As a solution, you could modify both FirstName and LastName by adding a call to OnPropertyChanged("FullName"). The setters would now look similar to those:

C#
if (this.LastName == value)
    return;

this._lastName = value;
this.OnPropertyChanged("LastName");
this.OnPropertyChanged("FullName");

//and:

if (this.FirstName == value)
    return;

this._firstName = value;
this.OnPropertyChanged("FirstName");
this.OnPropertyChanged("FullName");

Adding further properties

However, it starts getting complicated: imagine you want to extend the FullName by adding a title and additionally provide a property ShortName returning FullName without the title.

This means one more call to OnPropertyChanged("ShortName") in the setters of LastName and FirstName and one call to OnPropertyChanged("FullName") in the setter of Title.

SecondScreenhot.PNG

Disadvantages

However, the approach of simply inserting OnPropertyChanged calls into the setters comes with two disadvantages:

  • You can easily forget to add / remove an OnPropertyChanged("...") call when changing dependent properties like FullName or ShortName.
  • Your setters start getting messy because of the many OnPropertyChanged("...") calls.

In short, the maintainability of your code starts decreasing.

Solution - the "new" way

Now, what would you say if we could simply leave the implementation of LastName and FirstName the way we initially implemented them and only had to change FullName? It's easy - just follow these two steps:

Replacing the dependent properties with a read-only property relying on a backing field

I'll let the code speak for this step ;-)

C#
private string _fullName; 
private string _shortName;

public string FullName
{
    get
    {
        return this._fullName;
    }
    private set
    {
        if (this.FullName == value)
            return;

        this._fullName = value;
        this.OnPropertyChanged("FullName");
    }
}

public string ShortName
{
    get
    {
        return this._shortName;
    }
    private set
    {
        if (this.ShortName == value)
            return;

        this._shortName = value;
        this.OnPropertyChanged("ShortName");
    }
}

Adding the magic - introducing DataBinder

And now, add the following two lines to the constructor:

C#
DataBinder.Bind(s => this.FullName = s, () => String.Format("{0} {1} {2}", 
                this.Title, this.FirstName, this.LastName));
DataBinder.Bind(s => this.ShortName = s, () => this.LastName + ", " + this.FirstName);

That's it. You simply specify what FullName and ShortName should contain. DataBinder will do the entire refreshing process for you.

What it is able to do

Syntax

The syntax is quite easy to understand:

C#
DataBinder.Bind([callback], [expression to "bind" to the callback]);

Call the static method DataBinder.Bind passing a callback which should be called whenever the value of the second parameter might have changed. Most commonly, this callback will simply store the value in a property. As a second parameter, you should pass an expression returning the value you would like to "bind" to the callback. Please note that only objects accessed from within this expression might be observed. The usage of cached results in local variables might prevent you from always seeing the current value of the expression.

Supported cases

Property accesses

You already saw this in the first example. You can bind to any property of any type.

C#
DataBinder.Bind(v => this.Property = v, () => this.OtherProperty);

If the type providing the property implements INotifyPropertyChanged, the value will be kept up-to-date.

Chained property accesses

You are not only able to bind to direct properties of objects, but also to properties of properties (and so on).

C#
DataBinder.Bind(v => this.Property = v, () => this.Parent.OtherProperty);

Again, if the type providing the final property (in this case, this is the type of Parent) implements INotifyPropertyChanged and the property's value changes, the new result will be reported via the callback. In addition, if the type providing the first property (in this example, this) notifies about a change of the accessed property (Parent in this example), the new result will be reported via the callback, too. Furthermore, DataBinder will unsubscribe from PropertyChanged of the property's old value and subscribe to the property's new value. This makes DataBinder very handy!

Calling static methods

Feel free to call any static method inside the bound expression:

C#
DataBinder.Bind(v => this.Property = v, () => String.Format("{0}", this.OtherProperty));

Reevaluation occurs whenever a parameter's value changes (and the change is reported via PropertyChanged), or a property of the parameter itself changes.

Calling instance methods

Instance methods can be used like static methods:

C#
DataBinder.Bind(v => this.Property = v, () => this.Foo());

Just like with static methods, parameter changes will result in reevaluation. In addition, any property change of the target object will result in a reevaluation, too (of course, only if it implements INotifyPropertyChanged).

Using operators

Operators can be used, too:

C#
DataBinder.Bind(v => this.Property = v, () => (this.Age + 42).ToString() + " years old");

As soon as an accessed property of one of the operands changes, the expression will be reevaluated.

Conditions

It's also possible to make use of the tertiary operator or the null-coalescing operator:

C#
DataBinder.Bind(v => this.Property = v, () => this.FirstName == null ? 
                                               "<unknown>" : this.FirstName);
DataBinder.Bind(v => this.Property = v, () => this.FirstName ?? "<unknown>");

Besides property changes of the objects involved in the value paths, changes of properties used in the conditional part will result in expression reevaluation.

Any combination of them

By combining those possibilities, you should be able to formulate almost any expression you need. If not, please let me know via the comments.

Unleashing the full power (Second example)

Adding associations

We will add some new functionality to our old sample: the user is now able to select both a person and a job from a list. As a result, a payroll is displayed for the selected combination of job and person. However, the user should still be able to edit both job and person details. Here's how the application will look like (it's the one attached to this article):

FinalScreenshot.PNG

The View Model classes look like this:

ClassDiagram.png

Imagine how you would implement the payroll (if you want to return it as a string):

  • Add code to the setters of both SelectedPerson and SelectedJob to refresh the payroll when one of them changes
  • Subscribe to PropertyChanged of SelectedPerson and unsubscribe accordingly
  • Subscribe to PropertyChanged of SelectedJob and unsubscribe accordingly
  • Provide a method RefreshPayroll to recreate the payroll

Sounds quite much for such a simple task, doesn't it? Using DataBinder, you can again simply give this burden away from your responsibility:

C#
DataBinder.Bind(p => this.Payroll = p, () => String.Format("Payroll for {0}:\r\n", 
                this.SelectedPerson.FullName) + String.Format("{0}: Month: {1} - Year: {2}", 
                this.SelectedJob.Description, this.SelectedJob.MonthlySalary, 
                this.SelectedJob.MonthlySalary * 12));

...and you're done.

Note: Of course, you could refactor that expression by splitting it up into multiple smaller expressions. I only wanted you to show how powerful DataBinder actually is. :-)

What it doesn't do (yet) - Known limitations

  • The callback might be called passing the same value as the previous call.
  • The callback might be called multiple times for a single property change.
  • No caching of expression parts if performed - the entire expression will be evaluated every time.
  • No recursion detection - your properties have to ensure that they do not call PropertyChanged when applying the same value to them as they already hold.

How it works

You might now wonder what's inside DataBinder to enable its awesome features. You will be surprised (I was too) how less code is required to implement it.

Signature

Take a look at the signature of the Bind method again:

C#
public static void Bind<tresult>(Action<tresult> changedCallback, 
                   Expression<func><tresult>> valueExpression)

As you can see, the expression which provides the value actually is of type System.Linq.Expressions.Expression<TDelegate>. This explains why the parameter passed to Bind can be evaluated multiple times and why you are formulating it as a lambda.

Internals

Expression traversing

DataBinder internally uses an ExpressionVisitor to inspect the passed expression. It overrides the following two methods (meaning that it specially treats accesses to members and method calls).

Note that ExpressionVisitor already takes care of recursively traversing through the entire expression tree.

C#
protected override Expression VisitMember(MemberExpression node)
{
    if (typeof(INotifyPropertyChanged).IsAssignableFrom(node.Expression.Type))
    {
        Dependency dependency = this.AddDependencyToProperty(node.Expression, 
                                                             node.Member.Name);

        using (this.PushChildDependency(dependency))
            return base.VisitMember(node);
    }

    return base.VisitMember(node);
}

protected override Expression VisitMethodCall(MethodCallExpression node)
{
    foreach (Expression argument in node.Arguments)
    {
        if (typeof(INotifyPropertyChanged).IsAssignableFrom(argument.Type))
            this.AddDependencyToProperty(argument, "");
    }

    if (node.Object != null
        && typeof(INotifyPropertyChanged).IsAssignableFrom(node.Object.Type))
    {
        Dependency dependency = this.AddDependencyToProperty(node.Object, "");

        using (this.PushChildDependency(dependency))
            return base.VisitMethodCall(node);
    }

    return base.VisitMethodCall(node);
}

Members

Without having to take a look at the rest of the class, you should be able to follow what happens when the visitor comes across a member: it checks whether the member belongs to a type implementing INotifyPropertyChanged:

C#
if (typeof(INotifyPropertyChanged).IsAssignableFrom(argument.Type))

If so, a dependency to the accessed property is added:

C#
this.AddDependencyToProperty(node.Expression, node.Member.Name);

The PushChildDependency method takes care of expressions like this.Parent.Parent.FirstName. The dependency to this.Parent would have a dependency to Parent.FirstName as a child allowing handler (un-)subscription when this.Parent changes.

Methods

Methods are processed quite similar to members. However, instead of adding a dependency to a specific property, a dependency to all properties (empty string) is added. Parameters receive special treatment, too: if they are of type INotifyPropertyChanged, any change of a property results in a reevaluation of the entire expression. This is a feature! :-)

Misc

After first traversing the expression tree, subscriptions to PropertyChanged will be added where required. While doing so, the expression will be partially evaluated multiple times to extract the objects implementing INotifyPropertyChanged.

What's inside the attachment?

Attached to this article, you will find a solution containing three projects:

  • "DataBinder": The DataBinder component itself
  • "DataBinderSample": The sample application used in this article
  • "Tests": Some unit-tests for the DataBinder

Note: You will need the Code Contracts Rewriter to successfully build the projects. Alternatively, you can remove the definition of the constant "CONTRACTS_FULL" in the project settings.

Have fun using the DataBinder!

If you find any bugs or have any ideas for how to improve DataBinder, please let me know using the comments.

History

  1. 09/19/2010: Initial release.
  2. 09/20/2010: Corrected attachment containing the projects twice.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


Written By
Germany Germany
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionMy vote of 5 but have a compiler error Pin
marcelo.nicolet23-Jun-17 6:59
marcelo.nicolet23-Jun-17 6:59 
QuestionA .Net 3.5 version for help Pin
dvptUml28-Jul-11 4:46
dvptUml28-Jul-11 4:46 
QuestionDo you have a .Net 3.5 version? Pin
sestoenner30-Sep-10 5:29
sestoenner30-Sep-10 5:29 
AnswerRe: Do you have a .Net 3.5 version? Pin
winSharp931-Oct-10 3:07
winSharp931-Oct-10 3:07 
GeneralMy vote of 5 Pin
geo_m21-Sep-10 1:57
geo_m21-Sep-10 1:57 
GeneralAnother solution Pin
zlezj20-Sep-10 11:11
zlezj20-Sep-10 11:11 
GeneralClear and useful .5 from me . Pin
tarasn20-Sep-10 4:43
tarasn20-Sep-10 4:43 
GeneralMy vote of 5 Pin
christoph brändle19-Sep-10 22:00
christoph brändle19-Sep-10 22:00 
GeneralMy vote of 5 Pin
George Danila19-Sep-10 21:39
George Danila19-Sep-10 21:39 
GeneralMy vote of 5 Pin
jhb119-Sep-10 16:24
jhb119-Sep-10 16:24 

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.