Inside XPF: Data Binding

Updated 2010-09-20: Reflects the removal of the generic type parameter TOwner from ReactiveProperty (starting Build 20). Neuroanatomical Connectivity Macaque by Arenamontanus In the previous post in this series, we discussed XPF’s Reactive Properties – the counterpart to Silverlight’s Dependency Properties.  Reactive Properties allow you to data bind to any source that implements the new .Net 4.0 IObservable<T> (for one-way binding) and/or any source that implements IObserver<T> (for one-way-to-source binding).  Two-way binding is simply a combination of the two.

The Reactive Object

In Silverlight, all UI Elements are derived from DependencyObject which the MSDN documentation says "Represents an object that participates in the dependency property system".  In XPF all UI Elements are derived from ReactiveObject which represents an object that participates in the reactive property system.  ReactiveObject not only stores the current values of all the element’s reactive properties, but also allows you to bind to them in either (or both) directions.

The Bind method has overloads that allow you to bind a ReactiveProperty one-way to an IObservable, one-way-to-source to an IObserver or two-way to both.

The Binding Factory

So what do you do if the object you want to bind to doesn’t expose an IObservable/IObserver?  This is where the BindingFactory comes in.  The BindingFactory will create IObservables and IObservers around properties on either the data context or a specified source.

Binding to reactive properties, or properties on POCOs and objects that implement INotifyPropertyChanged is fully supported.  This enables element-to-element binding in addition to the use of familiar binding patterns often used in WPF/Silverlight such as Model View ViewModel (MVVM).

The observable/observer that the BindingFactory creates for you is internally wrapped in a Binding object which allows for either immediate or deferred resolution of the binding.  Immediate resolution is used when a reference to the source is provided, but if you are binding to an element’s data context then the binding resolution is deferred until the start of the Measure layout phase.  Deferred bindings are great because they allow the data context to be set or changed after the element binding has been declared.  This is essential for controls that use templates such as the ItemsControl.  The Binding’s resolution is handled seamlessly.

All data binding scenarios are supported.  The following table shows the various combinations of source objects/properties in each mode.  Note that binding to POCO properties works in both directions, but without INotifyPropertyChanged (INPC), one-way binding effectively becomes “one-time”.

Data Context /Source Object Reactive Property INPC Property CLR Property
One-way Supported Supported Supported Supported (one-time)
One-way-to-source Not applicable Supported Supported Supported
Two-way Not applicable Supported Supported Supported (one-time and one-way-to-source)

Show me some code

Here are the overloads to ReactiveObject’s Bind method:

Bind

You’ll notice that because it’s a generic method, the parameters are strongly typed to match the target ReactiveProperty specified.  The scenario in the screenshot above shows binding to a source reactive property on the element’s data context.  The specification for this scenario looks like this:

[Subject(typeof(ReactiveObject), "One Way")]
public class when_there_is_a_one_way_binding_to_a_property_on_the_data_context
{
    private const double ExpectedWidth = 10d;

    private static SourceObject source;

    private static ContentControl target;

    private Establish context = () =>
        {
            target = new ContentControl();

            IObservable<double> fromSource =
                BindingFactory.CreateOneWay<SourceObject, double>(SourceObject.WidthProperty);
            target.Bind(UIElement.WidthProperty, fromSource);

            source = new SourceObject();
            target.DataContext = source;
            target.Measure(Size.Empty);
        };

    private Because of = () => source.Width = ExpectedWidth;

    private It should_update_the_target = () => target.Width.ShouldEqual(ExpectedWidth);
}

Because we’re binding to the control’s data context the binding resolution is deferred until the target is measured.

Using the Binding Factory

Here are some examples of how to create Bindings using the Binding Factory.  Those that involve the data context are all deferred and those where the source is specified are all immediate.  An IDualChannel simply encapsulates both an IObservable and an IObserver so that data can flow in both directions (to and from the source).

Notice that you only need to specify type parameters when there is not enough information in the method’s parameters for the compiler to infer the types involved:

To the Data Context

IObservable<double> fromSource =
    BindingFactory.CreateOneWay<double>();

IObserver<double> toSource =
    BindingFactory.CreateOneWayToSource<double>();

To a Reactive Property on the Data Context

IObservable<double> fromSource =
    BindingFactory.CreateOneWay<SourceObject, double>(SourceObject.WidthProperty);

IObserver<double> toSource =
    BindingFactory.CreateOneWayToSource<SourceObject, double>(SourceObject.WidthProperty);

IDualChannel<double> fromAndToSource =
    BindingFactory.CreateTwoWay<SourceObject, double>(SourceObject.WidthProperty);

To a Reactive Property on a specified Source

IObservable<double> fromSource =
    BindingFactory.CreateOneWay(source, SourceObject.WidthProperty);

IObserver<double> toSource =
    BindingFactory.CreateOneWayToSource(source, SourceObject.WidthProperty);

IDualChannel<double> fromAndToSource =
    BindingFactory.CreateTwoWay(source, SourceObject.WidthProperty);

To an INotifyPropertyChanged or CLR Property on the Data Context

IObservable<double> fromSource =
    BindingFactory.CreateOneWay<SourceObject, double>(o => o.Width);

IObserver<double> toSource =
    BindingFactory.CreateOneWayToSource<SourceObject, double>(o => o.Width);

IDualChannel<double> fromAndToSource =
    BindingFactory.CreateTwoWay<SourceObject, double>(o => o.Width);

To an INotifyPropertyChanged or CLR Property on a specified Source

IObservable<double> fromSource =
    BindingFactory.CreateOneWay(source, o => o.Width);

IObserver<double> toSource =
    BindingFactory.CreateOneWayToSource(source, o => o.Width);

IDualChannel<double> fromAndToSource =
    BindingFactory.CreateTwoWay(source, o => o.Width);

Clearing a Binding

To clear a binding simply call the ClearBinding() method on the relevant control, specifying the reactive property whose binding you want to clear:

element.ClearBinding(Border.BorderBrushProperty);

Because an instance of ReactiveObject can only have one binding per property, calling the Bind() method replaces any existing binding.  Internally it calls ClearBinding() before attaching the new binding.

When a binding is cleared, its subscription to the source is automatically disposed.  This releases any attached event handlers (created automatically in order to listen to PropertyChanged events) which helps prevent memory leaks.  This is a great feature of Microsoft’s Reactive Extensions (Rx) that frees the developer from worrying about having to un-hook event handlers when an observer is no longer interested in the event.

Even though there can only be one binding at a time to an element’s property, you can still merge multiple observables together using any of the Rx combinators (Merge, Zip, CombineLatest etc) if you want your reactive property to listen to more than one source.

Summary

We hope that the binding system in XPF is simple to understand and easy to use.  Whilst the underlying property system in XPF is push based (rather than pull), we’ve strived to keep the development experience familiar to Silverlight and WPF developers.

The tight integration with Reactive Extensions makes it extremely flexible and powerful.  The statically typed nature of the binding and property system makes it intuitive, discoverable and refactoring friendly.  The absence of any magic strings means that there should be no run-time binding exceptions when a deferred binding cannot be resolved.  Binding to nested properties is currently not supported, but we plan to introduce this using the same strongly typed lambda expression syntax shown above.

Please download the latest build, play about with it and feedback to us!

Inside XPF Series: