Silverlight Validation in Detail

In a previous post I mentioned that Silverlight 3 has enhanced support for data entry validation. In this first of two mini-tutorials on the topic, I will take you through the process of implementing validation in some detail.
The key to understanding Silverlight validation is the division of logic from UI. In this case, the logic is delegated to the business object that the input control is bound to, and the UI is owned by the the input control (and the associated controls for displaying error conditions.)
In the case shown above, the text box into which the user is invited to type an ISBN is the input control. Not shown is a business object (also, in these cases called a data object) that holds the rules about what makes for a valid ISBN.

On the left of this diagram is the UI. It is presented to the user as a text box, and implemented using Xaml. On the right is the business logic. A business object is created, in this case as a class in C# that represents, most often, an object in the user’s domain of concern – here a book.
The business object (the book) knows what a valid ISBN is, the UI does not. The Business object determines if the value given to it by the UI is valid. If not, it throws an exception, specifying what is wrong. In this case, it might throw one of three exceptions:
- "Must be exactly 10 integers long"
- "Must be numbers or letter X"
- "Checksum is invalid!"
The UI doesn’t know what the rules are, and the Business object doesn’t know how the UI will present the problem to the user (if at all!). And that is good.
Using Binding to Mediate the Validation of the UI By the Data Object
We are already asking the Binding Object to connect the User Control to the Business object, so it is the obvious choice to also pass along the value for validation and the objects response (if any).
Normally, when you bind a property to a TextBox you would provide the Mode and the Path, in this case, you add two more properties:
1: Text="{Binding Mode=TwoWay,
2: NotifyOnValidationError=True,
3: Path=ISBN10,
4: ValidatesOnExceptions=True}"
The Business Object throws an exception if the data is not valid, and puts the reason in the Exception’s message. The Binding Object turns the exception into a message to the control, and sets the controls visual state from Valid to either InvalidUnfocused or InvalidFocused. What the control does when it changes visual state from Valid to one of the invalid states is entirely up to the designer of the control or whomever templates that control.
[We’ll look at templating error states in the second part of this mini-tutorial in a few weeks.]
For this to work, the control must bind using two way binding and it must support the new ValidationState group (you can see the validation states group very readily by beginning the templating process of any of the input controls that support validation out of the box as shown here:
As of this writing, the controls that will support validation out of the box at RTW are
- TextBox
- PasswordBox
- CheckBox
- RadioButton
- ListBox
- ComboBox
With the exception of PasswordBox, all of these already work in the Beta version.
Data Validation Step By Step
You now have all the pieces, let’s put them together step by step.
The designer sets up a binding between an input control (text box) and a data object (the Book object) ensuring that
- The binding is 2 way
- The Control has Visual state to support Validation states
- The BindingFramework knows to turn exceptions into validation state (flags)
The user is prompted to enter an ISBN into the text box. The value entered is not evaluated until one of two events; either the user leaves the text box (causing the text box to fire its TextChanged method, or the user clicks the handy Validate Now! button which invokes UpdateSource on the textBox’s TextProperty without having to actually leave the textBox.
In either case, the data is given to the Binding Framework which passes it to the Data Object for validation,
If the data is not valid, the DataObject throws an exception to the BindingFramework. The Framework turns the exception into an instruction to the input control to set its validation state to Invalid. This will kick off a storyboard, in our case turning the border red, and when the user clicks in the control bringing up the error message from the exception which is passed to the control in its error message.
[This drawing based on an original image by Karen Corby]
Steps To Creating Data Validation
There are times when I find that I follow every word of a presentation but I still have no idea how to actually do it. So here’s how.
1. Create a new project
2. Createi a business object that is going to own data validation
3. Create two way data binding between one or more input controls from the supported list above to one or more properties on your data object
4. For each control you want to validate, create a setter on the bound property that tests for the conditions you want to validate, and throws an exception if the data is not valid. In the control, be sure to set the two flags (NotifyOnValidationError=True, ValidatesOnExceptions=True).
That’s it! Honest.
Here is the complete MinPage.xaml for this example followed by the complete code behind…
1: <UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
2: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3: xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
4: xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
5: x:Class="Validation_OutOfBox.MainPage"
6: Width="600"
7: Height="250"
8: mc:Ignorable="d">
9: <Grid x:Name="LayoutRoot"
10: Background="White">
11: <Grid.RowDefinitions>
12: <RowDefinition Height="1*" />
13: <RowDefinition Height="1*" />
14: <RowDefinition Height="1*" />
15: <RowDefinition Height="1*" />
16: </Grid.RowDefinitions>
17: <Grid.ColumnDefinitions>
18: <ColumnDefinition Width="1*" />
19: <ColumnDefinition Width="1*" />
20: </Grid.ColumnDefinitions>
21: <TextBlock Text="{Binding Path=Title}"
22: HorizontalAlignment="Center"
23: VerticalAlignment="Bottom"
24: Grid.Row="0"
25: Grid.Column="0"
26: Grid.ColumnSpan="2"
27: FontFamily="Georgia"
28: FontSize="24" />
29: <TextBlock x:Name="Prompt10"
30: Text="Please enter the 10 digit ISBN"
31: TextWrapping="Wrap"
32: HorizontalAlignment="Right"
33: VerticalAlignment="Bottom"
34: FontFamily="Georgia"
35: FontSize="18"
36: Grid.Column="0"
37: Grid.Row="1"
38: Margin="5" />
39: <TextBox x:Name="TenDigits"
40: FontFamily="Georgia"
41: FontSize="18"
42: HorizontalAlignment="Left"
43: VerticalAlignment="Bottom"
44: Margin="5"
45: Width="150"
46: Height="40"
47: Grid.Column="1"
48: Grid.Row="1"
49: TextWrapping="Wrap"
50: Text="{Binding Mode=TwoWay,
51: NotifyOnValidationError=True,
52: Path=ISBN10,
53: ValidatesOnExceptions=True}" />
54:
55: <Button x:Name="FillIt"
56: Content="Fill With valid ISBN 10"
57: Width="120"
58: Height="25"
59: Grid.Column="1"
60: Grid.Row="2"
61: FontSize="12"
62: HorizontalAlignment="Left"
63: VerticalAlignment="Bottom"
64: Margin="5"
65: Background="#FF06F616" />
66: <Button x:Name="ValidateIt"
67: Content="Validate Now!"
68: Background="#FFFFFF00"
69: Width="120"
70: Height="25"
71: Grid.Column="0"
72: Grid.Row="2"
73: FontSize="12"
74: Margin="5"
75: VerticalAlignment="Bottom"
76: HorizontalAlignment="Right" />
77: </Grid>
78: </UserControl>
Here’s the complete code behind,
1: using System.Windows;
2: using System.Windows.Controls;
3: using System.Windows.Data;
4:
5: namespace Validation_OutOfBox
6: {
7: public partial class MainPage : UserControl
8: {
9: public MainPage()
10: {
11: InitializeComponent();
12: Book b = new Book();
13: b.Title = "Data Validation for Fun and Prophet";
14: LayoutRoot.DataContext = b;
15: FillIt.Click += new RoutedEventHandler( FillIt_Click );
16: ValidateIt.Click += new RoutedEventHandler( ValidateIt_Click );
17: }
18:
19: void ValidateIt_Click( object sender, RoutedEventArgs e )
20: {
21: BindingExpression bindingExpression = TenDigits.GetBindingExpression( TextBox.TextProperty );
22: bindingExpression.UpdateSource();
23: }
24:
25: void FillIt_Click( object sender, RoutedEventArgs e )
26: {
27: TenDigits.Text = "059652756X";
28: }
29: }
30: }
Finally, here is the business class/ data object
1: using System;
2: using System.ComponentModel;
3:
4: namespace Validation_OutOfBox
5: {
6: public class Book : INotifyPropertyChanged
7: {
8: public event PropertyChangedEventHandler PropertyChanged;
9:
10: private string title;
11: public string Title
12: {
13: get
14: {
15: return title;
16: }
17: set
18: {
19: title = value;
20: NotifyPropertyChanged( "Title" );
21: }
22: }
23:
24: private string isbn10;
25: public string ISBN10
26: {
27: get
28: {
29: return isbn10;
30: }
31: set
32: {
33:
34: if ( value.Length != 10 )
35: {
36: throw new ArgumentException( "Must be exactly 10 integers long" );
37: }
38:
39: char[] isbnAsArray = value.ToCharArray();
40:
41: foreach ( char c in isbnAsArray )
42: {
43: if ( ( !Char.IsNumber( c ) ) && c.ToString().ToUpper() != "X" )
44: {
45: throw new ArgumentException( "Must be numbers or letter X" );
46: }
47: }
48:
49: int runningTotal = 0;
50: for ( int i = 0; i < 9; i++ )
51: {
52: int val = ( Convert.ToInt32(isbnAsArray
.ToString())
53: * ( 10 - i ) );
54: runningTotal += val;
55: }
56: int mod = runningTotal % 11;
57: int checkSum = 11 - mod;
58:
59: int isbnCheckSum = -1;
60: if ( isbnAsArray[9].ToString().ToUpper() == "X" )
61: isbnCheckSum = 10;
62: else
63: isbnCheckSum = Convert.ToInt32(isbnAsArray[9].ToString());
64:
65: if ( isbnCheckSum != checkSum )
66: {
67: throw new ArgumentException( "Checksum is invalid!" );
68: }
69:
70: isbn10 = value;
71: NotifyPropertyChanged( "ISBN10" );
72: }
73: }
74:
75: private void NotifyPropertyChanged( String propertyName )
76: {
77: if ( PropertyChanged != null )
78: {
79: PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
80: }
81: }
82: }
83: }
That’s it!
Example-Code Quality, Guaranteed! This code was compiled with Silverlight 3 (Beta) using Visual Studio 8 SP1 on Windows 7 (RC) and also on Silverlight 3 (Beta) using Visual Studio 10 (Beta) on Windows 7 (RC). For more on this guarantee, please see this page.