Exposing FluentValidation Results over IDataErrorInfo
IDataErrorInfo
interface is really handy when implementing data validation in WPF. There's great built in support in XAML for displaying validation information to the user when DataContext
implements IDataErrorInfo
- only ValidatesOnDataErrors
property needs to be set to True
on the Binding
:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Text="Name" Grid.Row="0" Grid.Column="0" />
<TextBox Text="{Binding Name, ValidatesOnDataErrors=True}"
Grid.Row="0" Grid.Column="1" />
<TextBlock Text="Surname" Grid.Row="1" Grid.Column="0" />
<TextBox Text="{Binding Surname, ValidatesOnDataErrors=True}"
Grid.Row="1" Grid.Column="1" />
<TextBlock Text="Phone number" Grid.Row="2" Grid.Column="0" />
<TextBox Text="{Binding PhoneNumber, ValidatesOnDataErrors=True}"
Grid.Row="2" Grid.Column="1" />
</Grid>
By default, controls with validation errors are rendered with red border, but they don't show the actual error message. This can be changed with a custom style applied to them:
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="Background" Value="Pink"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<Border BorderBrush="Red" BorderThickness="1">
<AdornedElementPlaceholder />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Of course there are many different ways to implement IDataErrorInfo
for a DataContext
. But since I've recently become quite fond of FluentValidation
library for implementing validators, I'm going to focus on using it for the rest of this post. Creating a basic validator in FluentValidation
usually takes only a couple of lines of code:
public ContactValidator()
{
RuleFor(login => login.Name).NotEmpty();
RuleFor(login => login.Surname).NotEmpty();
RuleFor(login => login.PhoneNumber).NotEmpty();
RuleFor(login => login.PhoneNumber).Length(9,30);
RuleFor(login => login.PhoneNumber).Must(phoneNumber =>
phoneNumber == null || phoneNumber.All(Char.IsDigit))
.WithMessage("'Phone number' must only contain digits.");
}
The easiest way of using it from IDataErrorInfo
, would be calling Validate
from the indexer and filtering the results by the requested property:
public string this[string columnName]
{
get
{
var result = _validator.Validate(this);
if (result.IsValid)
{
return null;
}
return String.Join(Environment.NewLine,
result.Errors.Where(error => error.PropertyName == columnName)
.Select(error => error.ErrorMessage));
}
}
Since there can be more than one ValidationFailure
for a single property, I'm joining them together into a single string with each ErrorMessage
in its own line.
This approach causes the Validate
method to be called for every binding with ValidatesOnDataErrors
enabled. If your validator does a lot of processing, this can add up to a lot of unnecessary validating. To avoid that, the Validate
method can instead be called every time a property on the DataContext
changes:
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
Validate();
}
}
private void Validate()
{
var result = _validator.Validate(this);
_errors = result.Errors.GroupBy(error => error.PropertyName)
.ToDictionary(group => group.Key,
group => String.Join(Environment.NewLine,
group.Select(error => error.ErrorMessage)));
}
The indexer now only needs to retrieve the cached validation results from the _errors
Dictionary
inside the DataContext
:
public string this[string columnName]
{
get
{
string error;
if (_errors.TryGetValue(columnName, out error))
{
return error;
}
return null;
}
}
The only code that doesn't really belong in the DataContext
is now inside the Validate()
method. Instead of just calling the Validator
, it also parses its results and caches them in a Dictionary
for future IDataErrorInfo
indexer calls. This can be fixed by extracting the parsing logic into an extension method that can be used from any DataContext
:
public static Dictionary<string, string> GroupByProperty(this
IEnumerable<ValidationFailure> failures)
{
return failures.GroupBy(error => error.PropertyName)
.ToDictionary(group => group.Key,
group => String.Join(Environment.NewLine,
group.Select(error => error.ErrorMessage)));
}
This makes DataContext
's Validate
method much simpler:
private void Validate()
{
_errors = _validator.Validate(this).Errors.GroupByProperty();
}
The same pattern can be applied for any DataContext
with a corresponding Validator
. With minor modifications it can be used even in cases when DataContext
wraps a model class with its own validator or composites multiple such model classes. This is a quite common scenario when using MVVM pattern.