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.
In this recipe we will explain how to use this new powerful feature of WPF in .NET 4.5.
WPFValidation
.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)); } }
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)); } }
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>
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; }
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.
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:
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.
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: