In any GUI Application, if the user inputs are not valid, validation of user input and appropriate notification to the user has its own significance. It aids in communicating with the User on the type and expected format of the input with the User. In some cases, we might be having an input that could have multiple validation errors, each of the errors having its own priorities. The View could also represent this meta-information about priorities along with the actual error message to the User with color-coded messages, which might turn out to be useful for application end users.
In this post, we will look into a way to accomplish the same using WPF with the help of the INotifyErrorDataInfo interface.
INotifyErrorDataInfo and NotifyDataErrorValidationRule
WPF provides support for Validation using ValidationRules by associating the implementation of ValidationRules with the binding object. WPF comes with two in-build validation rules for us, in addition to the ability to build Custom Validation rules. The two inbuild validation rules that are frequently used are ExceptionValidationRule and DataErrorValidationRule.
- ExceptionValidationRule - Represents rule that checks if an exception is thrown during the update of the Source Property.
- DataErrorValidationRule - Represents rule that monitors errors raised by the implementation of IDataErrorInfo on the Source Object.
Both these implementations were limited to a single error per property and had limitations for handling asynchronous validations. This limitation was covered by the introduction of the INotifyErrorDataInfo and corresponding NotifyDataErrorValidationRule in WPF 4.5. Similar to DataErrorValidationRul, the NotifyDataErrorValidationRule checks for errors raised by the implementation of the INotifyErrorDataInfo property on the Source Object.
The INotifyErrorDataInfo provided the developers to implement asynchronous validation and supporting multiple error messages for each of the properties. The interface itself looked like the following.
public interface INotifyDataErrorInfo {
bool HasErrors {
get;
}
IEnumerable GetErrors(string propertyName);
event EventHandler < DataErrorsChangedEventArgs > ErrorsChanged;
}
As observed the interface consists of 2 properties and an Event. The implementations ensure the validations are executed when the property is updated and the event ErrorsChanged is raised. For the purpose of this post, the greater significance is the GetErrors() method that returns a collection for each property name. This is in contrast to the implementation of IDataErrorInfo, which had a single string property for representing the Error message. For sake of comparison, the following code shows the IDataErrorInfo interface.
public interface IDataErrorInfo
{
string this[string columnName] {get;}
string Error {get;}
}
Priorities and INotifyDataErrorInfo
The GetErrors() method not only provides a collection of errors, but a more significant factor is that it doesn't enforce any type on the error (IDataErrorInfo on the contrary enforces the message to be a string). This enables the developers to create Custom types for representing the Validation Error messages and include additional meta info along with it.
We will use this characteristic the INotifyDataErrorInfo to add Priority info for the error messages. As you can imagine, we will begin by implementing a Type to represent our error.
public class ErrorInfo {
public string Message {
get;
set;
}
public Prority Priority {
get;
set;
}
}
public enum Prority {
Low,
Medium,
High
}
Each ErrorInfo instance would have a string message representing the User-friendly message which we want to display to the User and an additional enum Priority which represents the significance or priority of the error (Low, Medium, High).
With that in place, we will move to our implementation of the INotifyDataErrorInfo. For sake of the example, we will consider a View that has a single field, a string representing the Name. We will associate 3 conditions for validating the user name.
- the name should be unique (Priority: High)
- Should contain only alphanumeric characters (Priority: Medium)
- Should contain a minimum of 3 characters and a maximum of 9 characters (Priority: Low)
public class NotifyDataErrorInfoWithProrityViewModel: ViewModelBase, INotifyDataErrorInfo {
private IDictionary < string, List < ErrorInfo >> _errors = new Dictionary < string, List < ErrorInfo >> ();
private string _name;
public override string Name {
get => _name;
set {
if (Equals(_name, value)) return;
_name = value;
NotifyOfPropertyChange();
Validate();
}
}
public bool HasErrors => _errors.Any();
public event EventHandler < DataErrorsChangedEventArgs > ErrorsChanged;
private void Validate([CallerMemberName] string propertyName = null) {
// Discussed below
}
public IEnumerable GetErrors(string propertyName) {
if (_errors.ContainsKey(propertyName)) return _errors[propertyName];
return null;
}
}
The basic implementation of the interface is quite simple. We will use a Dictionary to store our collection of Errors. Each property with validation errors in the Dictionary would be represented by a List<ErrorInfo>. The HasErrors would return true if Dictionary contains any elements, while the GetErrors would return the List of ErrorInfo for the property name pass as a parameter if it exists in the Dictionary.
The Setter of the Name property calls a method Validate which is where we would be validating the property. Let us go ahead and implement it now.
private void Validate([CallerMemberName] string propertyName = null) {
var errorList = new List < ErrorInfo > ();
if (Equals(propertyName, nameof(Name))) {
if (CheckIfNameExists(Name)) {
errorList.Add(new ErrorInfo {
Message = "UserName already exists",
Priority = Prority.High
});
}
if (Name is {
Length: < 3 or > 9
}) {
errorList.Add(new ErrorInfo {
Message = "UserName should be min 3 characters and Max 9 characters",
Priority = Prority.Low
});
}
if (!Name.All(x => char.IsLetterOrDigit(x))) {
errorList.Add(new ErrorInfo {
Message = "Username should contain only alphanumeric characters",
Priority = Prority.Medium
});
}
}
if (errorList.Any()) {
_errors[propertyName] = errorList;
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
return;
}
if (_errors.ContainsKey(propertyName)) _errors.Remove(propertyName);
}
The method checks through conditions we listed earlier and adds an instance ErrorInfo to the errorList. If there are validation errors, the method ensures the event ErrorsChanged is invoked to notify the View about the same. If there are no errors for the property, we will also ensure any existing validation errors in the dictionary are also removed.
Displaying the Errors and Priority
For the sake of example, we will use color-coded text to represent the priority of the validation messages. Let us go ahead and define the _Error Template _.
<ControlTemplate x:Key="FormErrorTemplate">
<StackPanel>
<AdornedElementPlaceholder x:Name="textBox"/>
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ErrorContent.Message}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="Orange"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ErrorContent.Priority}"
Value="{x:Static viewmodels:Prority.High}">
<Setter Property="Foreground" Value="Red"/>
</DataTrigger>
<DataTrigger Binding="{Binding ErrorContent.Priority}"
Value="{x:Static viewmodels:Prority.Low}">
<Setter Property="Foreground" Value="GreenYellow"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ControlTemplate>
As noticed in the Xaml above, we are using DataTriggers to change the forecolor of the display text depending on the Priority. Any validation ErrorInfo with Priority High is displayed a forecolor Red, while Orange represents Medium and GreenYellow represents the Low priority errors.
With our error template defined, it is time to consume the same in our control.
<TextBox Grid.Column="1" Text="{Binding Name, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}"
Validation.ErrorTemplate="{StaticResource FormErrorTemplate}"/>
That's all we would need to display Priority metainfo along with the Validation messages in WPF.
Of course, the Error Template could be altered to give it a more impressive representation with Icons but would leave that to the imagination of the individual developer. But I hope this post enables developers to understand a way for including Priority or any other useful information along with the validation message.