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.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License