Implementing asynchronous error handling with INotifyDataErrorInfo

The INotifyDataErrorInfo interface has been a .NET citizen for some time, at least for Silverlight developers. It brought a new improved system for validating data, which we now have in WPF, with all of its power and interesting things to discover.

Getting ready

In order to use this recipe you should have Visual Studio 2012 installed.

How to do it...

In this recipe we will explain how to use this new powerful feature of WPF in .NET 4.5.

  1. First, open Visual Studio 2012 and create a new project. We will select the WPF Application template from the Visual C# category and name it WPFValidation.
  2. Create a class named BaseClass.cs. Edit it and implement the interfaces INotifyPropertyChanged and IDataErrorInfo. We can copy the following code inside it:
    public abstract class BaseClass : INotifyPropertyChanged, INotifyDataErrorInfo
    {
        private static Dictionary<string, PropertyChangedEventArgs> argumentInstances = new Dictionary<string, PropertyChangedEventArgs>();
        private Dictionary<string, List<string>> errors = new Dictionary<string, List<string>>();
    
        public event PropertyChangedEventHandler PropertyChanged;
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
        public bool HasErrors
        {
            get { return this.errors.Count > 0; }
        }
    
        public IEnumerable GetErrors(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName) ||
                !this.errors.ContainsKey(propertyName))
                return null;
    
            return this.errors[propertyName];
        }
    
        public void AddError(string propertyName, string error, bool isWarning)
        {
            if (!this.errors.ContainsKey(propertyName))
                this.errors[propertyName] = new List<string>();
    
            if (!this.errors[propertyName].Contains(error))
            {
                if (isWarning)
                    this.errors[propertyName].Add(error);
                else
                    this.errors[propertyName].Insert(0, error);
            }
    
            this.RaiseErrorsChanged(propertyName);
        }
    
        public void RemoveError(string propertyName, string error)
        {
            if (this.errors.ContainsKey(propertyName) &&
                this.errors[propertyName].Contains(error))
            {
                this.errors[propertyName].Remove(error);
    
                if (this.errors[propertyName].Count == 0)
                    this.errors.Remove(propertyName);
            }
            this.RaiseErrorsChanged(propertyName);
        }
    
        public void RaiseErrorsChanged(string propertyName)
        {
            if (this.ErrorsChanged != null)
                this.ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
        }
    
        protected virtual void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
  3. We can now add a class that inherits this BaseClass, so let's add the BooksModel class to the equation:
    class BooksModel : BaseClass
    {
        private string name;
        public string Name
        {
            get { return name; }
            set 
            { 
                name = value;
                if ((name.Length < 2) || (name.Length > 150)) { 
                    this.AddError("Name", "The name must be over 2 characters and less than 150 characters", false);
                }
                OnPropertyChanged("Name");
            }
        }
    
        private String isbn;
        public String ISBN
        {
            get { return isbn; }
            set { 
                isbn = value;
                ValidateISBN();
            }
        }
    
        private async Task ValidateISBN()
        {
            await Wait_a_bit();
    
            Random rnd = new Random(DateTime.Now.Millisecond);
            int likeness = rnd.Next(0, 6);
            if (likeness > 2)
            {
                this.AddError("ISBN", "I don't like the ISBN", false);
            }
            else 
            {
                this.RemoveError("ISBN", "I don't like the ISBN");
            }    
        }
    
        private Task Wait_a_bit() {
            return Task.Run(() => Thread.Sleep(1500));        
        }
    }
  4. Continuing, we will open and edit the MainWindow.xaml file to provide a user interface, as follows:
    <Window x:Class="INotifyDataErrorInfoWPF.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="150" Width="425">
        <Grid Margin="0,0,0.4,-0.4">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="12.8"/>
                <ColumnDefinition Width="65:"/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="15.2"/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <Grid Margin="0.2,15,8.2,0.4" Grid.Column="1" Grid.RowSpan="2">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="7:"/>
                    <ColumnDefinition Width="23:"/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="32"/>
                    <RowDefinition Height="32"/>
                    <RowDefinition/>
                    <RowDefinition Height="4:"/>
                </Grid.RowDefinitions>
                <Label Content="Name:"/>
                <Label Content="ISBN:" Grid.Row="1"/>
                <TextBox x:Name="tbName" 
            Text="{Binding Name, Mode=TwoWay, ValidatesOnNotifyDataErrors=True}" 
            Grid.Column="1"
            Margin="2,2,2.4,2"
                       />
                <TextBox x:Name="tbISBN" 
            Text="{Binding ISBN, Mode=TwoWay, ValidatesOnNotifyDataErrors=True}" 
            Grid.Column="1" Grid.Row="1"
            Margin="2,2,1.4,2"
                       />
            </Grid>
        </Grid>
    </Window>
  5. Next, we will add some code in it to create an instance of BooksModel and set it as the context of the view. We will edit the MainWindow constructor as follows:
    public MainWindow()
    {
        InitializeComponent();
    
        BooksModel bm = new BooksModel();
        this.DataContext = bm;
    }
  6. If we run our application and introduce some information, for example, if we type a character in the Name field and exit it, we should see the input field surrounded by a red line, as shown in the following screenshot:
    How to do it...

Additionally, if we input some text in the ISBN field, it gets validated (in a bit more than a second) by a random function that decides whether it likes the value or not. So we can see an asynchronous validation in place. Note that the asynchronous nature of this validation would allow us to make it happen on the server, which could have more advanced business validation rules, that make sense to delegate some business validation to it.

How it works...

If we take a look at the INotifyDataErrorInfo class diagram in the following screenshot, we can appreciate its simplicity. Its interface exposes an ErrorsChanged event that is fired whenever an error is detected or removed, a HasErrors read-only property, and a GetErrors method that returns IEnumerable with the error list associated with the requested property:

How it works...

We implemented these elements on BaseClass, together with the usual event for INotifyPropertyChanged.

Additionally, we created the AddError and RemoveError helper methods to handle the addition and removal of errors in an easy way. Errors are automatically broadcasted to anybody who is listening to them—mainly the bindings in place.

The RaiseErrorsChanged method will raise the event to notify that the errors have changed, which will happen whenever we have added or removed an error.

Next, we have implemented the BookModel class, which inherits from BaseClass. For both its properties, we set up validations on its property setters.

For the first property, Name, we just check that its length is between 2 and 150 and if it is not we call the AddError method with the name of the property and the error. Note that there is a third parameter, which indicates if this is a warning.

For the ISBN property, we perform an asynchronous validation using the async method ValidateISBN(). As an example, it will first await an async method, named Wait_a_bit(), that just sleeps for 1.5 seconds, and then decides randomly whether the value is valid or not.

This example showcases that we could in fact go to the server, ask for it to validate the value, see if the updated ISBN already exists or is registered, and if it does, return an error.

Finally, we have created a very simple interface to enter data into two TextBox controls, which we have bound to the properties. Remember we need to add ValidatesOnNotifyDataErrors = True. If we don't do this, the UI field will not react to the validation events.

There's more...

This event based validation system is in many ways similar to INotifyPropertyInfo and is ideal for asynchronous validation systems that can be run on the server. Additionally, this enables us to implement really advanced scenarios such as:

  • Performing cross-property validations that apply to more than one field, for example, the typical from – to range of dates. Now, we can mark both as invalid when the from date is greater than the to date.
  • Hierarchical validation systems are those in which an error in a property element is cascaded to its parent, so if a piece of the complete hierarchy is invalid, the whole structure of data is invalidated.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset