Share via


Chapter 5: Data Binding

 

Introduction
Chapter 1: The "Longhorn" Application Model
Chapter 2: Building a "Longhorn" Application
Chapter 3: Controls and XAML
Chapter 4: Storage

Chapter 5: Data Binding

Brent Rector
Wise Owl Consulting

February 2004

Contents

Creating a Data Binding
Data Binding Types
Transformers
Providing Property Change Notifications
Summary

Data binding in its traditional sense means associating some underlying data with one or more user interface elements. The data provides the information to display. The user interface elements render the information in the appropriate format.

"Longhorn" extends the traditional idea of data binding in a number of ways. You can bind a property of a user interface element to a property of any common language runtime (CLR) object, or to an attribute of an XML node.

The data binding can be unidirectional (in either direction) or bidirectional. For example, traditional data binding has the information in the data source flow to its bound user interface element. Alternatively, the information in the user interface element can flow back to the data source. Bidirectional data binding, of course, supports information flow in each direction and enables user input via user interface element to update the data in the data source.

Data binding can also be static (one time only) or dynamic. With static data binding, the information transfer occurs when you initially create the data binding. Subsequent changes to values in the data do not affect the value in the user interface element. Dynamic data binding allows changes to data in the data source to propagate to the User Interface element and vice versa.

"Longhorn" data binding also supports transformation of the data as it flows to and from the data source and user interface elements. This transformation enables the application author to add UI semantics to the data.

Some typical transformations might be the following:

  • Displaying negative numbers in red and positive numbers in black
  • Displaying images based on a contact being online or offline
  • Creating dynamic bar charts by binding a rectangle's height to a stock price
  • Animating the position of an image by binding its coordinates to a property of a CLR object

"Longhorn" data binding is also fundamentally asynchronous. A user interface element receives an event when a new data source binds to the element. In addition, as a data source collects its data, it fires events to indicate that its contents have changed.

One can data-bind to any CLR object or XMLnode, and hence one can easily data-bind to various data models in "Longhorn"—CLR objects, XML, ADO.NET Datasets, Web service messages, or WinFS objects. "Longhorn" also provides a number of built-in data source classes that allow you to easily and declaratively bring data asynchronously into an application. There are specific data sources for XML, .NET objects, ADO.NET datasets, and WinFS objects. The data source model is extensible, so you can create your own custom data source classes when necessary.

Creating a Data Binding

You can bind a dynamic property of a user interface element to a property of any CLR object. To do this, you must describe the desired correspondence between some item of a data source and the target user-interface element. Each such correspondence, or binding, must specify the following:

  • The data source item
  • The path to the appropriate value in the data source item
  • The target user-interface element
  • The appropriate property of the target user-interface element

The Data Binding

The Framework represents a data binding by an instance of the MSAvalon.Data.Bind class. This class has a number of properties that control the data binding: Path, BindType, UpdateType, Transformer, Culture, BindFlags, and Source.

You set the Path property to a string that specifies the property or value in the data source to which the bind object binds. The BindType property controls the direction and frequency of the data bind. It must be one of three values: OneWay, TwoWay, and OneTime. A OneWay bind type causes the data binding to transfer new values from the data source to the target property but it does not propagate changes in the target property back to the data source. A TwoWay bind type propagates changes in both directions. When you specify the OneTime bind type, the data binding transfers the value from the data source to the target property only when you first activate the binding.

You can set the Transformer property to any object that implements the IDataTransformer interface. When the data binding propagates a value, it passes the value through the transformer. The transformer examines the incoming value and produces a new value as output. Note that the input and output values don't need to be the same type. You could have an integer flag as input and produce different image files as output.

The UpdateType property determines when changes to the target property propagate back to the data source when the bind type is TwoWay. You can specify one of three values: Immediate means propagate the new value to the data source immediately after the target property changes; OnLostFocus means propagate the value when the target control loses the input focus; and Explicit says to wait until code calls the bind object and tells it to propagate the new value.

The Source property references the source data item of the binding. You can use the DataSource, ElementSource, DataContextSource, or ObjectSource attributes to set the bind object's Source property. You use the ElementSource attribute to set the Source to an Element by providing the element ID as the value of the ElementSource attribute. The DataContextSource attribute allows you to set the Source to the data context of another element by setting the element ID to the DataContextSource. You use the ObjectSource attribute to specify an object as the source of the binding. Finally, the DataSource attribute allows you to set the Source to the Data property of the DataSource. This will be discussed in detail in The Data Source Item section.

The Culture property allows you to specify a CultureInfo for bindings that need to be culture-aware.

The BindFlags property supports a single nonzero value: NotifyOnTransfer. It's very important to understand that a data binding is inherently asynchronous. When you change the value in the data source, the corresponding target property doesn't receive the updated value right away. It might take an arbitrary amount of time before the new value propagates to the target property. When you need to know when the binding has completed updating the target property, you set the BindFlags property to the NotifyOnTransfer value. The data binding will then fire the MSAvalon.Data.DataTransfer event after updating the target property.

Defining a Binding Expression Using Code

You can create a data binding programmatically, although I expect you'll rarely need or want to do so. Simply create an instance of the Bind class and call the method SetBinding. Here's one possible example:

using MSAvalon.Data;

Bind binding = new Bind ();
binding.Path = path;
binding.BindType = bindType;
binding.Source = source;
binding.UpdateType = updateType;

element.SetBinding (property, binding);

Alternatively, here's another way to write the prior code using one of the Bind class's convenience constructors:

using MSAvalon.Data;

Bind binding = new Bind (path, bindType, source, updateType);
element.SetBinding (property, binding);

You could use other convenience methods, such as the SetBinding method on an element, and simplify the prior code to this:

element.SetBinding (property, path, bindType, source, updateType);

Defining a Binding Expression Using Markup

I expect you'll prefer to define most data bindings using markup. All the previous concepts still apply—you create a Bind object, set its properties to the appropriate values, and associate it with a property of a target element. For example, the following markup creates a Bind object as the value of the Button object's Text property.

<DockPanel xmlns="https://schemas.microsoft.com/2003/xaml" />
  <DockPanel.Resources>
     <myNameSpace:Person def:Name="MyPerson" Name="Bob"/>
  </DockPanel.Resources> . . .
  <Button>
    <Button.Content>
      <Bind Path="Name" BindType="OneWay" ObjectSource="{MyPerson}" />
    </Button.Content>
  </Button>

To associate a data binding with a particular user interface element's property, you use the data binding as the value of the property. In the example just shown, the data binding binds the resource called MyPerson to the Text property of the Button element because I defined the data binding between the Button.Text start and end tags.

The DockPanel Resources property declares that the following child elements are resources. Unlike regular XAML elements, which are instantiated when the runtime parses the XAML file, the runtime does not instantiate a resource until you actually use it.

In the prior example, the Source of the bindings is a Person object, therefore the Bind instance references this Person object instance.

The Path attribute specifies the path within the data source item to the value of interest. In the prior example, the path is simply Name, so the binding retrieves the Name property of the Person instance. However, the path could be more complex. For example, if the Name property returned an object with additional structure, the path could be something like Name.FirstName.

The prior example showed how to define a data binding using a complex property definition for the Button.Text property's value. However, you can use an alternative, and considerably more compact, definition for a data-binding expression. In this case, you define a string as the data-binding expression. The string begins with an asterisk character, which the XAML compiler interprets as an escape character, and then the name of the class to instantiate, and then, enclosed in parentheses, a series of semicolon-separated name value pairs.

<DockPanel >
  §
 <Button Text="*Bind(Path=Name;BindType=OneWay)" />
  §
</DockPanel>

When you define a data binding but don't specify a value for the Source property (using DataSource, ElementSource, DataContextSource or ObjectSource), the data binding retrieves the data source from the DataContext property for the current element. When the current element has no DataContext, the binding object retrieves the parent element's DataContext recursively. This allows you to define a data source once on the appropriate element in your markup and then use that data source in various bindings on child elements.

In the following example, I set the DataContext property of the DockPanel element to a data binding that references my data source. Effectively, all child elements inherit this DataContext property when they don't otherwise set it to a different value. Because the data bindings on the Button elements do not specify a value for the Source property, the binding uses the source from the inherited DataContext. Of course, you can always specify a source to cause a particular data binding to use a different data source.

<DockPanel xmlns="http:////schemas.microsoft.com//2003//xaml//"
           DataContext="{MyPerson}>
  §    <Button Text='*Bind(Path="Name";BindType="OneWay")' />
    <Button Text='*Bind(Path="Age";BindType="OneWay")' />
 §</DockPanel>

Data Binding Types

A particular data binding can be of three types: OneTime, OneWay, and TwoWay. You set the BindType property to one of those enumerated values when you declare the binding.

One-Time Data Binding

When you request one-time data binding, the runtime, using the data source and the specified path, retrieves the source value and initializes the specified target property to that value. Normally, nothing happens subsequently when the source or the target property change value.

However, there are two special cases. When the DataContext of an element changes, effectively, the data source has changed and therefore the binding performs another one-time transfer. In addition, in many cases, the data context refers to a collection of objects. When the current object for a collection changes, the data binding performs a one-time transfer.

One-Way Data Binding

When you request one-way data binding, the runtime retrieves the source value and initializes the specified target property to that value. Each time the source value changes the data binding retrieves the new value and reinitializes the target property.

Two-Way Data Binding

When you request two-way data binding, the runtime retrieves the source value and initializes the specified target property to that value. Each time the source value changes, the data binding retrieves the new value and reinitializes the target property. In addition, when the target property changes value—for example, when the user types into an edit control—the data binding retrieves the new target property value and propagates it back to the source. Two-way data binding is the default type of a data binding.

Transformers

A transformer allows you to convert a value from one form to another as it propagates to and from a data source to a target. You might use a transformer to convert a value from its internal representation to a unique displayed value. For example, you can use a transformer to display a negative floating-point number using red text and a positive number using black text. You can also display different icons for various credit-worthy ratings for a customer.

You can also use a transformer as a data type converter. For example, your source value could be a Point object, while the property to which you want to bind the value requires a Length instance.

A transformer also receives the culture information for the user interface as one of its parameters. You can use this information to tailor the presented user interface to the current culture of the user—for example, you can provide different icons when running under different cultures.

The IDataTransformer Interface

A transformer is any object that implements the IDataTransformer interface. Here's the definition of the interface:

interface IDataTransformer {
  object Transform (object o, DependencyID id, CultureInfo culture);
  object InverseTransform (object o, PropertyInfo pInfo, CultureInfo culture);
}

A data binding calls the Transform method when propagating a source value to a target property. Parameter o is the source value, parameter id identifies the target property, and parameter culture identifies the culture for the transformation.

The data binding calls the InverseTransform method when propagating a changed target property value back to the source. In this case, parameter o is the changed target property's value and pInfo identifies the type to which to convert the value. As before, culture is the culture for the transformation.

Both methods allow you to return null to indicate that the binding should not propagate a value in the respective direction. Here is a simple transformer that returns a color based on an integer value:

<SimpleText Text="*Bind(Path=Name)" Foreground="*Bind(Path=Age; Transformer=AgeToColorTransformer)"/>

public class AgeToColorTransformer: IDataTransformer {
  public object Transform (object o, DependencyID di, CultureInfo culture) {
    int age = (int) o;
    if (age < 0 || age > 120) return Grey;
    if (age <= 30) return Green;
    if (age <= 70) return Gold;
    if (age <= 120) return Red;
  }
  public object InverseTransform (object o, PropertyInfo i, CultureInfo c) {
        return null;
  }
}

Providing Property Change Notifications

The CLR does not provide a generic way for an object to notify its clients that one of its properties has changed. Nevertheless, a dynamic binding requires such notifications so that the binding can propagate changed property values to the target dynamic property. "Longhorn" introduces the IPropertyChange interface to allow an object to signal when one of its properties changes value. Note that the interface defines a single event of type PropertyChangedEventHandler and that the event handler can retrieve the name of the changed property using the PropertyName property of the second parameter to the handler.

interface IPropertyChange {
  event PropertyChangedEventHandler PropertyChanged;
}

delegate void PropertyChangedEventHandler (object sender, 
                                           PropertyChangedEventArgs e);

class PropertyChangedEventArgs : EventArgs {
  public virtual string PropertyName { get ;}
}

In the following code, I've rewritten the Person class from earlier in the chapter to support changing the Name and Age properties and to fire the appropriate events when such changes occur.

namespace MyNamespace {
  public class Person : IPropertyChange {
    private string m_name;
    private int    m_age;
    public event PropertyChangedEventHandler PropertyChanged;

    public string Name {
      get { return m_name; }
      set {
        if (m_name != value) {
          m_name = value;
          RaisePropertyChangeEvent ("Name");
        }
      }
    }
    public int Age {
      get { return m_age; }
      set {
        if (m_age != value) {
          m_age = value;
          RaisePropertyChangeEvent ("Age");
        }
      }
    }

    private void RaisePropertyChangedEvent (string propertyName) {
      if (PropertyChanged != null)
        PropertyChanged (this, new PropertyChangedEventArgs (propertyName));
    }
    
    public Person (string name, int age) {
      m_name = name; m_age = age;
    }
  }
}

Your object implements this interface by calling the PropertyChanged delegate whenever one of its "interesting" properties changes value. Note that you need to invoke the delegate only when a property used in a dynamic binding changes value. Your object can have properties for which you do not fire change notifications.

For performance reasons, you should fire the change notifications only when the property has really changed value. When the object doesn't know which property changed value, it can request that all bindings to any property on itself be updated, or it can pass String.Empty for the changed property name.

The Data Source Item

"Longhorn" provides a set of built-in data sources, which enables you to easily and declaratively get data into the application asynchronously and without blocking UI.

A data source item is any object that implements the IDataSource interface.

interface IDataSource {
        public virtual Object Data { get; }
        public virtual void Refresh()
}

This interface has a Data property that lets the binding get the data from the data source item. The Refresh method allows the binding to request that the data source item retrieve its data when it's not available. "Longhorn" provides a number of data source classes, and you'll see some of them shortly.

Generally, a data source implementation also provides a strongly typed property that returns the native API of the data provider. For example, the SqlDataSource and XmlDataSource classes provide the DataSet and Document properties, respectively:

class SqlDataSource : IDataSource, … {
   §    DataSet           DataSet { get; }
    §}
class XmlDataSource : IDataSource, … {
    §    XmlDocument       Document { get; }
    §}

Therefore, if you really have to, you can directly access the underlying data provider.

Using Any CLR Object as a Data Source

The ObjectDataSource class allows you to create an instance of a specified type as a data source item. Commonly, you'll use XAML and declare your data sources as resources in your markup. For example, assume that I have the following definition of a Person class in an assembly called MyAssembly and that I'd like to use an instance of this class as a data source item:

namespace MyNamespace {
  public class Person {
    private string m_name;
    private int m_age;

    public string Name { get { return m_name; } }
    public int Age { get { return m_age; } }

    public Person (string name, int age) {
      m_name = name; m_age = age;
    }
  }
}

The Person class doesn't need any additional support to be a data source item. I can use an instance of the ObjectDataSource class as the data source item and inform it that the underlying data provider should be an instance of the Person class. To do this, I can use markup to declare an instance of an ObjectDataSource as a XAML resource.

<DockPanel>
  <DockPanel.Resources>
    <ObjectDataSource def:Name ="source1"
       TypeName="MyNamespace.Person, MyAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=0123456789abcde f"
       Parameters="Brent, 0x30" />
  </DockPanel.Resources>
</DockPanel>

As always, XAML element names represent Framework class names. Therefore, the ObjectDataSource element says to create an instance of the ObjectDataSource class. The value of the def:Name attribute is the name of this resource ("source1" in the preceding example).

The ObjectDataSource instance will create a new instance of the class referred to by TypeName either by calling its default constructor or, when you specify the Parameters attribute, by calling the constructor that best matches the signature of the value of the Parameter attribute.

Recall that markup is equivalent to code, so the preceding markup is the same as the following code:

ObjectDataSource source1 = new ObjectDataSource();
source1.TypeName = "MyNamespace.Person, MyAssembly, Version=1.0.0.0,   
                    Culture=neutral, PublicKeyToken=0123456789abcdef";
source1.Parameters = "Brent, 0x30";

The ObjectDataSource class also provides mechanisms to call methods on objects and refer to existing objects in addition to simply instantiating a new object.

Using a Data Source with Data Binding

One can declare a data source as a resource in the Resources property of an element or the application level resources. Any data source declared in the Application resources can be used across the application in any page. A data source defined in the resources of an element can be used only within the scope of the element.

One can declare a data source as a resource in the Resources property of an element or the application level resources. Any data source declared in the Application resources can be used across the application in any page. A data source defined in the resources of an element can be used only within the scope of the element.

In the example just shown, the data binding binds the data source called source1. You can set the DataSource attribute to the resource ID of a XAML Resource. When the resource implements the IDataSource interface, the runtime will set the Source property of the Bind instance to the object returned by the Data property of the specified DataSource resource. When the resource does not implement the IDataSource interface, the runtime sets the Source property of the binding to the resource object itself.

In the prior example, the DataSource attribute references an ObjectDataSource resource. Therefore, the binding requests the Data property from the ObjectDataSource. The data source, in turn, instantiates the Person class as specified in the markup.

In the first Button, the Source property of the Bind instance references this Person object instance. The path is simply Name, so the binding retrieves the Name property of the Person instance.

In the second Button, DataContext is bound to the ObjectDataSource. This action will set DataContext of the Button to the Person object so that any bindings on Button will use the Person object as the default source for their bindings.

One can similarly data-bind to any of the available data sources. Some of the other data sources shipped with "Longhorn" are mentioned in paragraphs that follow. Others, such as data sources to get data from Web services, will be coming online.

Using XML as a Data Source

The XmlDataSource class is a data source that uses an XML Document Object Model (DOM) as the underlying data provider. You can use markup to create the DOM from a Uniform Resource Locator (URL) referencing an XML stream. You can also create the DOM by supplying the XML inline with the markup as the following example demonstrates:

Using URI

<DockPanel>
  <DockPanel.Resources>
    <XmlDataSource def:Name="source2"
       Source="http://www.wiseowl.com/People.xml"
       XPath="/People/Person[@Age>21]" />

Using inline markup

    <XmlDataSource def:Nsme="source3"
       XPath="/People/Person[@Age>50]" >
       <People>
           <Person Name='Bambi' Age='61'>
           <Person Name='Bozo' Age='54'>
           <Person Name='Brent' Age='48'>
           §       </People>
    </XmlDataSource>
</DockPanel.Resources> 
</DockPanel>

Using a DataSet as a Data Source

The SqlDataSource class is a data source that uses a DataSet as the underlying data provider. It creates a DataSet and fills it by executing a SQL command on the database.

<DockPanel>
  <DockPanel.Resources>
    <SqlDataSource def:Name="source4">
      ConnectionString="server=localhost;Database=UserGroup"
       SelectCommand="SELECT * FROM Members" />
    </SqlDataSource>
  <DockPanel.Resources> 
</DockPanel>

Alternatively, you can use the ObjectDataSource class and bind to a strongly typed DataSet-derived class that you've defined in a code-behind file.

<DockPanel>
  <DockPanel.Resources >
  <ObjectDataSource def:Name="sds1"
    Type="MyDataset"/>
  </ObjectDataSource>
 </DockPanel.Resources>
</DockPanel>

Using Windows Storage as a Data Source

The WinFS DataSource class uses WinFS as the underlying data provider. You can use it to bind to the everyday information maintained by Microsoft® Windows® Storage.

<DockPanel>
  <DockPanel.Resources>
    <WinFSDataSource ContextString="c:\">
      <WinFSDataSource.Query 
    Type="Person" Filter="DisplayName='Ted'" Sort="DisplayName ASC">
        <Query.ProjectionOptions Field="DisplayName" />
        <Query.ProjectionOptions Field="Birthdate">
          <Projection.ProjectionOptions … />
        </Query.ProjectionOptions>
      </ WinFSDataSource.Query>
    </WinFSDataSource>
  </DockPanel.Resources>
</DockPanel>

Using a Custom Data Source

You can also specify a custom class as a data source. The class must implement the IDataSource interface. You'll typically want to associate an XML namespace prefix with your custom class's namespace, and then use the prefix-qualified class name in the markup as usual:

<Canvas … >
  §   
  <Canvas.Resources>
    <WO:InfraredDataSource def:Name="source8"
       PropA='value1'
       PropB='value2'
    </WO:InfraredDataSource>
  </Canvas.Resources>
</Canvas>

Summary

Data binding provides an easy and efficient method to connect information to a user interface element that displays the data. You get automatic propagation of the values in either direction, one time or repeatedly, with the ability to convert the data representation on the fly when needed. And you can do so with little or no programmatic coding using markup. Data binding allows you to get the data where you want it and move on to writing the rest of your application.

Continue to Chapter 6: Communication