Click here to Skip to main content
15,867,568 members
Articles / Desktop Programming / WPF

Dynamic Validation with FluentValidation in WPF/MVVM

Rate me:
Please Sign up or sign in to vote.
5.00/5 (12 votes)
4 Jan 2016CPOL10 min read 44.8K   1.4K   18   2
This article shows how user inputs can be validated dynamically with FluentValidation and INotifyDataErrorInfo in a WPF/MVVM application.

Image 1

Introduction

Validating user inputs and providing good feedback is very important to any product level software applications. This article shows how user inputs can be validated dynamically – meaning that some parts of the validation rules can change at run time – by using FluentValidation and INotifyDataErrorInfo in a WPF/MVVM application. The demo application written for the article solves the Euler Project’s problem 5 with a little twist – allowing the user to change the two numbers in the subject against which the program has to validate and provide feedback, hence demonstrating the capabilities.

Background

WPF has been supporting validations in the data binding pipeline since its inception – e.g., ValidationRules, ErrorTemplate, and so on. There are, however, some issues with them. First of all, the binding engine throws a type-cast exception (and swallows it by default) when you bind the TextBox.Text property to a numeric property, such as int and double, of the source object and the user types in some alpha characters or leaves the text box blank. As Josh Smith pointed out years ago, not all users can understand the default error messages that can be displayed in a ToolTip as a result of the type-cast failure as shown below:

Image 2

Image 3

The first ToolTip is displayed when you set the Binding.ValidatesOnExceptions property to true and the second when false – i.e., doing nothing in the Binding statement.

You could override the message by adding a custom ValidationRule to the Binding with the desired message such that it would stop the validation process before the Binding engine tries to type-cast. Still, there can be a lot more ValidationRules you would want to add after a successful type-casting. For example, you might want to constrain the value within a certain range, or to validate the value whose valid range changes dynamically depending on some other property values. As the validation rules become more complex, they would not only scatter over multiple classes, but also be harder to debug.

That was when I found an open-source library called FluentValidation written by Jeremy Skinners that allows us to put all input validations on a bound object in a single class where every validation rule can be stated declaratively in a fluent syntax. So, I decided to start this project to see if the library could satisfy the following requirements:

  1. All validations, including type-cast exceptions, should be handled in a single place. This rules out the use of custom ValidationRules. It also means that every validation must occur within the setter function of the string property, rather than int or double.
  2. The object that owns the string properties – be it a view model or model – should implement INotifyDataErrorInfo, rather than the older IDataErrorInfo. The Binding engine does support IDataErrorInfo. But to use it, we have to set Binding.ValidatesOnDataErrors property to true. Also, its Error property is never used. On the other hand, the Binding engine supports INotifyDataErrorInfo by default, without adding anything to the Binding. All we have to do is to implement the interface in the bound object.
  3. Every error message that is relevant to a single UI element should be displayed in a ToolTip. The offensive UI element should be decorated by a red border as done by ErrorTemplate by default.
  4. All error messages should be consolidated and displayed somewhere so that the user can immediately see the problems without hovering the mouse over the offensive UI elements, as is done by the Silverlight’s ValidationSummary, which is very neat but not implemented in WPF for some reason.
  5. Validation must take place whenever the user changes the texts. This means that the Binding.UpdateSourceTrigger property is set to PropertyChanged.
  6. Validation rules that spans over multiple properties should also be supported. For example, property A must be greater than property B + C.
  7. Runtime changes on validation rules should be observed. For example, property A’s allowable maximum value can change from 10 to 20 depending on some other situations at runtime. This rules out the use of DataAnnotations where validation rules are attached as attributes of the property and so are fixed and cannot be changed at runtime.

Results were satisfactory and produced the following single class:

public class Problem5Validator : FluentValidator<Problem5>
{
    private const string FromProperty = "'N'";
    private const string ToProperty = "'M'";
    private const string CannotBeLeftBlank = " cannot be left blank.";
    private const string MustBeValidWholeNumber = " must be a valid whole number.";
    private const string MustBeLessThan = " must be less than ";
    private const string MustBeGreaterThan = " must be greater than ";
 
    public Problem5Validator()
    {
        this.CascadeMode = CascadeMode.StopOnFirstFailure;
 
        this.RuleFor(x => x.From)
            .NotEmpty()
            .WithMessage(FromProperty + CannotBeLeftBlank)
            .Must(a => a.IsInteger())
            .WithMessage(FromProperty + MustBeValidWholeNumber)
            .Must(
                (x, a) =>
                {
                    var from = int.Parse(a);
                    return x.MinFrom <= from && from <= x.MaxFrom;
                })
            .WithMessage("{0} <= " + FromProperty + " <= {1}", x => x.MinFrom, x => x.MaxFrom)
            .Must(
                (x, a) =>
                {
                    // We validate this rule only when the "To" parameter is a valid integer.
                    int to;
                    if (int.TryParse(x.To, out to) && x.MinTo <= to && to <= x.MaxTo)
                    {
                        return int.Parse(a) < to;
                    }
 
                    // If "To" parameter is invalid, we shouldn't show the error message.
                    return true;
                })
            .WithMessage(FromProperty + MustBeLessThan + ToProperty + ".");
 
        this.RuleFor(x => x.To)
            .NotEmpty()
            .WithMessage(ToProperty + CannotBeLeftBlank)
            .Must(a => a.IsInteger())
            .WithMessage(ToProperty + MustBeValidWholeNumber)
            .Must(
                (x, a) =>
                {
                    var to = int.Parse(a);
                    return x.MinTo <= to && to <= x.MaxTo;
                })
            .WithMessage("{0} <= " + ToProperty + " <= {1}", x => x.MinTo, x => x.MaxTo)
            .Must(
                (x, a) =>
                {
                    // We validate this rule only when the "From" parameter is a valid integer.
                    int from;
                    if (int.TryParse(x.From, out from) && x.MinFrom <= from && from <= x.MaxFrom)
                    {
                        return int.Parse(a) > from;
                    }
 
                    // If "From" parameter is invalid, we shouldn't show the error message.
                    return true;
                })
            .WithMessage(ToProperty + MustBeGreaterThan + FromProperty + ".");
    }
}

As you can see, all validations from the string-to-int type conversions to range validation to relation validation with the other property are handled within the same class. Clicking the "Change validation range" button will change the valid ranges and immediately revalidate the properties.

However, the intentional use of the string properties prevented me from taking advantage of the library’s built-in validators such as "GreaterThan()" and "LessThan()", much to my chagrin. I had to use "Must()" with a lambda where the custom rules with explicit conversions from string to integer are implemented. Still, the library’s customizability and configuarability allowed me to do so without losing much of the "fluentness". You wouldn’t have this hassle if the properties are numeric, or there is no need to dynamically change some of the parameters of the validation rules.

Using the Code

The sample application consists of three projects – main UI, infrastructure, and business. MVVM is used throughout. No particular MVVM framework is used. The BindableBase class and DelegateCommand class in the infrastructure project can easily be replaced by the equivalent classes in your favorite MVVM framework such as Prism and MVVM Light.

Now, let’s start looking at the ValidatableBindableBase class code that allows classes that inherit from it to validate properties whenever the setter is called with a new value:

public abstract class ValidatableBindableBase : BindableBase
{
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
 
    public abstract void ValidateAllProperties();
 
    protected virtual bool SetPropertyAndValidateAllProperties<T>(
        ref T storage,
        T value,
        [CallerMemberName] string propertyName = null)
    {
        // ReSharper disable once ExplicitCallerInfoArgument
        var result = this.SetProperty(ref storage, value, propertyName);
 
        if (result)
        {
            this.ValidateAllProperties();
        }
 
        return result;
    }
 
    protected virtual void OnErrorsChanged(DataErrorsChangedEventArgs e)
    {
        var handler = this.ErrorsChanged;
        if (handler != null)
        {
            handler(this, e);
        }
    }
}

SetPropertyAndValidateAllProperties() function calls ValidateAllProperties(), that must be implemented in derived classes, when a different value is set to the property. It also houses ErrorsChanged event invocator that is needed to support INotifyDataErrorInfo. The INotifyPropertyChanged is implemented in the BindableBase class as is usual.

Problem5 – the model class

The Problem5 is a model class that inherits from ValidatableBindableBase:

public class Problem5 : ValidatableBindableBase, INotifyDataErrorInfo
{
    private readonly IValidator<Problem5> validator;
    private string from;
    private string to;
    private string result;
 
    public Problem5(IValidator<Problem5> validator)
    {
        this.validator = validator;
        this.validator.ErrorsChanged += (s, e) => this.OnErrorsChanged(e);
        this.ClearResult();
 
        this.MaxFrom = 10;
        this.MinFrom = 1;
        this.MaxTo = 100;
        this.MinTo = 2;
        this.From = "1";
        this.To = "20";
    }
 
    public int MaxFrom { get; set; }
 
    public int MinFrom { get; set; }
 
    public int MaxTo { get; set; }
 
    public int MinTo { get; set; }
 
    public string From
    {
        get
        {
            return this.from;
        }
        
        set 
        {
            if (this.SetPropertyAndValidateAllProperties(ref this.from, value))
            {
                this.ClearResult();
            } 
        }
    }
 
    public string To
    {
        get
        {
            return this.to;
        }
 
        set
        {
            if (this.SetPropertyAndValidateAllProperties(ref this.to, value))
            {
                this.ClearResult();
            }
        }
    }
 
    public string Result
    {
        get { return this.result; }
        set { this.SetProperty(ref this.result, value); }
    }
 
    public bool HasErrors
    {
        get { return this.validator.HasErrors; }
    }
 
    public void Solve()
    {
        this.Result = Solver.Solve(int.Parse(this.From), int.Parse(this.To)).ToString("D");
    }
 
    public IEnumerable GetErrors(string propertyName)
    {
        return this.validator.GetErrors(propertyName);
    }
 
    public IList<string> GetAllErrors()
    {
        return this.validator.GetAllErrors();
    }
 
    public override void ValidateAllProperties()
    {
        this.validator.Validate(this);
    }
 
    private void ClearResult()
    {
        this.Result = string.Empty;
    }
}

It has three string properties (From, To, and Result) that can be directly data-bound to UI elements. It, of course, can solve the Euler project problem #5, which is the business purpose of this application. But the responsibility of validating the input properties and implementing the INotifyDataErrorInfo interface is delegated to the validator object that is declared as a dependency in the constructor with the type of IValidator<Problem5>, which has the following signature:

public interface IValidator<in T> : INotifyDataErrorInfo
{
    IDictionary<string, string> Validate(T instance);
 
    IList<string> GetAllErrors();
}

This tells us that the model class depends on (or uses) something that implements not only INotifyDataErrorInfo but also two functions, one of which actually validates the entire model. The validator is supposed to raise the ErrorsChanged event as a result of validation, if errors are actually changed. The Problem5 class just relays it so that the UI can show appropriate error messages.

Additionally, the class has four validation related parameters – MaxFrom, MinFrom, MaxTo, and MinTo that specify the valid ranges of the From and To parameters, respectively. Alternatively, you can implement those four validation related parameters inside the Problem5Validator class, rather than in the model class. In that case, you will need to change lambdas in WithMessage() such as “x => x.MinTo” to “x => this.MinTo”.

FluentValidator<T> class

The infrastructure project’s FluentValidator<T> class inherits from the FluentValidation’s AbstractValidator<T> and at the same time implements IValidator<T>:

public class FluentValidator<T> : AbstractValidator<T>, IValidator<T>
{
    private readonly Dictionary<string, string> errors = new Dictionary<string, string>();
 
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
 
    public bool HasErrors
    {
        get { return this.errors.Count > 0; }
    }
    
    IDictionary<string, string> IValidator<T>.Validate(T instance)
    {
        var currentErrors = new Dictionary<string, string>(this.errors);
        this.ValidateAndUpdateErrors(instance);
        this.RaiseErrorsChangedIfReallyChanged(currentErrors, this.errors);
        this.RaiseErrorsChangedIfReallyChanged(this.errors, currentErrors);
 
        return this.errors;
    }
 
    public IEnumerable GetErrors(string propertyName)
    {
        if (string.IsNullOrEmpty(propertyName))
        {
            // The caller requests all errors associated with this object.
            return this.GetAllErrors();
        }
 
        ThrowIfInvalidPropertyName(propertyName);
 
        return this.ExtractErrorMessageOf(propertyName);
    }
 
    public IList<string> GetAllErrors()
    {
        return this.errors.Select(error => error.Value).ToList();
    }
 
    protected virtual void OnErrorsChanged(DataErrorsChangedEventArgs e)
    {
        var handler = this.ErrorsChanged;
        if (handler != null)
        {
            handler(this, e);
        }
    }
 
    private static void ThrowIfInvalidPropertyName(string propertyName)
    {
        var propertyInfo = typeof(T).GetRuntimeProperty(propertyName);
        if (propertyInfo == null)
        {
            var msg = string.Format("No such property name '{0}' in {1}", propertyName, typeof(T));
            throw new ArgumentException(msg, propertyName);
        }
    }
 
    private void ValidateAndUpdateErrors(T instance)
    {
        this.errors.Clear();
        var result = this.Validate(instance);
        if (result.IsValid)
        {
            return;
        }
 
        foreach (var err in result.Errors)
        {
            this.errors.Add(err.PropertyName, err.ErrorMessage);
        }
    }
 
    private void RaiseErrorsChangedIfReallyChanged(
        IEnumerable<KeyValuePair<string, string>> errors1,
        IReadOnlyDictionary<string, string> errors2)
    {
        foreach (var err in errors1)
        {
            var propertyName = err.Key;
            var message = err.Value;
            if (!errors2.ContainsKey(propertyName) || !errors2[propertyName].Equals(message))
            {
                this.RaiseErrorsChanged(propertyName);
            }
        }
    }
 
    private void RaiseErrorsChanged(string propertyName)
    {
        this.OnErrorsChanged(new DataErrorsChangedEventArgs(propertyName));
    }
 
    private IEnumerable ExtractErrorMessageOf(string propertyName)
    {
        var result = new List<string>();
        if (this.errors.ContainsKey(propertyName))
        {
            result.Add(this.errors[propertyName]);
        }
 
        return result;
    }
}

This class basically converts the AbstractValidator<T>’s validation results so that the INotifyDataErrorInfo can understand. The AbstractValidator<T>.Validate(T) returns an object of ValidationResult, which is a class defined in the FluentValidation libraray, has just two members (IsValid Booelan and Errors collection), and has nothing to do with a WPF’s class that bears the same name. The ValidateAndUpdateErrors() shows how to harvest the validation results:

private void ValidateAndUpdateErrors(T instance)
{
    this.errors.Clear();
    var result = this.Validate(instance);
    if (result.IsValid)
    {
        return;
    }
 
    foreach (var err in result.Errors)
    {
        this.errors.Add(err.PropertyName, err.ErrorMessage);
    }
}

If the result of validation is not valid, we put the errors in the Dictionary<string, string> errors dictionary whose key is the property name and the value is the actual error message in string.

On the other hand, INotifyDataErrorInfo requires three members (HasErrors Boolean, GetErrors(propertyName) function that has to return an IEnumerable, and ErrorsChanged event). Implementing HasErrors is easy – just returning if the errors dictionary has any content. Implementing GetErrors(propertyName) needs some thought because it has two modes – one with the propertyName set to the actual property name, and the other with the parameter set to string.Empty in which case we have to return all errors relevant to the bound object. The implementation distinguishes them clearly:

public IEnumerable GetErrors(string propertyName)
{
    if (string.IsNullOrEmpty(propertyName))
    {
        // The caller requests all errors associated with this object.
        return this.GetAllErrors();
    }
 
    ThrowIfInvalidPropertyName(propertyName);
 
    return this.ExtractErrorMessageOf(propertyName);
}

In reality, the Binding engine always calls GetErrors() with the actual property name unless you use a BindingGroup with a custom ValidationRule. As I mentioned earlier, I decided not to use any ValidationRules. So, it is irrelevant. But having a method that returns all errors can be useful in other places.

The RaiseErrorsChangedIfReallyChanged() implements the ErrorsChanged event where the contents of the errors dictionary is compared before and after the Validate() call.

I used the explicit interface implementation for the Validate(T instance) function because the base class already has a function with the same name with a different return value.

Problem5Validator class

The Problem5Validator inherits from FluentValidator<Problem5> and states all of the validation rules that are relevant to the specific client object declaratively as can be seen in any FluentValidation examples and tutorials found on the Internet. The code was shown in the Background section of this article.

There are some things in the code that are worth mentioning.

  1. CascadeMode.StopOnFirstFailure
    By default, all cascaded validators that are attached to a particular property carries out validations. It would have been completely useless, if the library did not have the capability to change the mode. The validators that validate the numeric range and relation cannot execute if the string property cannot be recognized as a number. "Must(a => a.IsInteger())" validator stops validation right there, if the property is not an integer.
  2. Accessing property value
    The "Must()" validator allows the user to access the property value in lambda.
  3. Accessing instance object
    Both "Must()" validator and "WithMessage()" message formatter allow the user to access the instance object. This is how I access the valid ranges of the properties at runtime, contributing to dynamic validation that cannot be done with DataAnnotations.

MainWindowViewModel class

Finally, the MainWindowViewModel class instantiates the Problem5 model object with the appropriate validator. When a DI container is available, as is the case with Prism and MVVM Light frameworks, you can just specify it in its constructor as a dependency. The view model exposes the object, two commands, and AllErrors collection to the corresponding View class – i.e., MainWindow. Whenever the ErrorsChanged is raised, it replaces the AllErrors collection and also controls the "Solve" button’s IsEnabled property by raising the CanExecuteChanged of the DelegateCommand.

Note that AllErrors collection does not need to be an ObservableCollection<string> because it is simply replaced with a new one, rather than items are added/removed to/from the same collection object, every time errors are changed.

Conclusion

Avoiding the Binding engine’s type-cast exceptions is, in my opinion, crucial in order to be able to handle all validations in a single location and to provide the user consistent error messages. To do so, all numeric properties of the models need to be exposed as strings, following Josh Smith’s recommendation. When doing so, WPF’s ValidationRule objects have no place to serve. Instead, the FluentValidation library can nicely handle all validations, which are carried out in the bound properties’ setter functions. Once validated, the Binding engine smoothly works with INotifyDataErrorInfo that the bound model/view model objects implement so that we can easily show validation results in ToolTip and in ItemsControl.

I thank Jeremy Skinners for sharing such an excellent library with us.

Reference

History

1/4/2016: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Japan Japan
He started his career as a PDP-11 assembly language programmer in downtown Tokyo, learning what "patience" in real life means by punching a 110 baud ASR-33 Teletype frantically. He used to be able to put in the absolute loader sequence through the switch panel without consulting the DEC programming card.

Since then, his computer language experiences include 8051 assembly, FOCAL, BASIC, FORTRAN-IV, Turbo/MS C, VB. VB.NET, and C#.

Now, he lives with his wife, two grown-up kids (get out of my place!), and two cats in Westerville, Ohio.

Comments and Discussions

 
QuestionHow to handle Validation.HasError property in ListView Pin
Stan1k22-Mar-21 0:45
Stan1k22-Mar-21 0:45 
SuggestionUsing dictionary fails when more then one error on a field Pin
Member 811978123-Mar-16 11:31
Member 811978123-Mar-16 11:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.