Manish Dalal's blog

Exploring .net!

Silverlight Business Application Part 3: Validation (sync)

This is part three of Building Business Application with Silverlight series that showcases the basic building blocks of a data centric application.  

Series Link: Part 0, Part 1, Part 2

Almost all business application will have varying degree of validations. For our example, let say we have a business rule that states that Age must be a number and be greater than 0 and less than 200.

DataGrid already prevents non numeric entry in Age column, thanks to backing field of type integer. Behind the scene, DataGrid uses System.Convert by way of DataGridValueConverter to coerce values to proper type.

Try to change age to a non number, you will see that as soon as you exit cell, it reverts back to old values. This is because DataGrid tries to convert string to number and when it fails (FormatException), it reverts data back to original value. (If you examine Output window of Visual Studio Editor you will find the message: A first chance exception of type 'System.FormatException' occurred in mscorlib.dll)

You can see this by turning on two attributes on the binding, ValidatesOnExceptions and NotifyOnValidationError. Setting ValidatesOnExceptions to true tells the binding engine to create a validation error when an exception occurs and setting NotifyOnValidationError to true tells the binding engine to raise the BindingValidationError event when a validation error occurs.

Change the DataGrid declaration from

<data:DataGrid x:Name="peopleDataGrid" Grid.Row="1" />

to

<data:DataGrid 
    x:Name="peopleDataGrid"
    AutoGenerateColumns="False"
    Grid.Row="1"
    >
    <data:DataGrid.Columns>
        <data:DataGridTextColumn 
            Header="First Name"
            DisplayMemberBinding="{Binding FirstName}"
            />
        <data:DataGridTextColumn 
            Header="Last Name" 
            DisplayMemberBinding="{Binding LastName}"
            />
        <data:DataGridTemplateColumn
            Header="Age"
            >
            <data:DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock 
                        Text="{Binding Age}"
                        />
                </DataTemplate>
            </data:DataGridTemplateColumn.CellTemplate>
            <data:DataGridTemplateColumn.CellEditingTemplate>
                <DataTemplate>
                    <TextBox
                        Text="{Binding Path=Age,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}"
                        />
                </DataTemplate>
            </data:DataGridTemplateColumn.CellEditingTemplate>
        </data:DataGridTemplateColumn>
        <data:DataGridTextColumn
            Header="City"
            DisplayMemberBinding="{Binding City}"
            />
    </data:DataGrid.Columns>
</data:DataGrid>

Now setup event handler for BindingValidationError on the peopleDatagrid in the page.xaml.cs file as

public Page() {
    InitializeComponent();
    this.Loaded += new RoutedEventHandler(Page_Loaded);
    //this.addButton.Click += new RoutedEventHandler(addButton_Click);
    this.peopleDataGrid.KeyDown += new KeyEventHandler(peopleDataGrid_KeyDown);
    this.deleteButton.Click += new RoutedEventHandler(deleteButton_Click);
    this.peopleDataGrid.BindingValidationError += new EventHandler<ValidationErrorEventArgs>(peopleDataGrid_BindingValidationError);
}

void peopleDataGrid_BindingValidationError(object sender, ValidationErrorEventArgs e) {
    System.Diagnostics.Debug.WriteLine(e.Error.Exception.Message);
}

F5 and run the application. Enter non numeric value in Age field.

If you examine the “Output” window, you will see that we are getting exception - Input string was not in a correct format..

image

image

We can use above event handler to provide user with more descriptive error message as well as visual feedback. Add following code to BindingValidationError event handler

void peopleDataGrid_BindingValidationError(object sender, ValidationErrorEventArgs e) {
    System.Diagnostics.Debug.WriteLine(e.Error.Exception.Message);
    if (e.Action == ValidationErrorEventAction.Added) {
        ((Control)e.Source).Background = new SolidColorBrush(Colors.Red);
        this.Dispatcher.BeginInvoke(() => HtmlPage.Window.Alert(e.Error.Exception.Message));
    } else if (e.Action == ValidationErrorEventAction.Removed) {
        ((Control)e.Source).Background = new SolidColorBrush(Colors.Red);
    }
}

Now when you try to enter string, our code gets the notification, user is presented with a alter box and color of cell changes to red to indicate error.

image

You can use Exception type and Source to differentiate between different columns and customize the message. Change BindingValidationError event handler as shown

void peopleDataGrid_BindingValidationError(object sender, ValidationErrorEventArgs e) {
    System.Diagnostics.Debug.WriteLine(e.Error.Exception.Message);
    if (e.Action == ValidationErrorEventAction.Added) {
        ((Control)e.Source).Background = new SolidColorBrush(Colors.Red);
        string message = e.Error.Exception.Message;
        if (e.Error.Exception is System.FormatException
            && "Age" == ((Control)e.Source).Tag.ToString()) {
            message = "Age must be a number between 0 and 200";
        }
        this.Dispatcher.BeginInvoke(() => HtmlPage.Window.Alert(message));
    } else if (e.Action == ValidationErrorEventAction.Removed) {
        ((Control)e.Source).Background = new SolidColorBrush(Colors.White);
    }
}

Also change xaml CellEditingTemplate for Age as follows

<data:DataGridTemplateColumn.CellEditingTemplate>
    <DataTemplate>
        <TextBox
            Text="{Binding Path=Age,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}"
            Tag="Age"
            />
    </DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>
 
Notice addition of Tag to identify Age column. If you now try to enter non number in Age field, you get custom error message

image

Let consider domain validation for age. It should not be negative and between 0 and 200. Lets add that validation. Most logical place is setter of Age property in Person class. Change Age setter as

public int Age {
    get { return _age; }
    set {
        if (value == _age) return;
        if (value < 0 || value > 200) {
            throw new Exception("Age must be between 0 and 200");
        } 
        _age = value;
        OnPropertyChanged("Age");
    }
}

Here we are checking to make sure that age is not negative and less than 200. If not, we throw exception.

image

One alternative to disruptive alert message box is to use tooltip. Change BindingValidationError event handler as shown

private void peopleDataGrid_BindingValidationError(object sender, ValidationErrorEventArgs e) {
    if (e.Action == ValidationErrorEventAction.Added) {
        ((Control)e.Source).Background = new SolidColorBrush(Colors.Red);
        ((Control)e.Source).SetValue(ToolTipService.ToolTipProperty, e.Error.Exception.Message);
    } else if (e.Action == ValidationErrorEventAction.Removed) {
        ((Control)e.Source).Background = new SolidColorBrush(Colors.White);
        ((Control)e.Source).SetValue(ToolTipService.ToolTipProperty, null);
    }
}

Also change xaml CellEditingTemplate for Age as follows

<data:DataGridTemplateColumn.CellEditingTemplate>
    <DataTemplate>
        <TextBox
            Text="{Binding Path=Age,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}"
            ToolTipService.ToolTip="Please provide Age between 0 and 200"
            />
    </DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>

Notice addition of ToolTipService.ToolTip to Age column. This provides user with nice help when Age goes in edit mode

image

And when business rules fails

image

It will be nice to prevent user form entering string or negative values in first place. You can do that using AttachedProperties to extend control behavior (similar to AjaxControlToolkit FilterTextboxExtender), but that’s topic for another post!

Above takes care of all sync validations that provide user with immediate feedback. However, what about async validation? We will tackle that next!

Comments

ver_bal said:

Nice!

# August 28, 2008 12:47 PM

jemiller said:

I'm basically trying the same thing, only I'm running into a problem where if I try to set the BindingValidationError event handler on the DataGrid rather than on a specific control in the CellEditingTemplate, it never gets called. The event is raised if I set the BindingValidationError event handler on the specific control within the DataGrid that is doing the editing.

# September 11, 2008 5:32 PM

Joe Robe said:

This does NOT work on RC. The event does not get fired on the grid, it gets fired on the text box.

Joe

# October 28, 2008 1:01 PM

imd said:

((Control)e.Source).Background is not compilying. Getting the following error

Error 1 'System.Windows.Controls.ValidationErrorEventArgs' does not contain a definition for 'Source' and no extension method 'Source' accepting a first argument of type 'System.Windows.Controls.ValidationErrorEventArgs' could be found (are you missing a using directive or an assembly reference?) C:\TestSample\TestSample\SLSample\SLSample\DataBinding\DataBindingWithGrid.xaml.cs 38 29 SLSample

Can you please tell me which assembly i need to reference.

Thanks

# January 22, 2009 2:42 PM

... said:

Dies ist ein gro�er Ort. Ich m�chte hier noch einmal.

# March 8, 2009 1:17 AM

haritha said:

Hi Manish,

I am using a datagrid to display some objects.I have bound the datagrid to a list. In order to do data validation I have set both NotifyValidationException=true and BindingValidationError=true. I have set up an Binding Validation error event handler.

I have followed the same steps as per your blog.But in my code Binding Validation error event is not raised at all. The Exception is displayed in the next line to the code where I have specified to throw the exception.

1. set

2. {

3.    if (value == null || value == string.Empty)

4.    {

5.        throw new Exception("Description can't be null or empty!");

6.    }

7.    _description = value;

8.    OnPropertyChanged("Description");

9.  }

The Exception is displayed at line no 7. Can you help me out in this issue.

# July 2, 2009 2:39 AM