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.
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; } } }
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
Canvas
’ DataContext
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; } } }
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:
PersonPanel.DataContext = new Person { FirstName = "Maria", LastName = "Anders" };
Binding b1 = new System.Windows.Data.Binding("FirstName");
b1.Mode = System.Windows.Data.BindingMode.OneTime;
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.
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.
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.
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:
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:
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.
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.
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).
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:
A new validation error has been signaled
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")); } } } } }
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.
Covers WPF data binding in great detail; many of the concepts discussed can be used in Silverlight as well