A quick look at Silverlight 3: Data Validation

In Silverlight 2, if I’ve got a UI like this one which is just 3 TextBoxes bound to 3 data properties;

<StackPanel
    x:Name="LayoutRoot"
    Background="White">
    <TextBox
        Margin="5"
        Grid.Column="1"
        VerticalAlignment="Center"
        Width="192"
        Text="{Binding FirstName,Mode=TwoWay}" />
    <TextBox
        Margin="5"
        Grid.Column="1"
        VerticalAlignment="Center"
        Grid.Row="1"
        Width="192"
        Text="{Binding LastName,Mode=TwoWay}" />
    <TextBox
        Margin="5"
        Grid.Column="1"           
        VerticalAlignment="Center"
        Width="192"
        Text="{Binding Account,Mode=TwoWay}"
        Grid.Row="2" />
</StackPanel>

and I’ve set the DataContext for the StackPanel so that it binds to an instance of my Person class;

  public class Person 
  {
    public Person(string firstName, string lastName, int account)
    {
      this.FirstName = firstName;
      this.LastName = lastName;
      this.Account = account;
    }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Account { get; set; }
  }

then property getters/setters will get called automatically but if I wanted to do any validation around the values that come from the UI then I’ve got to take a custom approach because any exceptions that are thrown from the setters above will be swallowed and lost.

In Silverlight 3, I can have my validation in my setters (e.g.);

 public class InvalidDataException : Exception
  {
    public InvalidDataException(string msg) : base(msg)
    {
    }
  }
  public class Person 
  {
    public Person(string firstName, string lastName, int account)
    {
      this.firstName = firstName;
      this.lastName = lastName;
      this.Account = account;
    }
    public string FirstName
    {
      get
      {
        return (firstName);
      }
      set
      {
        CheckFieldLength(value, 5, 10);
        firstName = value;
      }
    }
    public string LastName
    {
      get
      {
        return (lastName);
      }
      set
      {
        CheckFieldLength(value, 5, 10);
        lastName = value;
      }
    }
    void CheckFieldLength(string value, int min, int max)
    {
      if ((value.Length < min) || (value.Length > max))
      {
        throw new InvalidDataException("Bad length on that string");
      }
    }
    public int Account
    {
      get
      {
        return (account);
      }
      set
      {
        if ((value < 12340000) || (value > 12349999))
        {
          throw new InvalidDataException("Account number is invalid");
        }
        account = value;
      }
    }
    string firstName;
    string lastName;
    int account;
  }

and then if I update my UI so that my bindings have a new ValidatesOnExceptions property then all kinds of new and exciting stuff kicks into gear;

<StackPanel

    x:Name="LayoutRoot"

    Background="White">

    <TextBox

        Margin="5"

        Grid.Column="1"

        VerticalAlignment="Center"

        Width="192"

        Text="{Binding FirstName,Mode=TwoWay,ValidatesOnExceptions=True}" />

    <TextBox

        Margin="5"

        Grid.Column="1"

        VerticalAlignment="Center"

        Grid.Row="1"

        Width="192"

        Text="{Binding LastName,Mode=TwoWay,ValidatesOnExceptions=True}" />

    <TextBox

        Margin="5"

        Grid.Column="1"           
        VerticalAlignment="Center"

        Width="192"

        Text="{Binding Account,Mode=TwoWay,ValidatesOnExceptions=True}"

        Grid.Row="2" />

</StackPanel>

image

and so we’re getting automatic (and, naturally, customisable) error display UI on controls like TextBox here. There are new attached properties on a static class called Validation called ErrorsProperty and HasErrorProperty and helper methods Validation.GetErrors() and Validation.GetHasError() which can obtain the set of errors that have occurred in pushing a UI value through into its associated property ( note – there’s a new for “errors” rather than “error” here because it’s possible to have different properties of a UI control bound to more than one property on a data class ).

There’s also a new routed event defined which is BindingValidationError and it’s defined on FrameworkElement. As far as I can tell, this event only gets fired if we switch on another flag called NotifyOnValidationError as in;

<StackPanel

        x:Name="LayoutRoot"

        Background="White"

        BindingValidationError="OnValidationError">

        <TextBox

            Margin="5"

            Grid.Column="1"

            VerticalAlignment="Center"

            Width="192"

            Text="{Binding FirstName,Mode=TwoWay,ValidatesOnExceptions=True,NotifyOnValidationError=True}" />

        <TextBox

            Margin="5"

            Grid.Column="1"

            VerticalAlignment="Center"

            Grid.Row="1"

            Width="192"

            Text="{Binding LastName,Mode=TwoWay,ValidatesOnExceptions=True,NotifyOnValidationError=True}" />

        <TextBox

            Margin="5"

            Grid.Column="1"           
            VerticalAlignment="Center"

            Width="192"

            Text="{Binding Account,Mode=TwoWay,ValidatesOnExceptions=True,NotifyOnValidationError=True}"

            Grid.Row="2" />

    </StackPanel>

and then I can add a handler;

    private void OnValidationError(object sender, ValidationErrorEventArgs e)
    {
      switch (e.Action)
      {
        case ValidationErrorEventAction.Added:
          Exception ex = e.Error.Exception;
          break;
        case ValidationErrorEventAction.Removed:
          break;
        default:
          break;
      }
    }

It’s interesting in that this handler gets called as validation problems are encountered and also as they are resolved as you can see with the ValidationErrorEventAction.Added and Removed enumerations.

At the time of writing, I’m not sure if there’s an automatic way to determine all validation errors that in play for the children of a particular containing piece of UI like a Panel. That is – I could use the event handler above to build a list of errors and then data-bind to it as in;

<UserControl

    x:Class="SilverlightApplication16.MainPage"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="clr-namespace:SilverlightApplication16">

    <StackPanel

        x:Name="LayoutRoot"

        Background="White"

        BindingValidationError="OnValidationError">

        <TextBox

            Margin="5"

            Grid.Column="1"

            VerticalAlignment="Center"

            Width="192"

            Text="{Binding FirstName,Mode=TwoWay,ValidatesOnExceptions=True,NotifyOnValidationError=True}" />

        <TextBox

            Margin="5"

            Grid.Column="1"

            VerticalAlignment="Center"

            Grid.Row="1"

            Width="192"

            Text="{Binding LastName,Mode=TwoWay,ValidatesOnExceptions=True,NotifyOnValidationError=True}" />

        <TextBox

            Margin="5"

            Grid.Column="1"           
            VerticalAlignment="Center"

            Width="192"

            Text="{Binding Account,Mode=TwoWay,ValidatesOnExceptions=True,NotifyOnValidationError=True}"

            Grid.Row="2" />

        <ListBox

            x:Name="lstErrors"

            ItemsSource="{Binding Errors}">

            <ListBox.ItemTemplate>

                <DataTemplate>

                    <TextBlock

                        Text="{Binding Exception.Message}" />

                </DataTemplate>

            </ListBox.ItemTemplate>

        </ListBox>

    </StackPanel>

</UserControl>

with code (which should probably also set the Handled property on the ValidationErrorEventArgs);

  public partial class MainPage : UserControl
  {
    public MainPage()
    {
      InitializeComponent();
      this.Errors = new ObservableCollection<ValidationError>();
      this.Loaded += OnLoaded;
    }
    void OnLoaded(object sender, RoutedEventArgs e)
    {
      this.DataContext = new Person("Fred", "Smith", 12345555);
      lstErrors.DataContext = this;
    }
    private void OnValidationError(object sender, ValidationErrorEventArgs e)
    {
      switch (e.Action)
      {
        case ValidationErrorEventAction.Added:
          Errors.Add(e.Error);
          break;
        case ValidationErrorEventAction.Removed:
          Errors.Remove(e.Error);
          break;
        default:
          break;
      }
    }
    public ObservableCollection<ValidationError> Errors { get; set; }
  }

and that gives me a UI like;

image

with my ListBox at the bottom ( note – I’m not sure that adding/removing those ValidationErrors to that ObservableCollection up above is legit as I think I’m relying on the same object being passed to my event handler each time to represent the same error and that might not be the case ).

Alternatively, rather than building up a list as the errors happen, I could gather the errors from my individual controls when (e.g.) someone clicks a “Submit” button as in;

<UserControl

    x:Class="SilverlightApplication16.MainPage"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="clr-namespace:SilverlightApplication16">

    <StackPanel

        x:Name="LayoutRoot"

        Background="White">

        <TextBox

            Margin="5"

            Grid.Column="1"

            VerticalAlignment="Center"

            Width="192"

            Text="{Binding FirstName,Mode=TwoWay,ValidatesOnExceptions=True,NotifyOnValidationError=True}" />

        <TextBox

            Margin="5"

            Grid.Column="1"

            VerticalAlignment="Center"

            Grid.Row="1"

            Width="192"

            Text="{Binding LastName,Mode=TwoWay,ValidatesOnExceptions=True,NotifyOnValidationError=True}" />

        <TextBox

            Margin="5"

            Grid.Column="1"           
            VerticalAlignment="Center"

            Width="192"

            Text="{Binding Account,Mode=TwoWay,ValidatesOnExceptions=True,NotifyOnValidationError=True}"

            Grid.Row="2" />

        <StackPanel>

        <ListBox

            x:Name="lstErrors"

            ItemsSource="{Binding}">

            <ListBox.ItemTemplate>

                <DataTemplate>

                    <TextBlock

                        Text="{Binding Exception.Message}" />

                </DataTemplate>

            </ListBox.ItemTemplate>

        </ListBox>

            <Button

                Content="Submit"

                Click="OnSubmit" />

        </StackPanel>

    </StackPanel>

</UserControl>

with code;

public partial class MainPage : UserControl

{

  public MainPage()

  {

    InitializeComponent();

    this.Loaded += OnLoaded;

  }

  void OnLoaded(object sender, RoutedEventArgs e)

  {

    this.DataContext = new Person("Fred", "Smith", 12345555);

    lstErrors.DataContext = this;

  }

  void OnSubmit(object sender, RoutedEventArgs args)

  {

    List<ValidationError> errors = new List<ValidationError>();

    foreach (UIElement ui in LayoutRoot.Children)

    {

      FrameworkElement fe = ui as FrameworkElement;

      if (fe != null)

      {

        foreach (ValidationError ve in Validation.GetErrors(fe))

        {

          errors.Add(ve);

        }

      }

    }

    lstErrors.DataContext = errors;

  }

}

and that builds up my error list ( in addition to the standard UI ) at the point where I click the Submit button;

image

I’ll update the post if I learn how to get a list of errors for all children from a containing Panel.

In terms of customising the UI for the error display – the UI is built into the Template of controls such as TextBox, CheckBox, RadioButton, ListBox, ComboBox and so on so you’d want to use Blend to alter the template and customise the error display.

You can download the code for this post from here.

This is one of a series of posts taking a quick look at Silverlight 3 – expect them to be a little “rough and ready” and that they’ll get expanded on as I’ve had more time to work with Silverlight 3.