Silverlight 4 Rough Notes: Binding and IDataErrorInfo

Note – these posts are put together after a short time with Silverlight 4 as a way of providing pointers to some of the new features that Silverlight 4 has to offer. I’m posting these from the PDC as Silverlight 4 is announced for the first time so please bear that in mind when working through these posts.

Silverlight 3 introduced the idea of types throwing exceptions from their setters as a way of reporting errors back to the UI. That works well but there are other ways of achieving that kind of error status from a type and the IDataErrorInfo interface has been around for a while as a way of doing this ( introduced in Windows Forms and then added to WPF V3.5 at a later point ).

It’s a simple enough interface;

  // Summary:
  //     Defines properties that data entity classes can implement to provide custom
  //     validation support.
  public interface IDataErrorInfo
  {
    // Summary:
    //     Gets a message that describes any validation errors for the object.
    //
    // Returns:
    //     The validation error on the object, or null or System.String.Empty if there
    //     are no errors present.
    string Error { get; }

    // Summary:
    //     Gets a message that describes any validation errors for the specified property
    //     or column name.
    //
    // Parameters:
    //   columnName:
    //     The name of the property or column to retrieve validation errors for.
    //
    // Returns:
    //     The validation error on the specified property, or null or System.String.Empty
    //     if there are no errors present.
    string this[string columnName] { get; }
  }

whereby a caller can enquire about the general state of an object or about specific properties. Lots of people have implemented this in the past for Windows Forms applications and might;

  • be very familiar with the interface so want to keep using it
  • want to avoid the model of “have to throw an exception in a setter in order to validate the object”
  • have a bunch of code that already uses IDataErrorInfo and want to keep using it

and so Silverlight 4 adds support for IDataErrorInfo. I can go and make myself a pretend class like this one;

  public class Person : IDataErrorInfo
  {
    public string FirstName
    {
      get
      {
        return (firstName);
      }
      set
      {
        firstName = value;
      }
    }
    public string LastName
    {
      get
      {
        return (lastName);
      }
      set
      {
        lastName = value;
      }
    }
    public int Age
    {
      get
      {
        return (age);
      }
      set
      {
        age = value;
      }
    }
    [Display(AutoGenerateField=false)]
    public string Error
    {
      get { return (null); }
    }
    [Display(AutoGenerateField = false)]
    public string this[string columnName]
    {
      get
      {
        string error = null;

        switch (columnName)
        {
          case "FirstName":
            if (string.IsNullOrEmpty(firstName))
            {
              error = "Provide a first name";
            }
            break;
          case "LastName":
            if (string.IsNullOrEmpty(lastName))
            {
              error = "Provide a last name";
            }
            break;
          case "Age":
            if ((age < 0) || (age > 120))
            {
              error = "Age out of range";
            }
            break;
        }
        return (error);
      }
    }
    string firstName;
    string lastName;
    int age;
  }

then I can wrap it up into a UI by feeding it as the DataContext of a DataGrid as below;


  public partial class MainPage : UserControl
  {
    public MainPage()
    {
      InitializeComponent();

      this.Loaded += (s, e) =>
        {
          this.DataContext = 
            new Person[] { new Person() { FirstName = "Fred", LastName = "Smith", Age = 22 } };
        };
    }
  }

with the corresponding XAML file;

<UserControl x:Class="SilverlightApplication25.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:dg="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">

    <Grid x:Name="LayoutRoot" Background="White">
        <dg:DataGrid
            ItemsSource="{Binding}" />
    </Grid>
</UserControl>

and that gives me a UI that knows about its errors as in;

image

Naturally – this isn’t just a DataGrid thing, it applies to regular controls and DataForms and so on and should help in getting a bunch of code across to Silverlight 4. In terms of making use from regular controls, if we switch our UI to be something like;

<UserControl
    x:Class="SilverlightApplication25.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400">
    <UserControl.Resources>
        <Style
            TargetType="Button">
            <Setter
                Property="Margin"
                Value="5" />
        </Style>
        <Style
            TargetType="TextBlock">
            <Setter
                Property="Margin"
                Value="5" />
        </Style>
        <Style
            TargetType="TextBox">
            <Setter
                Property="Margin"
                Value="5" />
        </Style>
    </UserControl.Resources>

    <StackPanel
        x:Name="LayoutRoot"
        Background="White"
        BindingValidationError="OnValidationError">
        <StackPanel
            Orientation="Horizontal">
            <TextBlock
                Text="First Name " />
            <TextBox
                MinWidth="192"
                Text="{Binding Person.FirstName,Mode=TwoWay,ValidatesOnDataErrors=True,NotifyOnValidationError=True}" />            
        </StackPanel>
        <StackPanel
            Orientation="Horizontal">
            <TextBlock
                Text="Last Name " />
            <TextBox
                MinWidth="192"
                Text="{Binding Person.LastName,Mode=TwoWay,ValidatesOnDataErrors=True,NotifyOnValidationError=True}" />
        </StackPanel>
        <StackPanel
            Orientation="Horizontal">
            <TextBlock
                Text="Age " />
            <TextBox
                MinWidth="192"
                Text="{Binding Person.Age,Mode=TwoWay,ValidatesOnDataErrors=True,NotifyOnValidationError=True}" />
        </StackPanel>
        <Button
            Content="Submit" IsEnabled="{Binding NoErrors}"/>
    </StackPanel>
</UserControl>

note that I’ve set the ValidatesOnDataErrors property of these TextBoxes to be True which causes them to do the right thing around IDataErrorInfo. I’ve also set the NotifyOnValidationError but that’s a Silverlight 3 thing and I’m really using that to count errors as they occur and get fixed so that I can enable/disable my Submit button based on that.

The code behind this I changed to;

  public partial class MainPage : UserControl, INotifyPropertyChanged
  {
    public MainPage()
    {
      InitializeComponent();

      this.person = new Person() { FirstName = "Fred", LastName = "Smith", Age = 22 };

      this.Loaded += (s, e) =>
        {
          this.DataContext = this;            
        };
    }
    public Person Person
    {
      get
      {
        return (person);
      }
    }
    public bool NoErrors
    {
      get
      {
        return (errorCount == 0);
      }
    }
    void FirePropertyChanged(string property)
    {
      if (PropertyChanged != null)
      {
        PropertyChanged(this, new PropertyChangedEventArgs(property));
      }
    }   
    private void OnValidationError(object sender, ValidationErrorEventArgs e)
    {
      switch (e.Action)
      {
        case ValidationErrorEventAction.Added:
          errorCount++;
          break;
        case ValidationErrorEventAction.Removed:
          errorCount--;
          break;
        default:
          break;
      }
      FirePropertyChanged("NoErrors");
    }
    int errorCount;
    Person person;
    public event PropertyChangedEventHandler PropertyChanged;
  }

and most of that is really around having a NoErrors property to enable/disable the button – the bits around IDataErrorInfo are as they were in the DataGrid example with the one exception of using the new NotifyOnValidationError on the binding directive and that gives me a UI made up of TextBoxes with the validation display being pretty much the same;

image

Note the Submit button’s disabled because of the error on the Age field.