The INotifyDataErrorInfo interface was added to the .NET Framework in .NET 4.5 to address concerns over the previous IDataErrorInfo interface. Like the IDataErrorInfo interface, the INotifyDataErrorInfo interface is also a simple affair, with only three members for us to implement.
With this interface, we now have a HasErrors property, which indicates whether the relevant data Model instance has any errors, a GetErrors method that retrieves the object's error collection, and an ErrorsChanged event to raise when the entity's errors change. We can see straight away that this interface was designed to work with multiple errors, unlike the IDataErrorInfo interface. Now, let's take a look at an implementation of this:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using CompanyName.ApplicationName.Extensions;
namespace CompanyName.ApplicationName.DataModels
{
public abstract class BaseNotifyValidationModel : INotifyPropertyChanged,
INotifyDataErrorInfo
{
protected Dictionary<string, List<string>> AllPropertyErrors { get; } =
new Dictionary<string, List<string>>();
public ObservableCollection<string> Errors =>
new ObservableCollection<string>(
AllPropertyErrors.Values.SelectMany(e => e).Distinct());
public abstract IEnumerable<string> this[string propertyName] { get; }
public void NotifyPropertyChangedAndValidate(
params string[] propertyNames)
{
foreach (string propertyName in propertyNames)
NotifyPropertyChangedAndValidate(propertyName);
}
public void NotifyPropertyChangedAndValidate(
[CallerMemberName]string propertyName = "")
{
NotifyPropertyChanged(propertyName);
Validate(propertyName);
}
public void Validate(string propertyName)
{
UpdateErrors(propertyName, this[propertyName]);
}
private void UpdateErrors(string propertyName,
IEnumerable<string> errors)
{
if (errors.Any())
{
if (AllPropertyErrors.ContainsKey(propertyName))
AllPropertyErrors[propertyName].Clear();
else AllPropertyErrors.Add(propertyName, new List<string>());
AllPropertyErrors[propertyName].AddRange(errors);
OnErrorsChanged(propertyName);
}
else
{
if (AllPropertyErrors.ContainsKey(propertyName))
AllPropertyErrors.Remove(propertyName);
OnErrorsChanged(propertyName);
}
}
#region INotifyDataErrorInfo Members
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
protected void OnErrorsChanged(string propertyName)
{
ErrorsChanged?.Invoke(this,
new DataErrorsChangedEventArgs(propertyName));
NotifyPropertyChanged(nameof(Errors), nameof(HasErrors));
}
public IEnumerable GetErrors(string propertyName)
{
List<string> propertyErrors = new List<string>();
if (string.IsNullOrEmpty(propertyName)) return propertyErrors;
AllPropertyErrors.TryGetValue(propertyName, out propertyErrors);
return propertyErrors;
}
public bool HasErrors =>
AllPropertyErrors.Any(p => p.Value != null && p.Value.Any());
#endregion
#region INotifyPropertyChanged Members
public virtual event PropertyChangedEventHandler PropertyChanged;
protected virtual void NotifyPropertyChanged(
params string[] propertyNames)
{
if (PropertyChanged != null) propertyNames.ForEach(
p => PropertyChanged(this, new PropertyChangedEventArgs(p)));
}
protected virtual void NotifyPropertyChanged(
[CallerMemberName]string propertyName = "")
{
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}
In our first implementation, we see the declaration of the read-only AllPropertyErrors auto property, initialized to a new instance. For this collection, we use the Dictionary<string, List<string>> type, where the name of each property in error is used as the dictionary key, and multiple errors for that property can be stored in the related string list.
We then see the read-only, expression-bodied Errors property, which will hold the string collection of errors to be displayed in the UI. It is set to return a compilation of unique errors from the AllPropertyErrors collection. Next, we find an abstract string indexer that returns an IEnumerable of the string type, which is responsible for returning multiple validation errors from derived classes that relate to the property specified by the propertyName input parameter. We'll see how we can implement this property in a derived class shortly.
After that, we add two convenient NotifyPropertyChangedAndValidate methods, which we can use to both provide notification of changes to our property and to validate it in a single operation. In these methods, we call our implementation of the NotifyPropertyChanged method and then our Validate method, passing the relevant property name to each of them.
In the Validate method, we call the UpdateErrors method, passing in the propertyName input parameter and the related errors for the specified property, returned from the this indexer property. In the UpdateErrors method, we begin by checking whether there are any errors in the collection specified by the errors input parameter.
If there are, and it does contain some, we clear the errors for the relevant property from the AllPropertyErrors collection, or initialize a new entry for the property, with an empty collection otherwise. We then add the incoming errors to the AllPropertyErrors collection for the relevant property and call the OnErrorsChanged method to raise the ErrorsChanged event.
If there are no errors in the collection specified by the errors input parameter, we remove all previous entries from the AllPropertyErrors collection for the relevant property, after first validating that some exist, so as to avoid an Exception being thrown. We then call the OnErrorsChanged method to raise the ErrorsChanged event to notify changes to the collection.
Next, we see the required INotifyDataErrorInfo interface members. We declare the ErrorsChanged event for internal use only and the related OnErrorsChanged method that raises it using the null conditional operator, although this method is not technically part of the interface and we are free to raise the event as we see fit. After raising the event, we notify the system of changes to the Errors and HasErrors properties, to refresh the error collection, and to update the UI of any changes.
In the GetErrors method, we are required to return the errors for the propertyName input parameter. We start by initializing the propertyErrors collection, which we return immediately if the propertyName input parameter is null, or empty. Otherwise, we use the TryGetValue method to populate the propertyErrors collection with the errors that relate to the propertyName input parameter from the AllPropertyErrors collection. We then return the propertyErrors collection.
The simplified HasErrors expression-bodied property follows and simply returns true if the AllPropertyErrors collection property contains any errors, or false otherwise. We complete the class with our default implementation of the INotifyPropertyChanged interface. Note that we can simply omit this if we intend this base class to extend another with its own implementation of this interface.
Let's copy our earlier Product class so as to create a new ProductNotify class that extends our new base class. Apart from the class name and the collection of errors, we need to make a number of changes. Let's look at these now:
using System;
using System.Collections.Generic;
namespace CompanyName.ApplicationName.DataModels
{
public class ProductNotify : BaseNotifyValidationModel
{
...
public string Name
{
get { return name; }
set { if (name != value) { name = value;
NotifyPropertyChangedAndValidate(); } }
}
public decimal Price
{
get { return price; }
set { if (price != value) { price = value;
NotifyPropertyChangedAndValidate(); } }
}
public override IEnumerable<string> this[string propertyName]
{
get
{
List<string> errors = new List<string>();
if (propertyName == nameof(Name))
{
if (string.IsNullOrEmpty(Name))
errors.Add("Please enter the product name.");
else if (Name.Length > 25) errors.Add("The product name cannot
be longer than twenty-five characters.");
if (Name.Length > 0 && char.IsLower(Name[0])) errors.Add("The
first letter of the product name must be a capital letter.");
}
else if (propertyName == nameof(Price) && Price == 0)
errors.Add("Please enter a valid price for the product.");
return errors;
}
}
}
}
The main differences between the ProductNotify and Product classes relate to the base class, the notification method used, and the handling of multiple concurrent errors. We start by extending our new BaseNotifyValidationModel base class. Each property, with the Exception of the Id property, which requires no validation, now calls the NotifyPropertyChangedAndValidate method from the new base class, instead of the NotifyPropertyChanged method from the BaseValidationModel class.
In addition to that, the this indexer property can now report multiple errors simultaneously, rather than the single error that the BaseValidationModel class could work with. As such, it now declares a string list to hold the errors, with each valid error being added to it in turn. The final difference is that we have also added a new error, which validates the fact that the first letter of the product name should start with a capital letter.
Let's now see our ProductNotifyViewModel class:
using System;
using System.Linq;
using CompanyName.ApplicationName.DataModels;
using CompanyName.ApplicationName.DataModels.Collections;
namespace CompanyName.ApplicationName.ViewModels
{
public class ProductNotifyViewModel : BaseViewModel
{
private ProductsNotify products = new ProductsNotify();
public ProductNotifyViewModel()
{
Products.Add(new ProductNotify() { Id = Guid.NewGuid(),
Name = "Virtual Reality Headset", Price = 14.99m });
Products.Add(new ProductNotify() { Id = Guid.NewGuid(),
Name = "Virtual Reality Headset" });
Products.CurrentItem = Products.Last();
Products.CurrentItem.Validate(nameof(Products.CurrentItem.Name));
Products.CurrentItem.Validate(nameof(Products.CurrentItem.Price));
}
public ProductsNotify Products
{
get { return products; }
set { if (products != value) { products = value;
NotifyPropertyChanged(); } }
}
}
}
We start our ProductNotifyViewModel View Model by extending our usual BaseViewModel base class. We declare a ProductsNotify collection and, in the constructor, we populate it with two ProductNotify objects, with the same property values that were used in the ProductViewModelExtended class example. We again call the Last method on the ProductsNotify collection and set that last element to its CurrentItem property to pre-select the second item in the UI.
We then call the Validate method twice on the object set to the CurrentItem property, passing in the Name and Price properties, using the nameof operator for correctness. The class ends with the standard declaration of the Products property. Note that the ProductsNotify class simply extends our BaseCollection class, just like our Products class did:
namespace CompanyName.ApplicationName.DataModels.Collections
{
public class ProductsNotify : BaseCollection<ProductNotify> { }
}
Also note that if we removed the call to the Validate method from the constructor, this implementation would no longer work in a pre-emptive manner. It would instead initially hide any pre-existing validation errors, such as empty required values, until the user makes changes and there is a problem. Therefore, empty required values would never cause an error to be raised, unless a value was entered and then deleted, to once again be empty.
To address this, we could instead declare a ValidateAllProperties method that our View Models can call to force a new validation pass, either pre-emptively, before the user has a chance to enter any data, or on the click of a save button, once all fields have been filled. We'll see an example of this later in this chapter, but for now, let's see the XAML of our ProductNotifyView class:
<UserControl x:Class="CompanyName.ApplicationName.Views.ProductNotifyView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="600" FontSize="14">
<Grid Margin="20">
<Grid.Resources>
<DataTemplate x:Key="ProductTemplate">
<TextBlock Text="{Binding Name,
ValidatesOnNotifyDataErrors=False}" />
</DataTemplate>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ListBox ItemsSource="{Binding Products}"
SelectedItem="{Binding Products.CurrentItem}"
ItemTemplate="{StaticResource ProductTemplate}" Margin="0,0,20,0" />
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Border Grid.ColumnSpan="2" BorderBrush="Red"
BorderThickness="2" Background="#1FFF0000" CornerRadius="5"
Visibility="{Binding Products.CurrentItem.HasErrors,
Converter={StaticResource BoolToVisibilityConverter}}"
Margin="0,0,0,10" Padding="10">
<ItemsControl ItemsSource="{Binding Products.CurrentItem.Errors}"
ItemTemplate="{StaticResource WrapTemplate}" />
</Border>
<TextBlock Grid.Row="1" Text="Name"
Style="{StaticResource LabelStyle}" />
<TextBox Grid.Row="1" Grid.Column="1"
Text="{Binding Products.CurrentItem.Name,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnNotifyDataErrors=True}"
Style="{StaticResource FieldStyle}" />
<TextBlock Grid.Row="2" Text="Price"
Style="{StaticResource LabelStyle}" />
<TextBox Grid.Row="2" Grid.Column="1"
Text="{Binding Products.CurrentItem.Price,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnNotifyDataErrors=True, Delay=250}"
Style="{StaticResource FieldStyle}" />
</Grid>
</Grid>
</UserControl>
In the Resources section, we have declared a new DataTemplate element, named ProductTemplate. This just displays the value of the Name property, but importantly, with the binding’s ValidatesOnNotifyDataErrors property set to False, so that no error template is displayed within the ListBoxItem elements.
Another point to note is that the Visibility property of the global error display's border has now been updated to work with the new HasErrors property from the INotifyDataErrorInfo interface, rather than the HasError property from our previous BaseValidationModelExtended class.
The only other change was made to the Text property binding of the two TextBox controls; when using the INotifyDataErrorInfo interface, instead of setting the ValidatesOnDataErrors property to True as before, we now need to set the ValidatesOnNotifyDataErrors property to True.
We'll update this example again shortly, but before that, let's explore another method of providing validation logic.