Overview
In WPF, the interface IDataErrorInfo
can be used in a ViewModel
class to provide automatic error notifications to the user. There are also attributes that can be used on properties to validate user input, but the two are not integrated in WPF out of the box. Together they can create a highly maintainable way to display errors with minimal effort.
Introduction
I recently started a new project with two developers who had only started working with WPF a few months; they had also inherited a code base that they had to work with. I was given the task to create a simple form that is displayed when a button in a grid is pressed. I have used this task to create the infrastructure so as to make development and maintenance easier. One of these infrastructure pieces is the abstract
class for the ViewModel
. Initially I implemented this abstract class to support INotifyPropertyChanged
. Once I had the basic functionality working, I started working on the IDataErrorInfo
.
I had done something like this before, but had used the Enterprise Library from Microsoft, Interestingly enough, the existing ViewModel
already had validation attributes associated with the properties, just no interface to allow the view to display the errors.
I had a general Idea that I wanted a standard to handle validations, and it seemed that IDataErrorInfo
and the use of validation attributes was the way to go. The interface
for IDataErrorInfo
is as follows:
public interface IDataErrorInfo
{
string Error { get; }
string this[string columnName] { get; }
}
In the Microsoft documentation, the implementation has a huge case statement to find errors for a property—this is a bad smell. I know that I really did not want to try to maintain this case statement. Another bad smell is the use of string
to specify the property.
Did some searching and found something on the web which I hoped I would to use more or less intact. It used the validation property attributes that derived from the ValidationAttribute
class and the IDataErrroInfo
interface. I started to do some clean up and found that I was very dissatisfied with this solution. My biggest issue was performance—I binding to the Error
property and the implementation had a LINQ
statement for the Error
property that would process all the validations each time. The INotifyPropertyChange
will not trigger a refresh unless that is a noticeable change, like a change in its address. Because the getter was dynamic due to the LINQ
statement, it could result in recalculations many times even when not required. What I wanted was to maintain a memory of the previous calculation, and compare the two, and if they are different, only then recalculate the Error
property.
The design started with used a dictionary using a key of the property name and a value being basically an array of validators. I replaced this array with a class to contain the previous value and the array of validators, and they moved most of the processing into this class. I then replaced the dictionary with a class, and was able to reduce the body of the square-bracket operator for determining the validation of a property to a single line.
Implementation
The DataErrorInfoAttributeValidator
class is initialized in the constructor with the class itself, and a function that is executed when there is a change in the validation, and returns a string of all the errors separated a line return. The constructor is responsible for getting the properties using reflection for a collection validation item class and putting the instances of this class into a dictionary that has a key of the property name:
public DataErrorInfoAttributeValidator(IDataErrorInfo classInstance,
Action<string> updateErrorFunction)
{
_classInstance = classInstance;
_updateErrorFucntion = updateErrorFunction;
var validators = classInstance.GetType()
.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty)
.Select(i => new PropertyValidator(i, classInstance)).Where(j => j.Validators.Any());
_propertyValidations = validators.ToDictionary(p => p.PropertyName, p => p);
}
The class instance is required by the process to find the properties, and is required in the PropertyValidator
instances to find the values of the properties.
The PropertyValidator
class is where these validator attributes of type PropertyValidator
for the property are maintained, and this class also maintains the property that contains the last array of error notification strings. These strings are used to compare to the strings found when validation for the property is recalculated. The constructor of this class
actually extracts the validators and maintains a collection of validators for the property.
public PropertyValidator(PropertyInfo propertyInfo, object classInstance)
{
_propertyInfo = propertyInfo;
_classInstance = classInstance;
Validators = propertyInfo.GetCustomAttributes().Where(i => i is ValidationAttribute)
.Cast<ValidationAttribute>().ToArray();
Validators.Where(j => j is MethodValidator).Cast<MethodValidator>().ToList()
.ForEach(i => i.SetClassInstance(_classInstance));
ErrorMessages = new string[0];
}
After the Validators are found, then each of the found Validators are checked to see if they are type MethodValidator
. This is because the MethodValidator
class needs to have an instance of the class
available so that it can use reflection to find the Method
that is provided the class
as a string
(an instance cannot be specified since this is specified as an Attribute
.
As can be seen the ErrorMessages
for the class instance is initially set to an empty collection so that the null condition does not need to be handled.
To validate a property, the Validate
method of the DataErrorInfoAttributeValidator
class is called with the name of the property:
public string Validate(string propertyName)
{
if (_propertyValidations.ContainsKey(propertyName))
{
var propertyValidation = _propertyValidations[propertyName];
if (propertyValidation.CheckValidations())
{
Errors = _propertyValidations.SelectMany(i => i.Value.ErrorMessages).ToArray();
_updateErrorFucntion?.Invoke(string.Join(Environment.NewLine, Errors));
}
return string.Join(Environment.NewLine, propertyValidation.ErrorMessages);
}
return string.Empty;
}
This method finds the validation instance in the dictionary, and calls CheckValidations
method of that instance. The return value of this method indicates if the validation issues are the same as the previous time the method was called. If there is a difference, then the errors array is recalculated and the function that was passed when this class was initialized is called with a string that indicates all the errors for the class. The errors for the property are then returned (and empty string if no errors).
public bool CheckValidations()
{
var newValue = _propertyInfo.GetValue(_classInstance);
var newErrorMessages = Validators.Where(i => !i.IsValid(newValue))
.Select(j => GetErrorMessage(_propertyInfo.Name, j))
.OrderBy(i => i).ToArray();
var isChanged = !StringArraysEqual(newErrorMessages, ErrorMessages);
ErrorMessages = newErrorMessages;
return isChanged;
}
In the CheckValidations
method, reflection is used to get the value of the property. The IsValid
method of the validator indicates the validity of the value, and the Name
property supplies the error messages associated with the validator. The CheckValidation
method only returns true is the validations have changed since the last call, that way, the error messages are only regenerated when there has been a change in validation. A collection of error message strings is saved in the ErrorMessages
property for the DataErrorInfoAttributeValidator.Validate
method to use to calculate the return value. There is an error message in this collection for each failed validation. The Validate
method then joins these messages together with a return so that each error is on a different line. All the error messages for all the properties that have failed validation are then combined, and the Error property of the ViewModel
is update with a new string containing the error messages joined with return characters.
You will notice that the GetErrorMessage
method is used to get the error message, and the reason for this is that for the ValidationAttribute
, the error message string
is not required when specifying the attribute for the property. It seemed like instead of requiring that a message be provided, a message could be created using String.Format
and the name of the field:
private static string GetErrorMessage(string name, ValidationAttribute validator)
{
if (!string.IsNullOrWhiteSpace(validator.ErrorMessage)) return validator.ErrorMessage;
if (validator is RequiredAttribute) return
$"{FromCamelCase(name)} is required";
if (validator is RangeAttribute) return
$"{FromCamelCase(name)} is not between the range of {((RangeAttribute)validator).Minimum} and {((RangeAttribute)validator).Maximum}";
if (validator is MaxLengthAttribute) return
$"{name} text cannot be longer than {((MaxLengthAttribute) validator).Length} characters";
throw new NotImplementedException(
$"Validator {validator.GetType().Name} does have default error message or specific error message for property {name}");
}
Only the Required
attribute has a default message, and that was all that was required in the initial release of the initial form that I created; as I refine the designs I will add support for additional attributes.
The Method Validator
The MethodValidationAttribute
was created because there was a need to do some dynamic testing of values. It takes a string which specifies the method to use in validation. There currently in no separate way to specify the error message to be generated since I figure that there would be a different validation method for each case, but I can see there may be a good reason to include this capability.
public class MethodValidationAttribute
{
public MethodValidationAttribute(string methodName)
{
MethodName = methodName;
}
private MethodInfo _validationMethod;
private object _classInstance;
public void SetClassInstance(object instance)
{
_classInstance = instance;
if (_validationMethod == null)
{
_validationMethod = instance.GetType()
.GetMethod(MethodName, BindingFlags.Public | BindingFlags.NonPublic
| BindingFlags.Instance);
Debug.Assert(_validationMethod != null,
$"The method {MethodName} could not be found in class {_classInstance.GetType().Name}");
Debug.Assert(_validationMethod.ReturnParameter?.ParameterType == typeof(string),
$"The return type of method {MethodName} is of type
{_validationMethod.ReturnParameter.ParameterType.Name},
not type string in class {_classInstance.GetType().Name}");
}
}
public string MethodName { get; }
public override bool IsValid(object value)
{
if (_validationMethod == null) return true;
ErrorMessage = (string)_validationMethod.Invoke(_classInstance, null);
return (string.IsNullOrWhiteSpace(ErrorMessage));
}
}
The MethodValidationAttribute
needs the name of the method to use for validation, and an instance of the class
. The method name is passed as an argument when in the decorator for the property to be checked. The class
instance is provided when property is validated. To save processing, a pointer to the validation method is maintained so the method only has to be found the first time the property is validated. When the IsValid
is called, the validation method is executed using Reflection. The method needs to be written to return a non-empty string
or null
if there is no error, otherwise a error message to be associated with the error notification. To do this the IsValid
method assigns the ErrorMessage
property the return value of the method found using Reflection
.
I have found that there is a problem with the MethodValidationAttribute
, and that is clearly seen in the sample. If two properties being validated have an interdependence, then can get in the situation that an error is being shown for one field, and is corrected in another field but the error will still show in the first field, also if an value is changed to an error condition in a property, it will not be shown in the dependent property.
The Required Enum Attribute Validator
There is another custom validator included in the solution, the RequiredEnumAttribute
. This had to be created because there were cases where an enumeration was bound to a ComboBox and initial value was not and there was no "0" enumeration defined. The framework RequiredAttribute
does not catch this problem, so I created this class to handle this situation. If you have problem where the RequiredAttribute
is not working for an enumerated binding, try this decorator. See Required Enumeration Validation Attribute.
Interfacing to the DataErrorInfoAttributeValidator
There are to areas that need to be programmed to use the DataErrorInfoAttributeValidator
class: the ViewModel
and the View
.
The ViewModel
class implements the properties from the IDataErrorInfo
interface. The DataErrorInfoAttributeValidator
class needs to be instantiated, and this probably should be done as part of the class initialization since there may be errors that need to be communicated to the user because of initial errors in the model, including when creating a new instance. The Error property only needs to implement the INotifyPropertyChanged
when the Error
value is changed so that View
will know that the Error
value has changed. There are a number of ways to use and instantiate this class, but I have the class initialized on first use instead of as part of the initialization because it means that only have the overhead if an attempt is made to use the IDataErrorInfo
interface:
private DataErrorInfoAttributeValidator _propertyValidations;
private string _error;
private DataErrorInfoAttributeValidator GetValidators()
{
return _propertyValidations ?? (_propertyValidations
= new DataErrorInfoAttributeValidator(this));
}
#region IDataErrorInfo
public string this[string propertyName]
{
get { return GetValidators().Validate(propertyName); }
}
public string Error
{
get { GetValidators(); return _error; }
set { Set(ref _error, value); }
}
endregion
Using the DataErrorInfoAttributeValidator
class makes implementation for IDataErrorInfo
really cleaned up the ViewModel
—I actually have this code in an abstract
class that I use for a lot of the ViewModel
s in the project—the class
that implements both INotifyPropertyChanged
and IDataErrorInfo
. In the project I am working in, there is no framework that is being used, so do not have any of the base classes to support WPF development that most significant projects use. I know that there are still some issues with the code, but if you do not use a framework to help with INotifyPropertyChanged
, then may want to borrow this code.
XAML
The next part is what needs to be done in the View
to bind to the error information:
<TextBox Text="{Binding Value,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True}" />
I have only shown the binding for the Text
on the TextBox
. The UpdateSourceTrigger
set to PropertyChanged
is required to ensure that validation is done on each keystroke. This obviously will decrease performance, but only when the user changes the value, so performance impact is minimal. The ValidatesOnDataErrors=True
is required to cause the validation checking to occur.
I had implemented this in the applications I had worked on, and had gotten the red borders on a TextBox
with an error and the tool tip with the error description, but when I tried the same on my sample, I only got the red box with no tool tip. This was because straight WPF, without any themes, will display a red box around a control, but will not provide any error indication. What is needed to display the error in a ToolTip
is to provide a ControlTemplate
for the Validation.ErrorTemplate
property. I have created a Style
for this in the example:
<Style x:Key="Version1" TargetType="{x:Type Control}">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate x:Name="TextErrorTemplate">
<Border BorderBrush="Red" BorderThickness="2">
<AdornedElementPlaceholder/>
</Brder>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors).CurrentItem}"/>
</Trigger>
</Style.Triggers>
</Style>
Note that originally I had "Path=(Validation.Errors)[0].ErrorContent
" and discovered that this was causing issues, and changed to "Path=(Validation.Errors).CurrentItem
", which solved the problem. I found this solution at Binding to (Validation.Errors)[0] without Creating Debug Spew.
The second group of TextBox
has this style applied, while the first does not. If Theme
is being used, then in all likelihood you will not need an ErrorTemplate
to display the ErrorAdorner
with the errors in the ToolTip
. You may still want to change the way errors are displayed. The following is code that I found in a Theme
, and is probably a good way to define the ErrorTemplate
:
<ControlTemplate x:Key="ValidationTooltipTemplate">
<Grid SnapsToDevicePixels="True"
VerticalAlignment="Top">
<Border Background="Transparent"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Width="3" Height="3"/>
<AdornedElementPlaceholder x:Name="Holder"/>
<Border BorderBrush="{StaticResource ValidationTooltipOuterBorder}"
BorderThickness="1"
CornerRadius="{StaticResource ValidationTooltip_CornerRadius}"/>
<Path Data="M2,1 L6,1 6,5 Z"
Fill="{StaticResource ValidationInnerTick}"
Width="7" Height="7"
HorizontalAlignment="Right"
VerticalAlignment="Top"/>
<Path Data="M0,0 L2,0 7,5 7,7 Z"
Fill="{StaticResource ValidationOuterTick}"
Width="7" Height="7"
HorizontalAlignment="Right"
VerticalAlignment="Top"/>
<ToolTipService.ToolTip>
<ToolTip x:Name="PART_ToolTip"
DataContext="{Binding RelativeSource={RelativeSource Mode=Self},
Path=PlacementTarget.DataContext}"
Template="{StaticResource ErrorTooltipTemplate}"
Placement="Right"/>
</ToolTipService.ToolTip>
</Grid>
</ControlTemplate>
This same template can now be used to define the ErrorTemplate
for multiple controls. I have the code in the app.config file, and is used in the last group of TextBox
es. The way this is then used in a Style
used:
<Style x:Key="Version4"
TargetType="{x:Type Control}">
<Setter Property="Validation.ErrorTemplate"
Value="{StaticResource ValidationTooltipTemplate}" />
</Style>
You will also note that there is a Template
for the used for the ToolTIp
. In this Template
, the Background
for the ToolTip
is defined as a red, and the Foreground
is a white.
BaseViewModel
To make it real easy to incorporate this code into a project, I created a BaseViewModel
which implements INotifyPropertyChanged
and IDataErrorInfo
:
public abstract class BaseViewModel : INotifyPropertyChanged, IDataErrorInfo
{
#region INotifyPropertyChanged
...
...
...
#endregion
#region validation (IDataErrorInfo)
private DataErrorInfoAttributeValidator _propertyValidations;
private string _error;
private DataErrorInfoAttributeValidator GetValidators()
{
return _propertyValidations ?? (_propertyValidations
= new DataErrorInfoAttributeValidator(this, str => Error = str));
}
#region IDataErrorInfo
public string this[string propertyName] => GetValidators().Validate(propertyName);
public string Error
{
get { GetValidators(); return _error; }
set { Set(ref _error, value); }
}
#endregion
#endregion
}
Using this class
, I was able to very quickly include the DataErrorInfoAttributeValidator
into my code.
The Code for Converting from Camel Case to Spaces
Here is the code I used to convert a Camel Case name to a name with spaces before each capital letter:
private static string FromCamelCase(string value) =>
Regex.Replace(value,
@"(?<a>(?<!^)((?:[A-Z][a-z])|(?:(?<!^[A-Z]+)[A-Z0-9]+(?:(?=[A-Z][a-z])|$))|(?:[0-9]+)))", @" ${a}");
Sample
I have five different examples of ErrorTemplate
s for the sample, including the out of the box WPF. They are all bound to the same two properties in the ViewModel
(properties Key
and Value
). There is also a button that is only enabled if there are no errors. To do this I use a converter on the IDataErrorInfo
Error property that only enables the button if the Error
property is null, empty or white space. I also have an error adorner to the right of the button that is only visible if the Error
property is null, empty, or white space. I use the same converter for this, a converter that evaluates if a value is null, empty, or white space, and returns the appropriate value from the ConverterParameter
. The ToolTip
for the error adorner is bound to the Error
property.
In the ViewModel
for this form is:
public class ViewModel : BaseViewModel
{
public ViewModel() { }
[Required]
[MethodValidator("KeyAndValueDifferent")]
public string Key{get { return _key; }set { Set(ref _key, value); }}
private string _key;
[Required]
[MethodValidator("KeyAndValueDifferent")]
public string Value{get { return _value; }set { Set(ref _value, value); }}
private string _value;
private string KeyAndValueDifferent()
{
return (Key != null && Value != null && Key == Value ) ? "Key and value must be different" : null;
}
}
While the XAML for each TextBox
pair is similar to this:
<Border Margin="2"
BorderBrush="Gray"
BorderThickness="1 ">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="300" />
<ColumnDefinition Width="50" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Grid.ColumnSpan="3"
Margin="5 5 0 5"
Content="Default WPF Error Template" />
<Label Grid.Row="1"
Grid.Column="0"
Margin="5"
Content="Key" />
<TextBox Grid.Row="1"
Grid.Column="1"
Margin="5"
Text="{Binding Key,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True}" />
<Label Grid.Row="2"
Grid.Column="0"
Margin="5"
Content="Value" />
<TextBox Grid.Row="2"
Grid.Column="1"
Margin="5"
Text="{Binding Value,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True}" />
</Grid>
</Border>
Conclusion
The code in this sample provides a very easy to use way to display errors in WPF applications, and there is a lot of information presented to allow customization of error presentation. I hope this is useful to you.
History
- 09/12/2015: Initial Version
- 03/03/2016: Updated Source Code
- 03/04/2016: Added
MethodValidator
use to sample
- 05/27/2016: Camel case to common name used for field names in automatic error message, and added automatic message generation for the Range Validation
- 07/11/2016: Replaced
Path=(Validation.Errors)[0].ErrorContent
with Path=(Validation.Errors).CurrentItem
which eliminates debug spew.
- 08/02/2016: Added a specialized
RangeValidator
(RangeUnitValidator
) to that code that has a Unit argument.