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:
How could the fitting View Model look like? Well, maybe, quite similar to that one:
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:
if (this.LastName == value)
return;
this._lastName = value;
this.OnPropertyChanged("LastName");
this.OnPropertyChanged("FullName");
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
.
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 ;-)
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:
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:
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.
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).
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:
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:
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:
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:
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):
The View Model classes look like this:
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:
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:
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.
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
:
if (typeof(INotifyPropertyChanged).IsAssignableFrom(argument.Type))
If so, a dependency to the accessed property is added:
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
- 09/19/2010: Initial release.
- 09/20/2010: Corrected attachment containing the projects twice.