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;
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;
Note the Submit button’s disabled because of the error on the Age field.