This is part five 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, Part 3, Part 4
So far we have seen how to validate data in sync and async fashion. We did validation in response to the changes to data, in setter of the fields. This works fine for doing individual field validation, but we also need a way to validate and track state of the entire object.
Rather than crowding the domain model, the Person class with validation and error state management logic, we will factor out validation into a separate class, PersonValidator. PersonValidator will house logic to validate individual fields and also provide method to validate entire object on demand, not just in response to changes to the data field. It will also be responsible for maintaining overall state of the object.
PersonValidator
Add a new class called PersonValidator.cs to the Silverlight project. Copy over validation code from Person.cs as shown.
using Silverlight.CityServiceReference;
using System.ComponentModel;
namespace Silverlight {
public class PersonValidator : INotifyPropertyChanged {
private bool _invalidAge;
public bool InvalidAge { get { return _invalidAge; } set { if (_invalidAge == value) return;
_invalidAge = value;
OnPropertyChanged("InvalidAge"); }
}
public void ValidateAge(int newValue) { InvalidAge = (newValue < 0 || newValue > 200);
}
private bool _invalidLastName;
public bool InvalidLastName { get { return _invalidLastName; } set { if (_invalidLastName == value) return;
_invalidLastName = value;
OnPropertyChanged("InvalidLastName"); }
}
public void ValidateLastName(string newValue) { if (string.IsNullOrEmpty(newValue)) { InvalidLastName = true;
return;
}
InvalidLastName = (0 == newValue.Trim().Length);
}
private bool _invalidCity;
public bool InvalidCity { get { return _invalidCity; } set { if (_invalidCity == value) return;
_invalidCity = value;
OnPropertyChanged("InvalidCity"); }
}
private bool _isValid;
public bool IsValid { get { return _isValid;
}
set { if (_isValid == value) return;
_isValid = value;
OnPropertyChanged("IsValid"); }
}
private void ValidateCity(string city) { if (string.IsNullOrEmpty(city)) { InvalidCity = true;
return;
}
CityServiceClient proxy = new CityServiceClient();
proxy.IsCityValidCompleted += new EventHandler<IsCityValidCompletedEventArgs>(proxy_IsCityValidCompleted);
((Page)Application.Current.RootVisual).StartWait("Please wait, validating City"); proxy.IsCityValidAsync(city, city);
}
void proxy_IsCityValidCompleted(object sender, IsCityValidCompletedEventArgs e) { ((Page)Application.Current.RootVisual).EndWait(null);
InvalidCity = !e.Result;
}
Person _data;
public PersonValidator(Person data) { _data = data;
_data.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(_data_PropertyChanged);
// default valid
_isValid = true;
}
public PersonValidator(Person data, bool defaultInvalid) : this(data) { if (defaultInvalid) { _invalidAge = true;
_invalidLastName = true;
_invalidCity = true;
_isValid = false;
}
}
void _data_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { if (e.PropertyName == "IsValid") { return;
}
switch (e.PropertyName) { case "Age":
ValidateAge(_data.Age);
break;
case "LastName":
ValidateLastName(_data.LastName);
break;
case "City":
ValidateCity(_data.City);
break;
}
}
public void Validate() { ValidateAge(_data.Age);
ValidateLastName(_data.LastName);
ValidateCity(_data.City);
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name) { if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
//
IsValid = !(_invalidAge || _invalidLastName || _invalidCity);
}
#endregion
}
}
Here we are validating three properties:
- Age - must be greater than 0 and less then 200
- LastName - required field (can not be null or empty)
- City - must be from valid list of cities (validated using web service).
We are tracking each field individually and also tracking overall object status using IsValid property.
Now that we have PersonValidator, we have two options for validating data
Option 1 - Manually
From each field setter in Person class, call corresponding validation method in PersonValidator class. This has advantage of being able to throw exception if validation fails and hence using the built in support for basic data validation.
Option 2 - Automatically
Validate data in response to data change notification. This has advantage of being able to change validation without affecting main domain model. We can wire up new field to validate and/or change existing rules, all without changing Person class. (Consider new validation rule that was added for LastName)
While option 1 may seems easier, it relies on exception mechanism which is only available for immediate sync validation. We will use option 2, as it provides consistent behavior across both sync and async validation. Another alternative will be to use a variation of option 1 that does not rely on exception. Main difference is who is in charge, in option 2, PersonValidator works semi autonomously where as in other options, Person class is in charge, directing when/what to validate.
In order to automate validation process, PersonValidator subscribes to PropertyChanged event from Person. Whenever any of the Person property to be validated changes, Person class fires property change event notification, and data is validated by PersonValidator in property changed event handler. PersonValidator in turn provides valid/invalid notification, which can be used to change the UI.
Now add Personvalidator variable declaration to the Person class and initialize it with object to validate(this) as shown. (Remember to comment out validation logic in Person.cs.)
using Silverlight.CityServiceReference;
using System.Windows.Browser;
namespace Silverlight { public class Person : INotifyPropertyChanged {
PersonValidator _validator;
#region Constructors
public Person() { _validator = new PersonValidator(this);
}
public Person(string firstName, string lastName, int age, string city): this() { this._firstName = firstName;
this._lastName = lastName;
this._age = age;
this._city = city;
}
#endregion
#region Properties
private string _firstName;
public string FirstName { get { return _firstName; } set { if (value == _firstName) return;
_firstName = value;
OnPropertyChanged("FirstName"); }
}
private string _lastName;
public string LastName { get { return _lastName; } set { if (value == _lastName) return;
_lastName = value;
OnPropertyChanged("LastName"); }
}
private int _age;
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"); }
}
private string _city;
public string City { get { return _city; } set { if (value == _city) return;
_city = value;
OnPropertyChanged("City"); //ValidateCity(_city);
}
}
//private void ValidateCity(string city) { // CityServiceClient proxy = new CityServiceClient();
// proxy.IsCityValidCompleted += new EventHandler<IsCityValidCompletedEventArgs>(proxy_IsCityValidCompleted);
// ((Page)Application.Current.RootVisual).StartWait("Please wait, validating City"); // proxy.IsCityValidAsync(city, city);
//}
//void proxy_IsCityValidCompleted(object sender, IsCityValidCompletedEventArgs e) { // ((Page)Application.Current.RootVisual).EndWait(null);
// if (!e.Result) { // HtmlPage.Window.Alert(e.UserState.ToString() + " is not valid, please correct...");
// }
//}
public PersonValidator Validator { get { return this._validator;
}
}
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name) { if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
#endregion
}
}
We are creating instance of PersonValidator in constructor of Person class and also exposing it has Validator property for data binding. With above in place, it is time to rewire DataGrid to display validation results. Change Page.xaml to following.
<UserControl x:Class="Silverlight.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
xmlns:src="clr-namespace:Silverlight"
>
<UserControl.Resources>
<src:InvalidToBrushConverter x:Key="brushConverter"/>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White" Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<!--<Button x:Name="addButton" Content="Add" Margin="5"/>-->
<Button x:Name="deleteButton" Content="Delete" Margin="5"/>
</StackPanel>
<!--<data:DataGrid x:Name="peopleDataGrid" Grid.Row="1" />-->
<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="Last Name"
>
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Background="{Binding Path=Validator.InvalidLastName, Converter={StaticResource brushConverter}}"> <TextBlock
Text="{Binding LastName}" />
</Border>
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
<data:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox
Text="{Binding Path=LastName,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}" Background="{Binding Path=Validator.InvalidLastName, Converter={StaticResource brushConverter}}" />
</DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>
</data:DataGridTemplateColumn>
<data:DataGridTemplateColumn
Header="Age"
>
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Background="{Binding Path=Validator.InvalidAge, Converter={StaticResource brushConverter}}"> <TextBlock
Text="{Binding Age}" />
</Border>
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
<data:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox
Text="{Binding Path=Age,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}" ToolTipService.ToolTip="Please provide Age between 0 and 200"
Background="{Binding Path=Validator.InvalidAge, Converter={StaticResource brushConverter}}" />
</DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>
</data:DataGridTemplateColumn>
<!--<data:DataGridTextColumn
Header="City"
DisplayMemberBinding="{Binding City}" />-->
<data:DataGridTemplateColumn
Header="City"
>
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Background="{Binding Path=Validator.InvalidCity, Converter={StaticResource brushConverter}}"> <TextBlock
Text="{Binding City}" />
</Border>
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
<data:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox
Text="{Binding Path=City,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}" Background="{Binding Path=Validator.InvalidCity, Converter={StaticResource brushConverter}}" />
</DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>
</data:DataGridTemplateColumn>
</data:DataGrid.Columns>
</data:DataGrid>
<src:WaitControl Grid.RowSpan="2" x:Name="waitControl" Visibility="Collapsed"/>
</Grid>
</UserControl>
Note that we have added xmlns:src="clr-namespace:Silverlight" namespace declaration and also added InvalidToBrushConverter as a static resource. InvalidToBrushConverter is used to change background color to Red when validation fails. Add new class called InvalidToBrushConverter.cs as follows.
public class InvalidToBrushConverter : IValueConverter {
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
return new SolidColorBrush(((bool)value ? Colors.Red : Colors.Transparent));
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
throw new NotImplementedException();
}
#endregion
}
Also add using System.Windows.Data namespace direction for IValueConverter. IValueConverter is used to convert the data as it passes through the binding engine, so as to display data in a format that differs from how it is stored. In our case, InvalidToBrushConverter converts invalid value to red color brush.
Ok, with all the code in place, it is time to test. F5 and run the application. Change age field to 222 and city to New City. Clear Last Name.
All changes fail validation check and UI is updated via data binding to indicate errors. One thing missing is error message for user. In order to provide user with information about errors, lets add a new converter class, PersonStateToErrMsgConverter.cs to the silverlight project. This class will convert details of person state, represented by instance of PersonValidator to a user friendly message.
using System.Windows.Data;
namespace Silverlight { public class PersonStateToErrMsgConverter : IValueConverter {
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if ((bool)value) { return InvalidMessage(parameter.ToString());
} else { return parameter.ToString());
}
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException();
}
private string InvalidMessage(string propertyName) { switch (propertyName) { case "Age":
return "Age must be greater than 0 and less then 200";
case "LastName":
return "Last Name is required";
case "City":
return "Please provide valid city";
default:
return propertyName;
}
}
#endregion
}
}
Here we are using ConverterParameter to pass PropertyName for which we are checking validation result. Also modify Person.xaml to include ToolTip as follows:
<UserControl x:Class="Silverlight.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
xmlns:src="clr-namespace:Silverlight"
>
<UserControl.Resources>
<src:InvalidToBrushConverter x:Key="brushConverter"/>
<src:PersonStateToErrMsgConverter x:Key="messageConverter"/>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White" Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<!--<Button x:Name="addButton" Content="Add" Margin="5"/>-->
<Button x:Name="deleteButton" Content="Delete" Margin="5"/>
</StackPanel>
<!--<data:DataGrid x:Name="peopleDataGrid" Grid.Row="1" />-->
<data:DataGrid
x:Name="peopleDataGrid"
AutoGenerateColumns="False"
Grid.Row="1"
>
<data:DataGrid.Columns>
<data:DataGridTextColumn
Header="First Name"
DisplayMemberBinding="{Binding FirstName}" />
<data:DataGridTemplateColumn
Header="Last Name"
>
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border
Background="{Binding Path=Validator.InvalidLastName, Converter={StaticResource brushConverter}}" ToolTipService.ToolTip="{Binding Path=Validator.InvalidLastName, Converter={StaticResource messageConverter},ConverterParameter=LastName}" >
<TextBlock
Text="{Binding LastName}" />
</Border>
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
<data:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox
Text="{Binding Path=LastName,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}" Background="{Binding Path=Validator.InvalidLastName, Converter={StaticResource brushConverter}}" ToolTipService.ToolTip="{Binding Path=Validator.InvalidLastName, Converter={StaticResource messageConverter},ConverterParameter=LastName}" />
</DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>
</data:DataGridTemplateColumn>
<data:DataGridTemplateColumn
Header="Age"
>
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Background="{Binding Path=Validator.InvalidAge, Converter={StaticResource brushConverter}}" ToolTipService.ToolTip="{Binding Path=Validator.InvalidAge, Converter={StaticResource messageConverter},ConverterParameter=Age}" >
<TextBlock
Text="{Binding Age}" />
</Border>
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
<data:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox
Text="{Binding Path=Age,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}" ToolTipService.ToolTip="{Binding Path=Validator.InvalidAge, Converter={StaticResource messageConverter},ConverterParameter=Age}" Background="{Binding Path=Validator.InvalidAge, Converter={StaticResource brushConverter}}" />
</DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>
</data:DataGridTemplateColumn>
<data:DataGridTemplateColumn
Header="City"
>
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Background="{Binding Path=Validator.InvalidCity, Converter={StaticResource brushConverter}}" ToolTipService.ToolTip="{Binding Path=Validator.InvalidCity, Converter={StaticResource messageConverter},ConverterParameter=City}" >
<TextBlock
Text="{Binding City}" />
</Border>
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
<data:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox
Text="{Binding Path=City,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}" Background="{Binding Path=Validator.InvalidCity, Converter={StaticResource brushConverter}}" ToolTipService.ToolTip="{Binding Path=Validator.InvalidCity, Converter={StaticResource messageConverter},ConverterParameter=City}" />
</DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>
</data:DataGridTemplateColumn>
</data:DataGrid.Columns>
</data:DataGrid>
<src:WaitControl Grid.RowSpan="2" x:Name="waitControl" Visibility="Collapsed"/>
</Grid>
</UserControl>
And now when validation fails, user is presented with a friendly message about the error.
PersonValidator - Take 2
While above works fine, we still have an issue with automatic validation carried out by DataGrid itself, as it is separate from our custom validator. Currently we have two places where validation occurs, DataGrid and PersonValdiator. It will be advantageous if we can combine results of all validations and have a single entity to work with from UI.
One way to achieve above is for PersonValidator to provide a common method to Register validation failure, whether it was carried out inside of PersonValidator or externally, like by DataGrid or some other external class. We will also enhance PersonValidator to provide error messages. Change PersonValidator.cs with following code: (I have kept commented out old code for comparison)
using Silverlight.CityServiceReference;
using System.ComponentModel;
using System.Collections.Generic;
namespace Silverlight { public class PersonValidator : INotifyPropertyChanged {
public const string PROPERTY_NAME_INVALID = "Invalid";
//private bool _invalidAge;
public const string PROPERTY_NAME_AGE = "Age";
public bool InvalidAge { get { //return _invalidAge;
return _errors.ContainsKey(PROPERTY_NAME_AGE);
}
set { //if (_invalidAge == value) return;
//_invalidAge = value;
if (value) { RegisterError(PROPERTY_NAME_AGE, "Age must be greater than 0 and less than 200");
} else { ClearError(PROPERTY_NAME_AGE);
}
//OnPropertyChanged(PROPERTY_NAME_INVALID + PROPERTY_NAME_AGE);
}
}
public void ValidateAge(int newValue) { InvalidAge = (newValue < 0 || newValue > 200);
}
//private bool _invalidLastName;
public const string PROPERTY_NAME_LASTNAME = "LastName";
public bool InvalidLastName { get { //return _invalidLastName;
return _errors.ContainsKey(PROPERTY_NAME_LASTNAME);
}
set { //if (_invalidLastName == value) return;
//_invalidLastName = value;
if (value) { RegisterError(PROPERTY_NAME_LASTNAME, "Last Name is required");
} else { ClearError(PROPERTY_NAME_LASTNAME);
}
//OnPropertyChanged(PROPERTY_NAME_INVALID + PROPERTY_NAME_LASTNAME);
}
}
public void ValidateLastName(string newValue) { if (string.IsNullOrEmpty(newValue)) { InvalidLastName = true;
return;
}
InvalidLastName = (0 == newValue.Trim().Length);
}
//private bool _invalidCity;
public const string PROPERTY_NAME_CITY = "City";
public bool InvalidCity { get { //return _invalidCity;
return _errors.ContainsKey(PROPERTY_NAME_CITY);
}
set { //if (_invalidCity == value) return;
//_invalidCity = value;
if (value) { RegisterError(PROPERTY_NAME_CITY, "City is not valid.");
} else { ClearError(PROPERTY_NAME_CITY);
}
//OnPropertyChanged(PROPERTY_NAME_INVALID + PROPERTY_NAME_CITY);
}
}
//private bool _isValid;
public const string PROPERTY_NAME_ISVALID = "IsValid";
public bool IsValid { get { //return _isValid;
return (0 == _errors.Keys.Count);
}
set { //if (_isValid == value) return;
//_isValid = value;
OnPropertyChanged(PROPERTY_NAME_ISVALID);
_data.RaisePropertyChanged("Validator"); }
}
private void ValidateCity(string city) { if (string.IsNullOrEmpty(city)) { InvalidCity = true;
return;
}
CityServiceClient proxy = new CityServiceClient();
proxy.IsCityValidCompleted += new EventHandler<IsCityValidCompletedEventArgs>(proxy_IsCityValidCompleted);
((Page)Application.Current.RootVisual).StartWait("Please wait, validating City"); proxy.IsCityValidAsync(city, city);
}
void proxy_IsCityValidCompleted(object sender, IsCityValidCompletedEventArgs e) { ((Page)Application.Current.RootVisual).EndWait(null);
InvalidCity = !e.Result;
}
Person _data;
public PersonValidator(Person data) { _data = data;
_data.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(_data_PropertyChanged);
// default valid
//_isValid = true;
_errors = new Dictionary<string, string>();
}
public PersonValidator(Person data, bool defaultInvalid)
: this(data) { if (defaultInvalid) { //_invalidAge = true;
//_invalidLastName = true;
//_invalidCity = true;
//_isValid = false;
_errors.Add(PROPERTY_NAME_AGE, PROPERTY_NAME_AGE);
_errors.Add(PROPERTY_NAME_CITY, PROPERTY_NAME_CITY);
_errors.Add(PROPERTY_NAME_LASTNAME, PROPERTY_NAME_LASTNAME);
}
}
void _data_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { if (e.PropertyName == "IsValid") { return;
}
switch (e.PropertyName) { case PROPERTY_NAME_AGE:
ValidateAge(_data.Age);
break;
case PROPERTY_NAME_LASTNAME:
ValidateLastName(_data.LastName);
break;
case PROPERTY_NAME_CITY:
ValidateCity(_data.City);
break;
}
}
public void Validate() { ValidateAge(_data.Age);
ValidateLastName(_data.LastName);
ValidateCity(_data.City);
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name) { if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
//
//IsValid = !(_invalidAge || _invalidLastName || _invalidCity);
//
}
#endregion
public string this[string propertyName] { get { if (_errors.ContainsKey(propertyName)) { return _errors[propertyName];
} else { return null;// propertyName;
}
}
}
private Dictionary<string,string> _errors;
public void RegisterError(string propertyName, string message) { if (_errors.ContainsKey(propertyName)) { _errors[propertyName] = message;
} else { _errors.Add(propertyName, message);
}
OnPropertyChanged(PROPERTY_NAME_INVALID + propertyName);
IsValid = false;
}
public void ClearError(string propertyName) { if (_errors.ContainsKey(propertyName)) { _errors.Remove(propertyName);
}
OnPropertyChanged(PROPERTY_NAME_INVALID + propertyName);
IsValid = true;
}
}
}
Above shows another way of tracking validation errors. Instead of individual fields tracking result of validation, we are using dictionary to track errors. If there is an entry in dictionary, we have an error, otherwise data is valid. If you have a very large number of properties to track and validate, it will be advantageous to use dictionary as it will reduce memory footprint. Invalid<PropertyName/> methods are now just helper method, making is easy and efficient to databind. Also note the use of indexer for providing common access to errors.
Next modify Person.cs to add RaisePropertyChanged helper method that will be used by PersonValidator to trigger Validator change notification. (This is required to let UI know of changes in validator state. Alternatively, you can expose a property on validator itself that will be a self reference and trigger change notification for that property)
internal void RaisePropertyChanged(string name) {
OnPropertyChanged(name);
}
Now modify PersonStateToErrMsgConverter.cs to user validator to retrieve error messages as follows:
using System.Windows.Data;
namespace Silverlight { public class PersonStateToErrMsgConverter : IValueConverter {
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { PersonValidator validator = value as PersonValidator;
string propertyName = parameter.ToString();
if (null != validator) { string message = validator[propertyName];
if (string.IsNullOrEmpty(message)) { // valid
return propertyName;
} else { // invalid
return message;
}
}
//
return value;
//if ((bool)value) { // return new TextBlock() { Text = InvalidMessage(propertyName) }; //} else { // return propertyName;// ValidMessage(propertyName);
//}
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException();
}
//private string InvalidMessage(string propertyName) { // switch (propertyName) { // case "Age":
// return "Age must be greater than 0 and less then 200";
// case "LastName":
// return "Last Name is required";
// case "City":
// return "Please provide valid city";
// default:
// return string.Format("Error for {0}", propertyName); // }
//}
#endregion
}
}
Tip: PersonStateToErrMsgConverter.cs above shows how to databind to an indexer. We are using ConverterParameter to provide key to lookup value using indexer.
Now change Page.xaml to use Validator as tooltip provider.
<UserControl x:Class="Silverlight.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
xmlns:src="clr-namespace:Silverlight"
>
<UserControl.Resources>
<src:InvalidToBrushConverter x:Key="brushConverter"/>
<src:PersonStateToErrMsgConverter x:Key="messageConverter"/>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White" Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<!--<Button x:Name="addButton" Content="Add" Margin="5"/>-->
<Button x:Name="deleteButton" Content="Delete" Margin="5"/>
</StackPanel>
<!--<data:DataGrid x:Name="peopleDataGrid" Grid.Row="1" />-->
<data:DataGrid
x:Name="peopleDataGrid"
AutoGenerateColumns="False"
Grid.Row="1"
>
<data:DataGrid.Columns>
<data:DataGridTextColumn
Header="First Name"
DisplayMemberBinding="{Binding FirstName}" />
<data:DataGridTemplateColumn
Header="Last Name"
>
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border
Background="{Binding Path=Validator.InvalidLastName, Converter={StaticResource brushConverter}}" ToolTipService.ToolTip="{Binding Path=Validator, Converter={StaticResource messageConverter},ConverterParameter=LastName}" >
<TextBlock
Text="{Binding LastName}" />
</Border>
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
<data:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox
Text="{Binding Path=LastName,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}" Background="{Binding Path=Validator.InvalidLastName, Converter={StaticResource brushConverter}}" ToolTipService.ToolTip="{Binding Path=Validator, Converter={StaticResource messageConverter},ConverterParameter=LastName}" Tag="LastName"
/>
</DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>
</data:DataGridTemplateColumn>
<data:DataGridTemplateColumn
Header="Age"
>
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Background="{Binding Path=Validator.InvalidAge, Converter={StaticResource brushConverter}}" ToolTipService.ToolTip="{Binding Path=Validator, Converter={StaticResource messageConverter},ConverterParameter=Age}" >
<TextBlock
Text="{Binding Age}" />
</Border>
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
<data:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox
Text="{Binding Path=Age,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}" Background="{Binding Path=Validator.InvalidAge, Converter={StaticResource brushConverter}}" ToolTipService.ToolTip="{Binding Path=Validator, Converter={StaticResource messageConverter},ConverterParameter=Age}" Tag="Age"
>
</TextBox>
</DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>
</data:DataGridTemplateColumn>
<data:DataGridTemplateColumn
Header="City"
>
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Background="{Binding Path=Validator.InvalidCity, Converter={StaticResource brushConverter}}" ToolTipService.ToolTip="{Binding Path=Validator, Converter={StaticResource messageConverter},ConverterParameter=City}" >
<TextBlock
Text="{Binding City}" />
</Border>
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
<data:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox
Text="{Binding Path=City,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}" Background="{Binding Path=Validator.InvalidCity, Converter={StaticResource brushConverter}}" ToolTipService.ToolTip="{Binding Path=Validator, Converter={StaticResource messageConverter},ConverterParameter=City}" Tag="City"
/>
</DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>
</data:DataGridTemplateColumn>
</data:DataGrid.Columns>
</data:DataGrid>
<src:WaitControl Grid.RowSpan="2" x:Name="waitControl" Visibility="Collapsed"/>
</Grid>
</UserControl>
And finally modify validation error handler peopleDataGrid_BindingValidationError in Page.xaml.cs to register errors.
private void peopleDataGrid_BindingValidationError(object sender, ValidationErrorEventArgs e) { if (e.Action == ValidationErrorEventAction.Added) { Person person = ((Control)e.Source).DataContext as Person;
if (null != person && null != ((Control)e.Source).Tag) { person.Validator.RegisterError(((Control)e.Source).Tag.ToString(), e.Error.Exception.Message);
}
//((Control)e.Source).Background = new SolidColorBrush(Colors.Red);
//((Control)e.Source).SetValue(ToolTipService.ToolTipProperty, e.Error.Exception.Message);
} else if (e.Action == ValidationErrorEventAction.Removed) { Person person = ((Control)e.Source).DataContext as Person;
if (null != person && null != ((Control)e.Source).Tag) { person.Validator.ClearError(((Control)e.Source).Tag.ToString());
}
//((Control)e.Source).Background = new SolidColorBrush(Colors.White);
//((Control)e.Source).SetValue(ToolTipService.ToolTipProperty, null);
}
}
When a validation exception occurs, we are registering error with validator, using Tag to provide property name.
Build and run the application
We are now capturing external errors and displaying them using the same unified mechanism. Same method can also be used to add other external messages.
One thing that you might have noticed is that we are using two different properties for conveying the same error information. Consider following xaml code snippet:
Background="{Binding Path=Validator.InvalidLastName, Converter={StaticResource brushConverter}}"
ToolTipService.ToolTip="{Binding Path=Validator, Converter={StaticResource messageConverter},ConverterParameter=LastName}"
We are using InvalidLastName for triggering Background color and Validator with converter parameter LastName, to trigger ToolTip for the same field validation, LastName. You can replace Background binding to Validator as well.
Background="{Binding Path=Validator, Converter={StaticResource brushConverter},ConverterParameter=LastName}"
ToolTipService.ToolTip="{Binding Path=Validator, Converter={StaticResource messageConverter},ConverterParameter=LastName}"
You will have to change InvalidToBrushConverter to use Validator in place of boolean value (just as in PersonStateToErrMsgConverter).
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
PersonValidator validator = value as PersonValidator;
if (null != validator) {
return new SolidColorBrush((!string.IsNullOrEmpty(validator[parameter.ToString()]) ? Colors.Red : Colors.Transparent));
}
return new SolidColorBrush(((bool)value ? Colors.Red : Colors.Transparent));
}
This approach is useful if you have lot of properties to validate and /or dynamically building the UI. It does have one disadvantage, any time a property is changed, all binding will be re-evaluated, not just the one that changed. This is because we are binding to the common Validator property in place of field specific Invalid<PropertyName/> property.
You can also optimize some of the async validation code. In case of city validation, instead of directly using city service, we can use a wrapper that will cache results of validation. Cache can then be used as first pass to perform local validation, relying on remote call only when needed.
This takes care of majority of validation situations. The separate validator class also provides us with a starting point to further automate the validation process. One idea is to use attribute based validation. For instance, instead of checking for Last Name, we can decorate LastName property with RequiredField attribute (similar to Asp.net RequiredFieldValidator) and have code to automatically check for that. (This will be is similar to Enterprise Library Validation Application Block functionality, but in context of Silverlight.).
Next we will implement IEditableObject and with PersonValidator, we have all pieces in place to enhance Add New Item functionality.