Chapter 10. Using Silverlight Data Binding

In most of the previous examples in this book, we “manually” set properties of XAML elements to display data. In real world applications, things do not always work that easily. Quite often you have a business logic that pulls data from somewhere, processes it, and then exposes the data in the form of an object. To display this object, you may need code again. Well, maybe, maybe not. With data binding, Silverlight provides a useful feature that once again separates code and logic from the presentation layer. Using declarative syntax, you can define which information from a business object goes where. Silverlight does the rest: it retrieves the data, displays it, and can even write the data back to the object, if you so desire.

Data Binding with Markup

To use Silverlight data binding, you first need a binding source—that’s where the data is coming from. Silverlight supports several kinds of sources, including XML files, the results of LINQ queries, and objects. Throughout most of this chapter we will use the last option—objects as sources—to keep the examples simple and self-contained (and to avoid having to add a new external dependency, e.g., a database). However, in the real world, the possibilities are virtually endless.

The data used in these examples will be data on a person or on various persons. Each person is represented by an instance of a class called Person. This class will be defined in every sample application’s Page.xaml.cs code-behind file. You could also put the code in any other external .cs file within the current project, as long as you make sure that you use the same namespace as the one you use in the XAML application file. Here is the class, using the new getter/setter shortcuts that C# 3.0 provides:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Example 10-1 contains the complete UI for the example. A StackPanel element is used to display several TextBlock elements to show the first and last names of a person.

Example 10-1. The data binding UI (Page.xaml, project Binding)

<UserControl x:Class="BindingCode.Page"
    xmlns="http://schemas.microsoft.com/client/2007" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="400" Height="300">
  <Grid x:Name="LayoutRoot" Background="White">
    <Canvas Canvas.Left="15" Canvas.Top="15" x:Name="PersonPanel">
      <StackPanel Orientation="Vertical" Margin="10">
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="First name: " />
          <TextBlock Text="{Binding FirstName}" Name="txtFirst" />
        </StackPanel>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="Last name: " />
          <TextBlock Text="{Binding LastName}" Name="txtLast" />
        </StackPanel>
      </StackPanel>
    </Canvas>
  </Grid>
</UserControl>

Note the two lines with emphasized code:

<TextBlock Text="{Binding FirstName}" Name="txtFirst" />
<TextBlock Text="{Binding LastName}" Name="txtLast" />

Instead of actually providing text for the TextBlock element, you use curly braces and the following format: {Binding PropertyName}. This markup instructs Silverlight to pull the value for the property from the currently bound data, using the PropertyName property. In Example 10-1, the first text block is filled with the text from the FirstName property, and the second one with the data from the LastName property.

There is still one piece missing: what is the currently bound data? However, you have several options. First, you can use the Source property of every individual element that gets bound data to define where the data to be bound comes from. Alternatively, you can set the DataContext property of the surrounding element (e.g., <Canvas>) to provide the data context for a whole set of elements. And finally, you can also use server code to set these properties.

In our example, using the DataContext property seems to be the best option, since both TextBlock elements need to query the same data source. However, our Person class does not contain any data; therefore, we need to use code to fill Person with data and then set the DataContext property. The best way to do that is to use the Page() constructor. Example 10-2 contains the complete code for this example’s code-behind C# file, and Figure 10-1 shows the output in the browser.

Example 10-2. The code-behind file sets the data context (Page.cs.xaml, project Binding)

using System.Windows.Controls;

namespace Binding
{
    public partial class Page : UserControl
    {
        public Page()
        {
            InitializeComponent();
            PersonPanel.DataContext = new Person { 
                FirstName = "Maria", LastName = "Anders"
            };
        }
    }

    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
}
The data is bound to the text blocks

Figure 10-1. The data is bound to the text blocks

There is another good reason for setting DataContext with code rather than with markup. Imagine that you have several persons to display, and you want to provide users with a UI to page through them. To do this, we will expand the example a little bit by adding two polygons (triangles, actually) that serve as UI elements for backward and forward. Clicking on these triangles then changes the person that is currently displayed. Example 10-3 contains the complete markup.

Example 10-3. Paging through several persons, the UI (Page.xaml.cs, project BindingPaging)

<UserControl x:Class="BindingPaging.Page"
    xmlns="http://schemas.microsoft.com/client/2007" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="400" Height="300">
  <Grid x:Name="LayoutRoot" Background="White">
    <Canvas>
      <Canvas Canvas.Left="15" Canvas.Top="15" x:Name="PersonPanel">
        <StackPanel Orientation="Vertical" Margin="10">
          <StackPanel Orientation="Horizontal">
            <TextBlock Text="First name: " />
        <TextBlock Text="{Binding FirstName}" />
      </StackPanel>
          <StackPanel Orientation="Horizontal">
            <TextBlock Text="Last name: " />
        <TextBlock Text="{Binding LastName}" />
      </StackPanel>
        </StackPanel>
      </Canvas>
      <Canvas Canvas.Left="125" Canvas.Top="85">
        <Polygon Fill="Black" Points="0, 5, 10, 0, 10, 10"
                 MouseLeftButtonDown="prev" />
        <Polygon Fill="Black" Points="15, 0, 15, 10, 25, 5"
                 MouseLeftButtonDown="next" />
      </Canvas>
    </Canvas>
  </Grid>
</UserControl>

The two mouse-click event handlers tied to the two triangles are called prev() and next(). These page through a list of persons and bind to the previous or next person in that list. The list of persons is called persons, and the current position in that list is stored in the variable pos. Here is the code for the two aforementioned methods:

private int pos = -1;
private Person[] persons;

private void prev(object sender, MouseButtonEventArgs e)
{
    if (pos > 0)
    {
        pos--;
    }
    bind();
}

private void next(object sender, MouseButtonEventArgs e)
{
    if (pos < persons.Length - 1)
    {
        pos++;
    }
    bind();
}

The bind() method sets the CanvasDataContext property to the current person in the list:

private void bind()
{
    PersonPanel.DataContext = persons[pos];
}

Finally, the persons array (or list; both data types work well here) needs to be filled. This is done in the constructor of the XAML page code-behind. Don’t forget to display the first person once persons has been populated!:

public Page()
{
    InitializeComponent();
    persons = new Person[] {
      new Person { FirstName = "Maria", LastName = "Anders" },
      new Person { FirstName = "Ana", LastName = "Trujillo" },
      new Person { FirstName = "Antonio", LastName = "Moreno" }
    };
    next(null, null);
}

Refer to Example 10-4 for the complete C# code, and to Figure 10-2 for the browser output.

Example 10-4. Paging through several persons, the code-behind (Page.xaml.cs, project BindingPaging)

using System.Windows.Controls;
using System.Windows.Input;

namespace BindingPaging
{
    public partial class Page : UserControl
    {
        private int pos = -1;
        private Person[] persons;

        public Page()
        {
            InitializeComponent();
            persons = new Person[] {
              new Person { FirstName = "Maria", LastName = "Anders" },
              new Person { FirstName = "Ana", LastName = "Trujillo" },
              new Person { FirstName = "Antonio", LastName = "Moreno" }
            };
            next(null, null);
        }

        private void prev(object sender, MouseButtonEventArgs e)
        {
            if (pos > 0)
            {
                pos--;
            }
            bind();
        }

        private void next(object sender, MouseButtonEventArgs e)
        {
            if (pos < persons.Length - 1)
            {
                pos++;
            }
            bind();
        }

        private void bind()
        {
            PersonPanel.DataContext = persons[pos];
        }
    }

    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
}
Paging back and forth through the person details

Figure 10-2. Paging back and forth through the person details

Data Binding with Code

All bindings so far have used a minimal amount of code and relied on XAML’s declarative syntax to provide the binding information. Of course, it is also possible to provide all binding data using code only. All binding types that you will encounter throughout the rest of this chapter can be implemented and used with code in an analogous fashion.

Let’s start again with the XAML UI. It is quite similar to Example 10-1, without the reference to the binding (see Example 10-5).

Example 10-5. Using code for data binding, the XAML file (Page.xaml, project BindingCode)

<UserControl x:Class="BindingCode.Page"
    xmlns="http://schemas.microsoft.com/client/2007" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="400" Height="300">
  <Grid x:Name="LayoutRoot" Background="White">
    <Canvas Canvas.Left="15" Canvas.Top="15" x:Name="PersonPanel">
      <StackPanel Orientation="Vertical" Margin="10">
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="First name: " />
          <TextBlock Name="txtFirst" />
        </StackPanel>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="Last name: " />
          <TextBlock Name="txtLast" />
        </StackPanel>
      </StackPanel>
    </Canvas>
  </Grid>
</UserControl>

It is up to the C# code to work on the remaining tasks:

Setting the data context

PersonPanel.DataContext = new Person { FirstName = "Maria", LastName = 
   "Anders" };
Setting up a binding
Binding b1 = new System.Windows.Data.Binding("FirstName");
Setting the binding mode (the next section will cover binding modes in more detail)
b1.Mode = System.Windows.Data.BindingMode.OneTime;
Attaching the binding to the target XAML element and property
txtFirst.SetBinding(TextBlock.TextProperty, b1);

Example 10-6 shows the complete code for this example.

Example 10-6. Using code for data binding, the C# file (Page.xaml.cs, project BindingCode)

using System.Windows.Controls;
using System.Windows.Data;

namespace BindingCode
{
    public partial class Page : UserControl
    {
        public Page()
        {
            InitializeComponent();
            PersonPanel.DataContext = new Person { 
                FirstName = "Maria", LastName = "Anders"
          };

          Binding b1 = new Binding("FirstName");
          b1.Mode = BindingMode.OneTime;
          txtFirst.SetBinding(TextBlock.TextProperty, b1);

          Binding b2 = new Binding("LastName");
          b2.Mode = BindingMode.OneTime;
          txtLast.SetBinding(TextBlock.TextProperty, b2);
        }
    }

    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
}

The output of this example is identical to Figure 10-1 shown earlier.

One-Way Data Binding

When working with Silverlight data binding, you need to decide which binding mode you want to use. There are three modes currently supported, and the first one has already been used in all previous examples in this chapter (since it is the default mode):

OneTime

Data binding happens only once, at the time the binding takes place. Changing the binding source later has no effect on the output. This is the default binding mode.

OneWay

Data binding works in one direction only, from the binding source to the binding target. Changing the binding source also changes the binding target, without any extra effort required on the code side.

TwoWay

Data binding works in two directions: changing the binding source changes the binding target, and vice versa.

There is a catch, however. If you want to use the OneWay or TwoWay binding modes, the binding source needs to implement the INotifyPropertyChanged interface (defined in System.ComponentModel). Furthermore, whenever the binding source gets changed, an appropriate event needs to be raised.

To demonstrate this, let’s once again update the UI of the example. This time, we add a text that serves as a button. Clicking this text will reverse the first and last names currently displayed. Example 10-7 shows the new UI markup.

Example 10-7. One-way data binding, the XAML file (Page.xaml, project BindingOneWay)

<UserControl x:Class="BindingOneWay.Page"
    xmlns="http://schemas.microsoft.com/client/2007" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="400" Height="300">
  <Grid x:Name="LayoutRoot" Background="White">
    <Canvas>
      <Canvas Canvas.Left="15" Canvas.Top="15" x:Name="PersonPanel">
        <StackPanel Orientation="Vertical" Margin="10">
          <StackPanel Orientation="Horizontal">
            <TextBlock Text="First name: " />
        <TextBlock Text="{Binding FirstName}" />
      </StackPanel>
          <StackPanel Orientation="Horizontal">
            <TextBlock Text="Last name: " />
        <TextBlock Text="{Binding LastName}" />
      </StackPanel>
        </StackPanel>
      </Canvas>
      <Canvas Canvas.Left="125" Canvas.Top="85">
        <Polygon Fill="Black" Points="0, 5, 10, 0, 10, 10" 
                 MouseLeftButtonDown="prev" />
        <Polygon Fill="Black" Points="15, 0, 15, 10, 25, 5" 
                 MouseLeftButtonDown="next" />
        <TextBlock Text="Reverse" Canvas.Left="40" Canvas.Top="-3"
                   FontSize="11" MouseLeftButtonDown="reverse"  />
      </Canvas>
    </Canvas>
  </Grid>
</UserControl>

In the C# file, we will implement the text reversing and will also use the INotifyPropertyChanged interface. Let’s start with the rather trivial reverse() method:

public void reverse()
{
   char[] c = new Char[this.FirstName.Length];
   for (int i = 0; i < this.FirstName.Length; i++) {
       c[i] = this.FirstName[this.FirstName.Length - 1 - i];
   }
   this.FirstName = new String(c);

   c = new Char[this.LastName.Length];
   for (int i = 0; i < this.LastName.Length; i++) {
       c[i] = this.LastName[this.LastName.Length - 1 - i];
   }
   this.LastName = new String(c);
}

Next, we need a public event that is raised when a property is changed:

public event PropertyChangedEventHandler PropertyChanged;

This event is not automatically used; therefore, we have to use code in the following fashion to fire it whenever a property is changed:

private string firstName;
public string FirstName
{
    get {
        return firstName;
    }
    set
    {
        firstName = value;
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs("FirstName"));
        }
    }
}

For demonstration purposes, only the FirstName property receives this extra treatment. LastName stays as it is:

public string LastName { get; set; }

Finally, we need to implement an event handler for a mouse-click on the new text block. Example 10-8 shows the complete code.

Example 10-8. One-way data binding, the C# file (Page.xaml.cs, project BindingOneWay)

using System;
using System.Windows.Controls;
using System.Windows.Input;
using System.ComponentModel;

namespace BindingOneWay
{
    public partial class Page : UserControl
    {
        private int pos = -1;
        private Person[] persons;

        public Page()
        {
            InitializeComponent();
            persons = new Person[] {
              new Person { FirstName = "Maria", LastName = "Anders" },
              new Person { FirstName = "Ana", LastName = "Trujillo" },
              new Person { FirstName = "Antonio", LastName = "Moreno" }
            };
            next(null, null);
        }

        private void prev(object sender, MouseButtonEventArgs e)
        {
            if (pos > 0)
            {
                pos--;
            }
            bind();
        }

        private void next(object sender, MouseButtonEventArgs e)
        {
            if (pos < persons.Length - 1)
            {
                pos++;
            }
            bind();
        }

        private void reverse(object sender, MouseButtonEventArgs e)
        {
            persons[pos].reverse();
        }
        private void bind()
        {
            PersonPanel.DataContext = persons[pos];
        }
    }

    public class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

          private string firstName;
          public string FirstName
          {
          get {
          return firstName;
          }
          set
          {
          firstName = value;
          if (PropertyChanged != null)
          {
          PropertyChanged(this, new PropertyChangedEventArgs
          ("FirstName"));
          }
          }
          }
          public string LastName { get; set; }
          public void reverse()
          {
          char[] c = new Char[this.FirstName.Length];
          for (int i = 0; i < this.FirstName.Length; i++) {
          c[i] = this.FirstName[this.FirstName.Length - 1 - i];
          }
          this.FirstName = new String(c);

          c = new Char[this.LastName.Length];
          for (int i = 0; i < this.LastName.Length; i++) {
          c[i] = this.LastName[this.LastName.Length - 1 - i];
          }
          this.LastName = new String(c);
          }
    }
}

Notice what happens when you click on Reverse: only the first name is reversed (see Figure 10-3); the last name is not. However, if you move to the next (or previous) person and then go back, both names are reversed (see Figure 10-4). The reason is that only the FirstName property fires the PropertyChanged event. Therefore, only the display of the first name is updated. When going back and forth, however, the data is always bound to the elements again; thus the reversed last name appears.

Only the first name is reversed

Figure 10-3. Only the first name is reversed

Now, both names are reversed

Figure 10-4. Now, both names are reversed

Two-Way Data Binding

The previous example managed to maintain the altered (reversed) person names, since we were manually persisting them in the persons class member. However, Silverlight 2 also features a two-way data binding that allows changes in the display data (the binding target) to be reflected in the original data (the binding source). To use this feature, we change the UI again. We remove the Reverse text element, and exchange the <TextBlock> elements for the first and last names with two <TextBox> elements. This way, not only is the data shown, but it can also be edited by the user. We also need to instruct Silverlight to use two-way data binding:

<TextBox Text="{Binding FirstName, Mode=TwoWay}" x:Name="txtFirst" />
<TextBox Text="{Binding FirstName, Mode=TwoWay}" x:Name="txtLast" />

Example 10-9 contains the complete new markup.

Example 10-9. Two-way data binding, the XAML file (Page.xaml, project BindingTwoWay)

<UserControl x:Class="BindingTwoWay.Page"
    xmlns="http://schemas.microsoft.com/client/2007" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="400" Height="300">
    <Grid x:Name="LayoutRoot" Background="White">
    <Canvas>
      <Canvas Canvas.Left="15" Canvas.Top="15" x:Name="PersonPanel">
        <StackPanel Orientation="Vertical" Margin="10">
          <StackPanel Orientation="Horizontal">
            <TextBlock Text="First name: " />
            <TextBox Text="{Binding FirstName, Mode=TwoWay}"
                x:Name="txtFirst" />
      </StackPanel>
          <StackPanel Orientation="Horizontal">
            <TextBlock Text="Last name: " />
            <TextBox Text="{Binding LastName, Mode=TwoWay}"
                x:Name="txtLast" />
      </StackPanel>
        </StackPanel>
      </Canvas>
      <Canvas Canvas.Left="125" Canvas.Top="85">
        <Polygon Fill="Black" Points="0, 5, 10, 0, 10, 10" 
                 MouseLeftButtonDown="prev" />
        <Polygon Fill="Black" Points="15, 0, 15, 10, 25, 5" 
                 MouseLeftButtonDown="next" />
      </Canvas>
    </Canvas>
  </Grid>
</UserControl>

The C# code has also changed a bit. The reverse() methods have been removed, but the LastName setter now also raises the PropertyChanged event, as Example 10-10 shows.

Example 10-10. Two-way data binding, the C# file (Page.xaml.cs, project BindingTwoWay)

using System.Windows.Controls;
using System.Windows.Input;
using System.ComponentModel;

namespace BindingTwoWay
{
    public partial class Page : UserControl
    {
        private int pos = -1;
        private Person[] persons;

        public Page()
        {
            InitializeComponent();
            persons = new Person[] {
              new Person { FirstName = "Maria", LastName = "Anders" },
              new Person { FirstName = "Ana", LastName = "Trujillo" },
              new Person { FirstName = "Antonio", LastName = "Moreno" }
            };
            next(null, null);
        }

        private void prev(object sender, MouseButtonEventArgs e)
        {
            if (pos > 0)
            {
                pos--;
            }
            bind();
        }

        private void next(object sender, MouseButtonEventArgs e)
        {
            if (pos < persons.Length - 1)
            {
                pos++;
            }
            bind();
        }

        private void bind()
        {
            PersonPanel.DataContext = persons[pos];
        }
    }

    public class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private string firstName;
        public string FirstName
        {
            get
            {
                return firstName;
            }
            set
            {
                firstName = value;
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, 
                                    new PropertyChangedEventArgs("FirstName"));
                }
            }
        }

        private string lastName;
        public string LastName
        {
            get
            {
                return lastName;
            }
            set
            {
                lastName = value;
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, 
                                    new PropertyChangedEventArgs("LastName"));
                }
            }
        }
    }
}

If you run the associated test page (BindingTwoWayTestPage.aspx or BindingTwoWayTestPage.htm), you can now change the names of the persons (see Figure 10-5). When you tab out of the text field (or the text box otherwise loses the focus), the PropertyChanged event is automatically raised. It is not, however, raised immediately after you type something. In the end, when you go back and forth, your changes are still there, so they were automatically transmitted back to the binding source, thanks to two-way data binding.

Changing the text box changes the data source (make sure you tab out of the text box)

Figure 10-5. Changing the text box changes the data source (make sure you tab out of the text box)

Data Conversions

There are some cases when you don’t want to display exactly the data you receive from the data source. Maybe you don’t get strings and numbers, but rather, complex types, and you need to serialize them into something readable. Or you get a date and want to display it according to the culture settings of the browser. Or you want to add some validation and sanitize data before you display it.

We will demonstrate the latter scenario in an example, and the other scenarios can be implemented in the same fashion. As a (very simple and certainly not viable) way of checking data in our application, we want to make sure that no name (neither first nor last) starts with a lowercase letter. Therefore, we convert the data to be displayed by uppercasing the first letter.

Once again, there is an interface to be implemented for such a converter. The interface is called IValueConverter and resides in the System.Windows.Data namespace. The following two methods need to be implemented:

Convert()

Called when data is sent from the binding source to the binding target

ConvertBack()

Called when data is sent back from the binding target to the binding source

Note

Only when using two-way binding is there a need to really implement both methods. If using one-way binding, just implement Convert() and fill ConvertBack() with code in the following fashion:

               throw new NotImplementedException();

Both methods expect the same four arguments:

value

The value to be bound (type: object)

targetType

The target data type of the conversion (type: Type)

parameter

The parameter provided to the conversion method (type: object)

culture

The current culture (type: CultureInfo)

In our example, Convert() uppercases the first letter of the input, and ConvertBack() just returns the data sent to the method (we do not want to alter the data we have; we just want to display it in an amended fashion). Example 10-11 contains the complete C# code-behind for this example.

Example 10-11. Converting bound data, the C# file (Page.xaml.cs, project BindingConverter)

using System;
using System.Windows.Controls;
using System.Windows.Input;
using System.ComponentModel;
using System.Windows.Data;

namespace BindingConverter
{
    public partial class Page : UserControl
    {
        private int pos = -1;
        private Person[] persons;

        public Page()
        {
            InitializeComponent();
            persons = new Person[] {
              new Person { FirstName = "Maria", LastName = "Anders" },
              new Person { FirstName = "Ana", LastName = "Trujillo" },
              new Person { FirstName = "Antonio", LastName = "Moreno" }
            };
            next(null, null);
        }

        private void prev(object sender, MouseButtonEventArgs e)
        {
            if (pos > 0)
            {
                pos--;
            }
            bind();
        }

        private void next(object sender, MouseButtonEventArgs e)
        {
            if (pos < persons.Length - 1)
            {
                pos++;
            }
            bind();
        }

        private void bind()
        {
            PersonPanel.DataContext = persons[pos];
        }

    }

    public class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private string firstName;
        public string FirstName
        {
            get
            {
                return firstName;
            }
            set
            {
                firstName = value;
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, 
                                    new PropertyChangedEventArgs("FirstName"));
                }
            }
        }

        private string lastName;
        public string LastName
        {
            get
            {
                return lastName;
            }
            set
            {
                lastName = value;
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, 
                                    new PropertyChangedEventArgs("LastName"));
                }
            }
        }
    }

    public class UCFirst : IValueConverter
          {
          public object Convert(object value, Type targetType, 
          object parameter, System.Globalization.CultureInfo culture)
          {
          var name = (string)value;
          if (name.Length > 0)
          {
          var first = name.Substring(0, 1);
          name = first.ToUpper() + name.Substring(1);
          }
          return name;
          }

          public object ConvertBack(object value, Type targetType, 
          object parameter, System.Globalization.CultureInfo culture)
          {
          return (string)value;
          }
          }
}

The second step is to tell Silverlight in the XAML markup that this converter needs to be used. To do this, we must first allow XAML to access the classes inside the BindingConverter namespace, including UCFirst. So, we load the converter as an internal resource. First of all, we need to define an XML prefix for classes residing in BindingConverter.

Use the following markup for the first <Canvas> element in the example (we could also use the <UserControl> element):

<Canvas xmlns:local="clr-namespace:BindingConverter">
  ...
</Canvas>

The prefix name local is arbitrary, but it is the de facto standard name in this case. After clr-namespace:, you need to provide the appropriate namespace. If this namespace resides in a different assembly, reference it as well:

<Canvas xmlns:local="clr-namespace:BindingConverter;assembly=myOtherAssembly">
  ...
</Canvas>

Next, create a Resources subelement; in our case (remember that we defined the new prefix in the <Canvas> element), it’s <Canvas.Resources>. There, you can use all classes in that namespace, including the UCFirst converter, and even have IntelliSense support, as Figure 10-6 shows.

IntelliSense support for resources

Figure 10-6. IntelliSense support for resources

Load the converter as a resource, and provide a key (x:Key property) that serves as a reference to that resource:

<Canvas.Resources>
    <local:UCFirst x:Key="ucfirstConverter" />
</Canvas.Resources>

In the two <TextBox> elements, provide the name of the converter using the following syntax:

<TextBox x:Name="txtFirst" 
         Text="{Binding FirstName, Mode=TwoWay, 
         Converter={StaticResource ucfirstConverter}}" />
<TextBox x:Name="txtLast" 
         Text="{Binding LastName, Mode=TwoWay, 
         Converter={StaticResource ucfirstConverter}}" />

As you see, bindings can also be nested in some way. The keyword StaticResource hints that the resource to be used is internal, not external; ucfirstConverter is the value of the x:Key property we assigned to the <local:UCFirst> element.

Warning

Note that the properties of the Text attributes are broken down into two lines for layout reasons only; in your code, just put everything in one line.

No further code changes are necessary; refer to Example 10-12 for the complete XAML markup.

Example 10-12. Converting bound data, the XAML file (Page.xaml, project BindingConverter)

<UserControl x:Class="BindingConverter.Page"
    xmlns="http://schemas.microsoft.com/client/2007" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="400" Height="300">
  <Grid x:Name="LayoutRoot" Background="White">
    <Canvas xmlns:local="clr-namespace:BindingConverter">
      <Canvas.Resources>
        <local:UCFirst x:Key="ucfirstConverter" />
      </Canvas.Resources>
      <Canvas Canvas.Left="15" Canvas.Top="15" x:Name="PersonPanel">
        <StackPanel Orientation="Vertical" Margin="10">
          <StackPanel Orientation="Horizontal">
            <TextBlock Text="First name: " />
            <TextBox x:Name="txtFirst" 
                     Text="{Binding FirstName, Mode=TwoWay, 
          Converter={StaticResource ucfirstConverter}}" />
          </StackPanel>
          <StackPanel Orientation="Horizontal">
            <TextBlock Text="Last name: " />
            <TextBox x:Name="txtLast" 
                     Text="{Binding LastName, Mode=TwoWay, 
          Converter={StaticResource ucfirstConverter}}" />
          </StackPanel>
        </StackPanel>
      </Canvas>
      <Canvas Canvas.Left="125" Canvas.Top="85">
        <Polygon Fill="Black" Points="0, 5, 10, 0, 10, 10" 
                 MouseLeftButtonDown="prev" />
        <Polygon Fill="Black" Points="15, 0, 15, 10, 25, 5" 
                 MouseLeftButtonDown="next" />
      </Canvas>
    </Canvas>
  </Grid>
</UserControl>

When you enter a name that starts with a lowercase letter (see Figure 10-7), tab out of the text box, move to the next (or previous) name, and then go back. Your input now indeed starts with an uppercase letter (see Figure 10-8).

If you enter a name that starts with a lowercase letter...

Figure 10-7. If you enter a name that starts with a lowercase letter...

...the converter will fix your input once you come back to the person

Figure 10-8. ...the converter will fix your input once you come back to the person

Data Validation

When bound data is changed, you may want to make sure that the new data still conforms to some preset rules. In other words: data validation. Since Beta 2, Silverlight 2 supports data validation for bound data. As a starting point, we use the code from Examples 10-9 and 10-10, where we introduced two-way binding. There is a simple reason for that: Silverlight 2’s data validation requires that two-way binding is used. We want to expand the aforementioned examples by outputting an error message if a user provides an empty first or last name in the form.

To do so, we first need to update the XAML file. When providing the binding information in the <TextBox> elements’ Text properties, we need to set two attributes to True:

  • NotifyOnValidationError

  • ValidatesOnExceptions

Note the plurals used in the name of the second attribute—it’s easy to miss (and you will currently not get any help from IntelliSense).

Example 10-13 shows the updated XAML file for this application.

Example 10-13. Two-way data binding plus validation, the XAML file (Page.xaml, project BindingValidation)

<UserControl x:Class="BindingTwoWay.Page"
    xmlns="http://schemas.microsoft.com/client/2007" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="400" Height="300">
    <Grid x:Name="LayoutRoot" Background="White">
    <Canvas>
      <Canvas Canvas.Left="15" Canvas.Top="15" x:Name="PersonPanel">
        <StackPanel Orientation="Vertical" Margin="10">
          <StackPanel Orientation="Horizontal">
            <TextBlock Text="First name: " />
            <TextBox Text="{Binding FirstName, Mode=TwoWay,
                NotifyOnValidationError=True, ValidatesOnExceptions=True}" 
                x:Name="txtFirst" />
      </StackPanel>
          <StackPanel Orientation="Horizontal">
            <TextBlock Text="Last name: " />
            <TextBox Text="{Binding LastName, Mode=TwoWay,
                NotifyOnValidationError=True, ValidatesOnExceptions=True}"
                x:Name="txtLast" />
      </StackPanel>
        </StackPanel>
      </Canvas>
      <Canvas Canvas.Left="125" Canvas.Top="85">
        <Polygon Fill="Black" Points="0, 5, 10, 0, 10, 10" 
                 MouseLeftButtonDown="prev" />
        <Polygon Fill="Black" Points="15, 0, 15, 10, 25, 5" 
                 MouseLeftButtonDown="next" />
      </Canvas>
    </Canvas>
  </Grid>
</UserControl>

Now to the C# code. The way validation works with bound data is as follows: whenever you change the data, the property’s setter is called. This is your chance to validate the new data. If the validation fails, just throw an exception; any kind of .NET exception will do (the most logical seems to be ArgumentException). For instance, this is how the FirstName property would now look:

private string firstName;
public string FirstName
{
    get
    {
        return firstName;
    }
    set
    {
        if (String.IsNullOrEmpty(value))
        {
            throw new ArgumentException();
        }
        firstName = value;
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs("FirstName"));
        }
    }
}

So when the user enters invalid data, an exception occurs; we now just have to handle it. For both text boxes, we have to set the BindingValidationError property to a new event handler. The event is fired when an exception is thrown during data binding, and the event handler may then take appropriate actions. Both text boxes will use the same event handler in our example:

txtFirst.BindingValidationError += showValidationError;
txtLast.BindingValidationError += showValidationError;

The event handler receives two arguments, just as any other .NET event handler. The type of the second argument is ValidationErrorEventArgs, and it provides access to interesting information about the validation event. The OriginalSource property points to the element that triggered the validation exception, so in our scenario you can use it to access the appropriate text box. The Action property can assume these values:

Added

A new validation error has been signaled

Removed

A previous validation error has vanished, i.e., previously incorrect input is now correct

Depending on which action has been detected, you may want to output an error message or remove a previously shown error message. Another idea would be to color-code the border or the background of the text box depending on the validity of the input: good input is denoted by green, bad input in red. In our example, we will just output an error message in the text box that caused the validation exception:

void showValidationError(object sender, ValidationErrorEventArgs e)
{
    if (e.Action == ValidationErrorEventAction.Added)
    {
        (e.OriginalSource as TextBox).Text = "*** Error ***";
    }
    e.Handled = true;
}

Notice that the code sets the Handled property to true, so that the exception does not bubble further up the element tree.

Example 10-14 shows the complete C# code for this example. You can see in Figure 10-9 what happens if you clear out one of the text boxes and then trigger the validation by tabbing out of the box: the error message is inserted.

Example 10-14. Two-way data binding plus validation, the C# file (Page.xaml.cs, project BindingValidation)

using System.Windows.Controls;
using System.Windows.Input;
using System.ComponentModel;

namespace BindingTwoWay
{
    public partial class Page : UserControl
    {
        private int pos = -1;
        private Person[] persons;

        public Page()
        {
            InitializeComponent();
            persons = new Person[] {
              new Person { FirstName = "Maria", LastName = "Anders" },
              new Person { FirstName = "Ana", LastName = "Trujillo" },
              new Person { FirstName = "Antonio", LastName = "Moreno" }
            };
            next(null, null);
            txtFirst.BindingValidationError += showValidationError;
  txtLast.BindingValidationError += showValidationError;
        }

        private void prev(object sender, MouseButtonEventArgs e)
        {
            if (pos > 0)
            {
                pos--;
            }
            bind();
        }

        private void next(object sender, MouseButtonEventArgs e)
        {
            if (pos < persons.Length - 1)
            {
                pos++;
            }
            bind();
        }

        private void bind()
        {
            PersonPanel.DataContext = persons[pos];
        }

        void showValidationError(object sender, ValidationErrorEventArgs e)
        {
            if (e.Action == ValidationErrorEventAction.Added)
            {
                (e.OriginalSource as TextBox).Text = "*** Error ***";
            }
            e.Handled = true;
            }
    }

    public class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private string firstName;
        public string FirstName
        {
            get
            {
                return firstName;
            }
            set
            {
                if (String.IsNullOrEmpty(value))
                {
                    throw new ArgumentException();
                    }
                firstName = value;
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, 
                                    new PropertyChangedEventArgs("FirstName"));
                }
            }
        }

        private string lastName;
        public string LastName
        {
            get
            {
                return lastName;
            }
            set
            {
                if (String.IsNullOrEmpty(value))
                {
                    throw new ArgumentException();
                }
                lastName = value;
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, 
                                    new PropertyChangedEventArgs("LastName"));
                }
            }
        }
    }
}
The validation feature shows an error message if a field is blanked out

Figure 10-9. The validation feature shows an error message if a field is blanked out

Silverlight’s data binding is quite powerful. Currently, not all of WPF’s data binding features have been fully ported, but the available features are very useful and future versions may add even more.

Further Reading

Programming WPF by Chris Sells and Ian Griffiths (O’Reilly)

Covers WPF data binding in great detail; many of the concepts discussed can be used in Silverlight as well

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset