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

Attributes-based Validation in a WPF MVVM Application

Rate me:
Please Sign up or sign in to vote.
4.88/5 (30 votes)
28 Jul 2010CPOL6 min read 223.5K   7.2K   101   50
Description of a method which uses attribute to perform user entry validation in a WPF MVVM application

Introduction

In this article, I'm sharing a simple MVVM application with validation using an attribute-based approach. Basically, it means that you'll be able to describe validation rules using this syntax:

C#
[Required(ErrorMessage = "Field 'FirstName' is required.")]
public string FirstName
{
    get
    {
        return this.firstName;
    }
    set
    {
        this.firstName = value;
        this.OnPropertyChanged("FirstName");
    }
}

Here is a picture of the demo application running:

Image 1

The UI requirements for the demo application are:

  • Fill various information in 3 different tabs and provide error meaningful messages and feedback to the user when a field is incorrect:

    Image 2

  • Give real time feedback about the completeness of the filling process using 3 approaches:
    • When a tab is fully completed, it goes from red to green:

      Image 3

    • The progress is shown to the user using a progress bar:

      Image 4

    • When everything is filled, the Save button is activated:

      Image 5

Background

WPF provides several techniques in order to validate what the user enters in an application.

ValidationRules

From the MSDN article on ValidationRules:

When you use the WPF data binding model, you can associate ValidationRules with your binding object. To create custom rules, make a subclass of this class and implement the Validate method. The binding engine checks each ValidationRule that is associated with a binding every time it transfers an input value, which is the binding target property value, to the binding source property.

IDataErrorInfo

From the original blog post of Marianor about using attributes with IDataErrorInfo interface:

WPF provides another validation infrastructure for binding scenarios through IDataErrorInfo interface. Basically, you have to implement the Item[columnName] property putting the validation logic for each property requiring validation. From XAML, you need to set ValidatesOnDataErrors to true and decide when you want the binding invoke the validation logic (through UpdateSourceTrigger).

Combining IDataErrorInfo and attributes

In this article, we use a technique which combines validation attributes (an explanation of each validation attribute is out of the scope of this article) and the IDataErrorInfo interface.

Overall Design

Here is the class diagram of the application’s classes:

Image 6

There is nothing really new here. The MainWindow’s Content is set to the MainFormView view. The associated ViewModel, MainFormViewModel manages a set of FormViewModel which is the base ViewModel class for each tab in the View.

In order to specify to the WPF engine how to render a FormViewModelBase, all we need to do is to create a DataTemplate and gives the correct TargetType. For example, in order to associate the ProfileFormViewModel with the ProfileFormView, we create this DataTemplate:

XML
<DataTemplate DataType="{x:Type ViewModel:ProfileFormViewModel}">
<View:ProfileFormView />
</DataTemplate>

Then the DataBinding does everything to populate the content of the TabControl using this very simple XAML:

XML
<TabControl ItemsSource="{Binding Forms}" />

This is very similar to an approach I blogged a couple of months ago (see here for more details). The new stuff resides in the ValidationViewModelBase class which is the new base class I’m introducing for ViewModel classes which need to support validation. That’s the goal of the next section of this article.

Attribute-based Validation and the IDataError Interface

The solution I’m using here to combine attributes and IDataError interface is based on the work of Marianor (full article here). I tweaked a little bit of his code in order to have a generic solution (and that’s the goal of the ValidationViewModelBase class). The basic idea is to be able to implement the IDataErrorInfo interface (and its 2 properties) in a generic way using attributes in System.ComponentModel. The class uses extensively LINQ in order to perform the validation.

C#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;

using WpfFormsWithValidationDemo.Toolkit.Behavior;

namespace WpfFormsWithValidationDemo.Toolkit.ViewModel
{
    /// <summary>
    /// A base class for ViewModel classes which supports validation 
    /// using IDataErrorInfo interface. Properties must defines
    /// validation rules by using validation attributes defined in 
    /// System.ComponentModel.DataAnnotations.
    /// </summary>
    public class ValidationViewModelBase : ViewModelBase, 
		IDataErrorInfo, IValidationExceptionHandler
    {
        private readonly Dictionary<string, 
		Func<ValidationViewModelBase, object>> propertyGetters;
        private readonly Dictionary<string, ValidationAttribute[]> validators;

        /// <summary>
        /// Gets the error message for the property with the given name.
        /// </summary>
        /// <param name="propertyName">Name of the property</param>
        public string this[string propertyName]
        {
            get
            {
                if (this.propertyGetters.ContainsKey(propertyName))
                {
                    var propertyValue = this.propertyGetters[propertyName](this);
                    var errorMessages = this.validators[propertyName]
                        .Where(v => !v.IsValid(propertyValue))
                        .Select(v => v.ErrorMessage).ToArray();

                    return string.Join(Environment.NewLine, errorMessages);
                }

                return string.Empty;
            }
        }

        /// <summary>
        /// Gets an error message indicating what is wrong with this object.
        /// </summary>
        public string Error
        {
            get
            {
                var errors = from validator in this.validators
                             from attribute in validator.Value
                             where !attribute.IsValid(this.propertyGetters
				[validator.Key](this))
                             select attribute.ErrorMessage;

                return string.Join(Environment.NewLine, errors.ToArray());
            }
        }

        /// <summary>
        /// Gets the number of properties which have a 
        /// validation attribute and are currently valid
        /// </summary>
        public int ValidPropertiesCount
        {
            get
            {
                var query = from validator in this.validators
                            where validator.Value.All(attribute => 
			  attribute.IsValid(this.propertyGetters[validator.Key](this)))
                            select validator;

                var count = query.Count() - this.validationExceptionCount;
                return count;
            }
        }

        /// <summary>
        /// Gets the number of properties which have a validation attribute
        /// </summary>
        public int TotalPropertiesWithValidationCount
        {
            get
            {
                return this.validators.Count();
            }
        }

        public ValidationViewModelBase()
        {
            this.validators = this.GetType()
                .GetProperties()
                .Where(p => this.GetValidations(p).Length != 0)
                .ToDictionary(p => p.Name, p => this.GetValidations(p));

            this.propertyGetters = this.GetType()
                .GetProperties()
                .Where(p => this.GetValidations(p).Length != 0)
                .ToDictionary(p => p.Name, p => this.GetValueGetter(p));
        }

        private ValidationAttribute[] GetValidations(PropertyInfo property)
        {
            return (ValidationAttribute[])property.GetCustomAttributes
			(typeof(ValidationAttribute), true);
        }

        private Func<ValidationViewModelBase, object> 
			GetValueGetter(PropertyInfo property)
        {
            return new Func<ValidationViewModelBase, object>
			(viewmodel => property.GetValue(viewmodel, null));
        }

        private int validationExceptionCount;

        public void ValidationExceptionsChanged(int count)
        {
            this.validationExceptionCount = count;
            this.OnPropertyChanged("ValidPropertiesCount");
        }
    }
}

Please note I’m also exposing the number of valid properties and the total number of properties with validation (this is used in order to compute the value of the progress bar). From the developer point of view, using this class in an existing ViewModel is very straightforward: Inherit from the new ValidationViewModelBase class instead of your traditional ViewModelBase class and then add validation attributes on the properties which requires validation.

Available Attributes (from the System.ComponentModel.DataAnnotations namespace)

NameDescription
RequiredAttributeSpecifies that a data field value is required
RangeAttributeSpecifies the numeric range constraints for the value of a data field
StringLengthAttributeSpecifies the minimum and maximum length of characters that are allowed in a data field
RegularExpressionAttributeSpecifies that a data field value must match the specified regular expression
CustomValidationAttributeSpecifies a custom validation method to call at run time (you must implement the IsValid() method)

Dealing with Validation Exceptions

As I was working with this new approach based on attributes, I faced a problem: how to deal with validation exception. A validation exception happens when the user input is incorrect, for example if a TextBox has its Text property to an int property, then an input like "abc" (which cannot be converted of course to an int) will raise an exception.

The approach I'm proposing is based on a behavior. A complete description of what behaviors are is out of the scope of this article. For a nice description, you can check out this article.

The behavior I'm proposing must be attached to the parent UI elements which contain the input controls that can raise exception. These are a couple of lines in the XAML:

XML
<Grid>
 <i:Interaction.Behaviors>
   <Behavior:ValidationExceptionBehavior />
 </i:Interaction.Behaviors>

 <!-- rest of the code... -->

When this behavior is loaded, it adds an handler for the ValidationError.ErrorEvent RoutedEvent so that it is notified each time a validation error occurs. When this happens, the behavior calls a method on the ViewModel (through a simple interface in order to limit coupling) so that the ViewModel can track the number of validation errors. Here is the code of the behavior:

C#
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;

namespace WpfFormsWithValidationDemo.Toolkit.Behavior
{
    /// <summary>
    /// A simple behavior that can transfer the number of 
    /// validation error with exceptions
    /// to a ViewModel which supports the INotifyValidationException interface
    /// </summary>
    public class ValidationExceptionBehavior : Behavior<FrameworkElement>
    {
        private int validationExceptionCount;

        protected override void OnAttached()
        {
            this.AssociatedObject.AddHandler(Validation.ErrorEvent, 
		new EventHandler<ValidationErrorEventArgs>(this.OnValidationError));
        }

        private void OnValidationError(object sender, ValidationErrorEventArgs e)
        {
            // we want to count only the validation error with an exception
            // other error are handled by using the attribute on the properties
            if (e.Error.Exception == null)
            {
                return;
            }

            if (e.Action == ValidationErrorEventAction.Added)
            {
                this.validationExceptionCount++;
            }
            else
            {
                this.validationExceptionCount--;
            }

            if (this.AssociatedObject.DataContext is IValidationExceptionHandler)
            {
                // transfer the information back to the viewmodel
                var viewModel = (IValidationExceptionHandler)
				this.AssociatedObject.DataContext;
                viewModel.ValidationExceptionsChanged(this.validationExceptionCount);
            }
        }
    }
}

Progress Reporting

One of my requirements was “When a tab is fully completed, it goes from red: to green: ”. In order to realize this particular feature, I added an “IsValid” property to the FormViewModelBase class (which is the base class for all ViewModels in the TabControl). This property is updated whenever a PropertyChanged occurs by looking if the Error property (of the IDataErrorInfo interface) is empty:

C#
protected override void PropertyChangedCompleted(string propertyName)
{
    // test prevent infinite loop while settings IsValid
    // (which causes an PropertyChanged to be raised)
    if (propertyName != "IsValid")
    {
        // update the isValid status
        if (string.IsNullOrEmpty(this.Error) &&
        	this.ValidPropertiesCount == this.TotalPropertiesWithValidationCount)
        {
            this.IsValid = true;
        }
        else
        {
            this.IsValid = false;
        }
    }
}

Then a simple trigger in the XAML is enough to have the visual effect I described:

XML
<Style TargetType="{x:Type TabItem}">
  <Setter Property="HeaderTemplate">
    <Setter.Value>
      <DataTemplate>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="{Binding FormName}" VerticalAlignment="Center" Margin="2" />
          <Image x:Name="image"
                 Height="16"
                 Width="16"
                 Margin="3"
                 Source="../Images/Ok16.png"/>
        </StackPanel>
        <DataTemplate.Triggers>
          <DataTrigger Binding="{Binding IsValid}" Value="False">
            <Setter TargetName="image" 
		Property="Source" Value="../Images/Warning16.png" />
          </DataTrigger>
        </DataTemplate.Triggers>
      </DataTemplate>
    </Setter.Value>
  </Setter>
</Style>

In order to have the progress bar working, I’m computing the overall progress in the parent ViewModel (the one which owns the various FormViewModelBase ViewModels):

C#
/// <summary>
/// Gets a value indicating the overall progress (from 0 to 100) 
/// of filling the various properties of the forms.
/// </summary>
public double Progress
{
    get
    {
        double progress = 0.0;
        var formWithValidation = this.Forms.Where(f => 
		f.TotalPropertiesWithValidationCount != 0);
        var propertiesWithValidation = this.Forms.Sum(f => 
		f.TotalPropertiesWithValidationCount);

        foreach (var form in formWithValidation)
        {
            progress += (form.ValidPropertiesCount * 100.0) / propertiesWithValidation;
        }

        return progress;
    }
}

Points of Interest

The goal of this technique as I said in the introduction is to have a replacement for the traditional ValidationRules approach which complicates the XAML a lot. Using LINQ and Attributes is a nice way to implement this new possibility. While approaching the end of the writing of this article, I noticed that a similar solution is available in a famous MVVM frameworks made by Mark Smith (the MVVM Helpers).

Acknowledgment

I would like to profusely thank people who helped me to review this article: my co-worker Charlotte and Sacha Barber (CodeProject and Microsoft MVP).

History

  • 28th July 2010: Original version

License

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


Written By
Software Developer (Junior)
France France
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralRe: ErrorMessage issue Pin
Goran _19-Sep-12 13:11
Goran _19-Sep-12 13:11 
QuestionLinked to your Article Pin
nazzyg11-Sep-12 10:49
nazzyg11-Sep-12 10:49 
Question1) Dynamic controls? 2) Localization? Pin
Claire Streb14-Aug-12 15:35
Claire Streb14-Aug-12 15:35 
AnswerRe: 1) Dynamic controls? 2) Localization? Pin
Jeremy Alles24-Aug-12 23:46
Jeremy Alles24-Aug-12 23:46 
GeneralRe: 1) Dynamic controls? 2) Localization? Pin
Claire Streb25-Aug-12 7:38
Claire Streb25-Aug-12 7:38 
QuestionCombobox validation Pin
Member 450897825-Jun-12 2:46
Member 450897825-Jun-12 2:46 
AnswerRe: Combobox validation Pin
Jeremy Alles24-Aug-12 23:42
Jeremy Alles24-Aug-12 23:42 
GeneralMy vote of 5 Pin
Member 450897822-Jun-12 2:11
Member 450897822-Jun-12 2:11 
GeneralMy vote of 5 Pin
Florian.Witteler25-Apr-12 7:47
Florian.Witteler25-Apr-12 7:47 
SuggestionGreat solution Pin
rliviu17-Apr-12 14:28
rliviu17-Apr-12 14:28 
QuestionExcellent solution! Pin
Robert Brower1-Feb-12 7:05
Robert Brower1-Feb-12 7:05 
GeneralCompleted with hierarchical VM and voted 5 Pin
Jac5-Oct-11 0:54
Jac5-Oct-11 0:54 
GeneralMy vote of 5 Pin
Puchko Vasili14-Apr-11 11:26
Puchko Vasili14-Apr-11 11:26 
QuestionPossible Extension? Pin
Tom Thorp6-Nov-10 14:26
Tom Thorp6-Nov-10 14:26 
Generalcross field validation Pin
Member 39408249-Sep-10 6:17
Member 39408249-Sep-10 6:17 
GeneralRe: cross field validation Pin
kiduk15-Jul-12 5:06
kiduk15-Jul-12 5:06 
GeneralSimiliar to Present Pin
thejuan10-Aug-10 21:49
thejuan10-Aug-10 21:49 
GeneralNice Pin
Shaun Stewart4-Aug-10 5:55
Shaun Stewart4-Aug-10 5:55 
GeneralRe: Nice Pin
Jeremy Alles8-Aug-10 22:01
Jeremy Alles8-Aug-10 22:01 
GeneralMy vote of 5 Pin
Shaun Stewart4-Aug-10 5:54
Shaun Stewart4-Aug-10 5:54 
GeneralVery nice, have a 5 Pin
Sacha Barber29-Jul-10 3:59
Sacha Barber29-Jul-10 3:59 
GeneralRe: Very nice, have a 5 Pin
Jeremy Alles8-Aug-10 22:01
Jeremy Alles8-Aug-10 22:01 
GeneralMy vote of 4 Pin
User 246299128-Jul-10 23:21
professionalUser 246299128-Jul-10 23:21 
GeneralRe: My vote of 4 Pin
Jeremy Alles29-Jul-10 3:36
Jeremy Alles29-Jul-10 3:36 
GeneralYour article Pin
Fred POINDRON28-Jul-10 19:21
Fred POINDRON28-Jul-10 19:21 

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.