This post is about how you can validate data in a WPF application using the System.ComponentModel.INotifyDataErrorInfo interface that was introduced in the .NET Framework 4.5 (the same interface has been present in Silverlight since version 4).
It is a common requirement for any user interface application that accepts user input to validate the entered information to ensure that it has the expected format and type. Since the .NET Framework 3.5 you have been able to use the IDataErrorInfo interface to validate properties of a view model or model that is bound to some element in the view. While this interface basically only provides the capability to return a string that specifies what is wrong with a single given property, the new INotifyDataErrorInfo interface gives you a lot more flexibility and should in general be used when implementing new classes.
/* The built-in System.ComponentModel.INotifyDataErrorInfo interface */
public
interface
INotifyDataErrorInfo
{
bool
HasErrors {
get
; }
event
EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
IEnumerable GetErrors(
string
propertyName);
}
The GetErrors method of the interface returns an IEnumerable that contains validation errors for the specified property or for the entire entity. You should always raise the ErrorsChanged event whenever the collection returned by the GetErrors method changes. If the source of a two-way data binding implements the INotifyDataErrorInfo interface and the ValidatesOnNotifyDataErrors property of the binding is set to true (which it is by default), the WPF 4.5 binding engine automatically monitors the ErrorsChanged event and calls the GetErrors method to retrieve the updated errors once the event is raised from the source object, provided that the HasErrors property returns true.
Below is an example of a simple service with a single method that validates a username by first querying a database to determine whether it is already in use or not and then checks the length of it and finally determines whether it contains any illegal characters by using a regular expression. The method returns true or false depending on whether the validation succeeded or not and it also returns a collection of error messages as an out parameter. Declaring an argument as out is useful when you want a method in C# to return multiple values.
IService
ValidateUsername(
username,
out
ICollection<
> validationErrors);
class
Service : IService
> validationErrors)
validationErrors =
new
List<
>();
int
count = 0;
using
(SqlConnection conn =
SqlConnection(ConfigurationManager.ConnectionStrings[0].ConnectionString))
SqlCommand cmd =
SqlCommand(
"SELECT COUNT(*) FROM [Users] WHERE Username = @Username"
, conn);
cmd.Parameters.Add(
"@Username"
, SqlDbType.VarChar);
cmd.Parameters[
].Value = username;
conn.Open();
count = (
)cmd.ExecuteScalar();
if
(count > 0)
validationErrors.Add(
"The supplied username is already in use. Please choose another one."
);
/* Verifying that length of username */
(username.Length > 10 || username.Length < 4)
"The username must be between 4 and 10 characters long."
/* Verifying that the username contains only letters */
(!Regex.IsMatch(username, @
"^[a-zA-Z]+$"
))
"The username must only contain letters (a-z, A-Z)."
return
validationErrors.Count == 0;
The following view model implementation of the INotifyDataErrorInfo interface then uses this service to perform the validation asynchronously. Besides a reference to the service itself, it has a Dictionary<string, ICollection<string>> where the key represents a name of a property and the value represents a collection of validation errors for the corresponding property.
ViewModel : INotifyDataErrorInfo
private
readonly
IService _service;
Dictionary<
, ICollection<
>>
_validationErrors =
>>();
ViewModel(IService service)
_service = service;
...
#region INotifyDataErrorInfo members
void
RaiseErrorsChanged(
propertyName)
(ErrorsChanged !=
null
)
ErrorsChanged(
this
,
DataErrorsChangedEventArgs(propertyName));
System.Collections.IEnumerable GetErrors(
(
.IsNullOrEmpty(propertyName)
|| !_validationErrors.ContainsKey(propertyName))
;
_validationErrors[propertyName];
HasErrors
_validationErrors.Count > 0; }
#endregion
The setter of a Username property of the view model is then using a private method to call the service method asynchronously using the async and await keywords – these were added to introduce a simplified approach to asynchronous programming in the .NET Framework 4.5 and the Windows Runtime (WinRT) – and update the dictionary based on the result of the validation:
_username;
Username
_username; }
set
_username = value;
ValidateUsername(_username);
async
username)
const
propertyKey =
"Username"
> validationErrors =
/* Call service asynchronously */
isValid = await Task<
>.Run(() =>
_service.ValidateUsername(username,
validationErrors);
})
.ConfigureAwait(
false
(!isValid)
/* Update the collection in the dictionary returned by the GetErrors method */
_validationErrors[propertyKey] = validationErrors;
/* Raise event to tell WPF to execute the GetErrors method */
RaiseErrorsChanged(propertyKey);
else
(_validationErrors.ContainsKey(propertyKey))
/* Remove all errors for this property */
_validationErrors.Remove(propertyKey);
If a user enters an invalid username and the validation fails, a validation error will occur and a visual feedback will be provided to the user to indicate this. By default you will see a red border around the UI element when this happens:
The actual message that is describing the error is stored in the ErrorContent property of a System.Windows.Controls.ValidationError object that is added to the Validation.Errors collection of the bound element by the binding engine at runtime. When the attached property Validation.Errors has ValidationError objects in it, another attached property named Validation.HasError returns true.
To be able to see the error messages in the view you can replace the default control template that draws the red border around the element with your own custom template by setting the Validation.ErrorTemplate attached property of the control. You typically use an ItemsControl present a collection of items in XAML:
<
TextBox
Text
=
"{Binding Username, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}"
>
Validation.ErrorTemplate
ControlTemplate
StackPanel
<!-- Placeholder for the TextBox itself -->
AdornedElementPlaceholder
x:Name
"textBox"
/>
ItemsControl
ItemsSource
"{Binding}"
ItemsControl.ItemTemplate
DataTemplate
TextBlock
"{Binding ErrorContent}"
Foreground
"Red"
</
Note that the Validation.ErrorTemplate will be displayed on the adorner layer. Elements in the adorner layer are rendered on top of the rest of the visual elements and they will not be considered when the layout system is measuring and arranging the controls on the adorned element layer. The adorned element in this case is the TextBox control itself and you include an AdornedElementPlaceholder in the control template where you want to leave space for it. The template above will cause any validation error messages to be displayed below the TextBox. The TextBlocks containing the validation error messages rendered by the ItemsControl will appear on top of any elements that are located right below the TextBox as adorners are always visually on top.
As mentioned, the INotifyErrorDataError interface also makes it possible to return error objects of any type from the GetErrors method and this can be very useful when you want to present some custom error reporting in the view. Consider the following sample type that has a string property that describes the validation error and an additional property of enumeration type that specifies the severity of the error:
CustomErrorType
CustomErrorType(
validationMessage, Severity severity)
.ValidationMessage = validationMessage;
.Severity = severity;
ValidationMessage {
Severity Severity {
enum
Severity
WARNING,
ERROR
/* The service method modifed to return objects of type CustomErrorType instead of System.String */
ICollection<CustomErrorType> validationErrors)
List<CustomErrorType>();
/* query database as before */
, Severity.ERROR));
"The username should be between 4 and 10 characters long."
, Severity.WARNING));
If you use the same ErrorTemplate as shown above to present validation errors of the above type, you will see the ToString() representation of it when an error has been detected. You can choose to override the ToString() method of the custom type to return an error message or simply adjust the template to fit the custom type. Below is for example how you could change the color of a validation error message based on the Severity property of the CustomErrorType object returned by the ErrorContent property of a ValidationError object in the Validation.Errors collection:
xmlns:local
"clr-namespace:WpfApplication1"
"{Binding ErrorContent.ValidationMessage}"
TextBlock.Style
Style
TargetType
"{x:Type TextBlock}"
Setter
Property
"Foreground"
Value
Style.Triggers
DataTrigger
Binding
"{Binding ErrorContent.Severity}"
"{x:Static local:Severity.WARNING}"
"Orange"
As the GetErrors method returns a collection of validation errors for a given property, you can also easily perform cross-property validation – in cases where a change to a property value may cause an error in another property – by adding appropriate errors to the dictionary, or whatever collection you are using to store the validation error objects, and then tell the binding engine to re-call this method by raising the ErrorsChanged event.
This is illustrated in the below sample code where the Interest property is only mandatory when the Type property has a certain value and the validation of the Interest property occurs whenever either of the properties are set.
Int16 _type;
Int16 Type
_type; }
_type = value;
ValidateInterestRate();
decimal
? _interestRate;
? InterestRate
_interestRate; }
_interestRate = value;
dictionaryKey =
"InterestRate"
validationMessage =
"You must enter an interest rate."
/* The InterestRate property must have a value only if the Type property is set to 1 */
ValidateInterestRate()
(_type.Equals(1) && !_interestRate.HasValue)
(_validationErrors.ContainsKey(dictionaryKey))
_validationErrors[dictionaryKey].Add(validationMessage);
_validationErrors[dictionaryKey] =
> { validationMessage };
_validationErrors.Remove(dictionaryKey);