Silverlight Business Application Part 5: Validation - Refactored!

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:

  1. Age - must be greater than 0 and less then 200
  2. LastName - required field (can not be null or empty)
  3. 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.

image

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.

image

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

image

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.

Published Wednesday, September 10, 2008 11:27 AM by manish.dalal
Filed under: , ,

Comments

# Silverlight Cream for September 10 -- #365

Wednesday, September 10, 2008 5:47 PM by Community Blogs

Bart Czernicki on MultiThreading, Cigdem with HangRobot, Corey Schuman with a video slider, Terence Tsang

# 2008 September 11 - Links for today &laquo; My (almost) Daily Links

Thursday, September 11, 2008 1:01 AM by 2008 September 11 - Links for today « My (almost) Daily Links

Pingback from  2008 September 11 - Links for today &laquo; My (almost) Daily Links

# re: Silverlight Business Application Part 5: Validation - Refactored!

Thursday, September 11, 2008 1:31 AM by Parag Mehta

This proves it's pretty time consuming to create Validators in Silverlight/WPF applications ! The way you are showing means even for each subset of data being shown, we should have a seperate Validator class :(, This is highly unproductive code! Can you imagine how many validator class we will end up in a reasonably complex business application ? There's got to be a simple way. I will put up on my site as soon as I have sometime :)

# Silverlight news for September 11, 2008

Thursday, September 11, 2008 5:29 AM by Silverlight news for September 11, 2008

Pingback from  Silverlight news for September 11, 2008

# re: Silverlight Business Application Part 5: Validation - Refactored!

Thursday, September 11, 2008 1:26 PM by manish.dalal

Parag,

SL2 B2 clearly lacks in validation area, actually quite a bit. Hopefully MS can port some of the experience from other technologies(winforms, asp.net) and incorporate into SL.

What I am trying to show are the basic constucts. Idea is that once you understand the nuts and blots, you can build higher level constructs!

In meanwhile, there are couple of approaches to automate, one I already hinted is Attribute based validation. Also you can use code generator to write out quite a bit of code.

# Building Business Application with Silverlight 2 (Beta 2) - Manish Dalal's blog

Pingback from  Building Business Application with Silverlight 2 (Beta 2) - Manish Dalal's blog

# Cross Browser Copy and Paste in DataGrid with Excel support – Part 1

Wednesday, November 19, 2008 11:48 AM by Manish Dalal's blog

Silverlight 2 is a cross browser platform(plug-in), providing developers with a familiar .net programming

# re: Silverlight Business Application Part 5: Validation - Refactored!

Friday, January 09, 2009 6:22 AM by Syed Mehroz Alam

Very nice implementation.

Just one question: In you validator class, the Invalid properties(e.g. InvalidAge) have public setters. Do you have any specific reason for  this?

Regards,

Syed Mehroz Alam

# re: Silverlight Business Application Part 5: Validation - Refactored!

Tuesday, June 16, 2009 8:57 AM by Sanjay Dholakiya

I code same as you have given. but still when it give error when exception occurs in class. can i download code and any other type of error.

I need the same requirement.

my email id is:sldholakiya@hotmail.com

Thank you.

# re: Silverlight Business Application Part 5: Validation - Refactored!

Friday, November 06, 2009 9:32 PM by Eric Scherrer

Great info - thanks!

Leave a Comment

(required) 
(required) 
(optional)
(required)