Silverlight 4 and Asynchronous Validation with INotifyDataErrorInfo

During this week I helped a company with the design of a RIA with Silverlight. In the existing Web and Window Forms application they have a form where the users can enter an account id (or was it a customer id ;)). When they leave the TextBox they did a check if the account id already exists or not. They use their own way of showing the validation message. Now in the new application they want to use the red boxes showed up in Silverlight when a validation fails. In Silverlight 3 the validation only showed up if the bounded property of a TextBox thrown an exception in the set method. It’s not easy to manually trigger the nice red validation message box for a specific control outside the set method of a bounded property. Every call to the service layer from the Client should be asynchronous and in that way the callback can’t trigger the validation error box. With Silverlight 4 it’s now possible to notify when a validation fails when an async. method is completed. This is thanks to the INotifyDataErrorInfo interface. This interface can be used to notify the UI when an validation fails, and that can be done outside the set method of a property, for example in a async. callback method. The INotifyDataErrorInfo has the following members:

public interface INotifyDataErrorInfo
{
     bool HasErrors { get; }

     event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

     IEnumerable GetErrors(string propertyName);
}

The HasError is used to tell if there are any errors, the GetErrors should return a list or errors for a specific property. A property can have several errors, for example several validations added to one property. The ErrorsChanged event is the event we can use to notify the UI if there is any errors.

The INotifyDataErrorInfo is perfect in the ViewModel of the Model View View Model pattern pattern when async. operations can take place. Here is an example of a ViewModel for a View where the user can specify an account ID:

public class AccountViewModel : ViewModel
{
    private string _accountID = null;
private const string ACCOUNT_ALREADY_EXIST_ERROCODE = 100;
private AccountContext _accountContext = new AccountContext(); public string AccountID { get { return _accountID; } set { if (_accountID != value) { var propertyName = "AccountID"; ValidateAccountAlreadyExists( value, propertyName, ACCOUNT_ALREADY_EXIST_ERROCODE, string.Format("Account with the ID {0} already exists", value)); _accountID = value; NotifyPropertyChanged(propertyName); } } } private void ValidateAccountAlreadyExists( string accountID, string propertyName, int errorCode, string errorMsg) { _accountContext.DoesAccountExists( accountID, invokeOperation => { if (invokeOperation.Value) { AddErrorToPropertyAndNotifyErrorChanges( propertyName, new ValidationErrorInfo() { ErrorCode = errorCode, ErrorMessage = errorMsg }); } else { RemoveErrorFromPropertyAndNotifyErrorChanges( propertyName, errorCode); } }, null); } }

I use WCF RIA Services Invoke (ServiceOpertation) operation in this example for the async. call. As you can see the AccountViewModel’s set method of the AccoundID property will do a async. validation to check if the account exists. The callback of the async. call will call a AddErrorToPropertyAndNotifyErrorChanges if the account exists, and if the accounts doesn’t exists or if the validation is fixed (a user enter an ID that don’t already exists) the RemoveErrorProeprtyAndNotifyErrorChanges will be called to remove an validation error message if it’s already added and displayed of the user. The Add and Remove methods are added to the ViewModel base class, only to reduce code and also to reuse code among different ViewModels. The following is the ViewModel base class implementation:

public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
    public event PropertyChangedEventHandler PropertyChanged;
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;


    private Dictionary<string, List<ValidationErrorInfo>> _errors = 
new Dictionary<string, List<ValidationErrorInfo>>(); protected void RemoveErrorFromPropertyAndNotifyErrorChanges(
string propertyName,
int errorCode) { if (_errors.ContainsKey(propertyName)) { RemoveErrorFromPropertyIfErrorCodeAlreadyExist(propertyName, errorCode);
NotifyErrorsChanged(propertyName); } } private void RemoveErrorFromPropertyIfErrorCodeAlreadyExist(
string propertyName,
int errorCode) { if (_errors.ContainsKey(propertyName)) { var errorToRemove = _errors[propertyName].SingleOrDefault(
error => error.ErrorCode == errorCode); if (errorToRemove != null) { _errors[propertyName].Remove(errorToRemove);




if (_errors[propertyName].Count == 0)
_errors.Remove(propertyName);
} } }

    protected void AddErrorToPropertyAndNotifyErrorChanges(
string propertyName,
ValidationErrorInfo errorInfo) { RemoveErrorFromPropertyIfErrorCodeAlreadyExist(propertyName, errorInfo.ErrorCode);
        if (!_errors.ContainsKey(propertyName))
            _errors.Add(propertyName, new List<ValidationErrorInfo>());

        _errors[propertyName].Add(errorInfo);

        NotifyErrorsChanged(propertyName);
    }


    public IEnumerable GetErrors(string propertyName)
    {
        if (!_errors.ContainsKey(propertyName))
            return _errors.Values;

        return _errors[propertyName];
    }


    public bool HasErrors
    {
        get { return this._errors.Count > 0; }
    }

    
    private void NotifyErrorsChanged(string propertyName)
    {
        if (ErrorsChanged != null)
            ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
    }


    protected void NotifyPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

Note: I have also implemented the INotifyPropertyChanged interface into the ViewModel class, but it has nothing to do with the error notification, only to reuse code in the ViewModels.

As you can see the GetErrors method will return a List of ValdationErrorInfo object for a specific property.

public IEnumerable GetErrors(string propertyName)
{
    if (!_errors.ContainsKey(propertyName))
        return _errors.Values;

    return _errors[propertyName];
}

Because the GetErrors method returns an IEnumerable for a specific property, I have added a Dictionary where the key is the name of the property and the values are a List of ValidationErrorInfo objects:

private Dictionary<string, List<ValidationErrorInfo>> _errors = 
new Dictionary<string, List<ValidationErrorInfo>>();


Note: The GetErrors could simply return a list of strings containing the error messages, but because I want an easy way to remove an validation error after a user enter a valid account ID, I have added a ErrorCode property to hold an unique value of a specific error. At the end of this post, you will se the simples implementation of the INotifyDataErrorInfo, where only one validation error message is added to one property.


The ValidationErrorInfo is a class which I have created to hold some validation errors, here is the code for the ValidationErrorInfo and as you can see the ToString is overridden, this is because the validation feature added to Silverlight will use the ToString method of the objects passed from the GetErrors method, and when it does, I want it to return the validation error message.

public class ValidationErrorInfo
{
    public int ErrorCode { get; set; }

    public string ErrorMessage { get; set; }

    public override string ToString()
    {
       return ErrorMessage;
    }
}


The HasErrors method added to the ViewModel class, returns true if the _errors Dictionary has any items added. If so there are some errors.

public bool HasErrors
{
    get { return this._errors.Count > 0; }
}

The NotifyErrorsChanged method added to the ViewModel class is a helper method to trigger the ErrorsChanged event for a specific property. When this event is trigged the UI will be notified if there are errors or not.

private void NotifyErrorsChanged(string propertyName)
{
    if (ErrorsChanged != null)
        ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}

The Add and Remove methods will add or remove a ValidationErrorInfo from a specific property:

protected void RemoveErrorFromPropertyAndNotifyErrorChanges(
                string propertyName,
                int errorCode)
{
    if (_errors.ContainsKey(propertyName))
    {
        RemoveErrorFromPropertyIfErrorCodeAlreadyExist(propertyName, errorCode);
        NotifyErrorsChanged(propertyName);
    }
}

private void RemoveErrorFromPropertyIfErrorCodeAlreadyExist(
string propertyName,
int errorCode) { if (_errors.ContainsKey(propertyName)) { var errorToRemove = _errors[propertyName].SingleOrDefault(
error => error.ErrorCode == errorCode); if (errorToRemove != null) {
             _errors[propertyName].Remove(errorToRemove);




if (_errors[propertyName].Count == 0)
_errors.Remove(propertyName);
} } } protected void AddErrorToPropertyAndNotifyErrorChanges( string propertyName, ValidationErrorInfo errorInfo) {
RemoveErrorFromPropertyIfErrorCodeAlreadyExist(propertyName, errorInfo.ErrorCode);



if (!_errors.ContainsKey(propertyName)) _errors.Add(propertyName, new List<ValidationErrorInfo>()); _errors[propertyName].Add(errorInfo); NotifyErrorsChanged(propertyName); }


The Remove method takes two arguments, propteryName and errorCode. First I passed a ValidationErrorInfo object instead of a errorCode, but I thought it was just unnecessarily  to create a new ValidtionErrorInfo object when only the errorCode is needed to locate the ValidationErrorInfo which should be removed from the _errors Dictionary. The Add method takes a propertyName to which the ValidationErrorInfo object should be added to. The add method will first check if there is already an added validation with the same code to the _errors Dictionary, if so it will remove it so it will be repleced with the new validation error of the same kind. For example if the user enter a invalid UserID, the validation till be added to the _errors, it the user doesn't correct the error and write a new invalid userid, that previouse validation must be replaced with the new one.  If there aren’t any errors already added to the specific property, a new List of ValidationErrorInfo is created. The ValidationErrorInfo is then added to the _errors dictionary where the key is the property name. Both Add and the Remove methods will make a call to the NotifyErrorsChanged helper method to update the UI.

Here is the XAML for the View which uses the AccountViewModel:

<UserControl x:Class="SilverlightBlog.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:wm="clr-namespace:SilverlightBlog"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
    <UserControl.Resources>
        <wm:AccountViewModel x:Name="accountViewModel"/>
    </UserControl.Resources>

    <Grid DataContext="{StaticResource accountViewModel}" ... >
        <TextBlock Text="Account ID:" ... />
        <TextBox Text="{Binding AccountID, Mode=TwoWay, NotifyOnValidationError=True}" ... />
    </Grid>
</UserControl>


To make sure the UI will display the validation error, we need to set the Binding expression’s NotifyValudationError property to true.

Note: I Have not added one single of code to the code-behind, all logic is added to the ViewModel for separation of concerns.

The following shows the nice validation error box after a async. call is made and the account already exists:

image

Here is the DomainService with the Invoke operation and the Account DTO if you are interested, I only fake the data access in the code:

[EnableClientAccess()]
public class AccountService : DomainService
{
    [Invoke]
    public bool DoesAccountExists(string accountID)
    {
        if (accountID == "12345")
            return true;

        return false;
     }
}


public class Account
{
    [Key]
    public string AccoutnID { get; set; }
}

Here is a short and simple example code where only one error message is added to a property, only to demonstrate how easy it’s to use the INotifyDataErrorInfo interface and the concept:

public class SimpleModel : INotifyDataErrorInfo
{
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    private Dictionary<string, List<String>> _errors = new Dictionary<string, List<String>>();

    private string _accountID = null;

    public string AccountID
    {
        get { return _accountID; }
        set
        {
            if (_accountID != value)
            {
                var propertyName = "AccountID";

                if (string.IsNullOrEmpty(value))
                {
                    if (!_errors.ContainsKey(propertyName))
                        _errors.Add(propertyName, new List<string>());

                    _errors[propertyName].Add("AccountID can't be null or empty");
                }
                else
                {
                    if (_errors.ContainsKey(propertyName))
_errors.Remove(propertyName); } NotifyErrorsChanged(propertyName); //Maybe you don't want to set this field to a value if the validation fails _accountID = value; } } } public System.Collections.IEnumerable GetErrors(string propertyName) { if (_errors.ContainsKey(propertyName)) return _errors[propertyName]; return _errors.Values; } public bool HasErrors { get { return _errors.Count > 0; } } private void NotifyErrorsChanged(string propertyName) { if (ErrorsChanged != null) ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName)); } }

If you want to know when I publish new blog posts or simply want to follow my life etc, you can find me on twitter: http://www.twitter.com/fredrikn

4 Comments

  • Nice overview of INDEI. However, your code has a few bugs in it:

    - If the user enters an ID that is already in use, and then enters another ID that is already in use, then two errors are added to the error list, causing SingleOrDefault (in RemoveError...) to throw an exception. You should remove the first error whenever the user specifies a new value.

    - GetErrors should probably return _error.Values only when propertyName is null or empty (indicating that errors for the whole object are being requested). If propertyName has a non-null/empty value that is not a key in the _errors dictionary, you should probably return null.

    - Errors are removed from the lists within the dictionary, but the lists are not removed from the dictionary when they become empty. Therefore, your HasErrors implementation will incorrectly return true if there were ever any errors, even if they have been removed.


  • @Karl Erickson:
    Thanks for you great observation,sometimes it's hard to be water proof when showing the concepts. I made changes to the code, haven't tested it yet though, because I just updated it in the post.. ;)

  • What if I want this on server side? There's only IDataErrorInfo. How do I implement the Change event?

  • I like what you're doing here, but the one thing I can't figure out is: How do you continue to use RIA Entities (and thus all the built in client and server-side validations that come along with that) and take advantage of this idea.

    I'm talking about exposing the RIA Entity through your ViewModel so that binding's done directly on the RIA Entity. It's the only way I seem to be able to make use of the RIA Data Annotations at the same time as using MVVM.

    Any ideas?

    Tom

Comments have been disabled for this content.