Powered By Blogger

Tuesday, October 12, 2010

Binding and IDataErrorInfo in Silverlight 4

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;

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;

No comments:

Post a Comment