Click here to Skip to main content
15,885,244 members
Articles / Desktop Programming / WPF
Tip/Trick

WPF MVVM Validation ViewModel using IDataErrorInfo

Rate me:
Please Sign up or sign in to vote.
4.68/5 (19 votes)
24 Sep 2014CPOL3 min read 102.7K   5.9K   35   8
A base viewmodel implementing IDataErrorInfo

Introduction

While writing WPF applications, validation in MVVM is primarily done through the IDataErrorInfo interface. Data is binded to the control through a viewmodel implementing the IDataErrorInfo interface.

We shall cover some concepts of a base viewmodel calling it ViewModelBase and extend it to ValidationViewModelBase.

Using the Code

Most of the boilerplate code involved in the implementation of the IDataErrorInfo is the evaluation of error of individual properties and looking at the state of the entire object and qualifying it as valid or invalid.

We build towards a sample that has:

  1. User input as string whose length follows 3 simple business rules:
    1. Must be multiple of 2
    2. Greater than 10 digits
    3. Less than 32 digits
  2. OK button that can be clicked if only the user input follows the rules (is valid).

The invalid state shall have the OK button disabled.

Image 1

As soon as the user input is correct, the error clears and the OK button is enabled.

Image 2

The implementation is based upon our base class ValidationViewModel.cs that will be explained later. The UI contains a regular TextBox and a Button.

The DataContext is set and binded to the TextBox Text property as:

XML
<TextBox Text="{Binding Aid,UpdateSourceTrigger=PropertyChanged,Mode=TwoWay,ValidatesOnDataErrors=True}"

Override the default ErrorTemplate for changing the Background color:

XML
<TextBox.Style>
    <Style TargetType="TextBox">
        <Style.Triggers>
            <Trigger Property="Validation.HasError" Value="True">
                <Setter Property="Background" Value="Pink"/>
                <Setter Property="ToolTip"
                        Value="{Binding RelativeSource={x:Static RelativeSource.Self},
                        Path=(Validation.Errors)[0].ErrorContent}"/>
            </Trigger>
        </Style.Triggers>
    </Style>
</TextBox.Style>

Implementing the ValidationViewModel on the sample viewmodel can be done as such corresponding to our original two use cases.

1. Implementing the Business Rule

The rule is added as Func<bool> to the rule dictionary using the AddRule() method.

C#
public ViewModel()
{
    base.AddRule(() => Aid, () =>
    Aid.Length >= (5 * 2) &&
    Aid.Length <= (16 * 2) &&
    Aid.Length % 2 == 0, "Invalid AID.");
}

2. Defining behavior of the ‘OK’ button

This is implemented by using the RelayCommand which uses the HasErrors to evaluate the ICommand.CanExecute.

C#
public ICommand OkCommand
{
    get
    {
        return Get(()=>OkCommand, new RelayCommand(
            ()=> MessageBox.Show("Ok pressed"),
            ()=> !base.HasErrors));
    }
}

Also as a side note, private field for the command is not needed, as the result is cached and the same command is returned every time the getter is called.

Implementing the ViewModelBase

First up is the generic ViewModelBase which will implement the INotifyPropertyChanged. Also in the base, we tackle quite a few generic points.

  1. Removing the “Magic String” in PropertyChanged Event

    This is a common problem and having an Expression removes the need of property string is a pretty neat solution. This is nice as it removes typing errors and makes refactoring easy.

    The code is primarily the NotificationObject of the PRISM library.

    C#
    protected static string GetPropertyName<T>(Expression<Func<T>> expression)
          {
              if (expression == null)
                  throw new ArgumentNullException("expression");
    
              Expression body = expression.Body;
              MemberExpression memberExpression = body as MemberExpression;
              if (memberExpression == null)
              {
                  memberExpression = (MemberExpression)((UnaryExpression)body).Operand;
              }
              return memberExpression.Member.Name;
          }
    
  2. Generic Getter

    We have property name to value map for mapping last known value of corresponding property.

    C#
    private Dictionary<string, object> propertyValueMap;
    
    protected ViewModelBase()
    {
        propertyValueMap = new Dictionary<string, object>();
    } 

    We have Get that takes an Expression that is used to extract the property name and default value.

    C#
    protected T Get<T>(Expression<Func<T>> path)
    {
        return Get(path, default(T));
    }
    
    protected virtual T Get<T>(Expression<Func<T>> path, T defaultValue)
    {
        var propertyName = GetPropertyName(path);
        if (propertyValueMap.ContainsKey(propertyName))
        {
            return (T)propertyValueMap[propertyName];
        }
        else
        {
            propertyValueMap.Add(propertyName, defaultValue);
            return defaultValue;
        }
    }
  3. Generic Setter

    Building up on the property map, we have generic setter that raises the PropertyChanged event.

    C#
    protected void Set<T>(Expression<Func<T>> path, T value)
    {
        Set(path, value, false);
    }
    
    protected virtual void Set<T>(Expression<Func<T>> path, T value, bool forceUpdate)
    {
        var oldValue = Get(path);
        var propertyName = GetPropertyName(path);
    
        if (!object.Equals(value, oldValue) || forceUpdate)
        {
            propertyValueMap[propertyName] = value;
            OnPropertyChanged(path);
        }
    }

Implementing the ValidationViewModel

Building up on the previous ViewModelBase, we implement IDataErrorInfo interface on ValidationViewModel. The features that it exposes are:

1. Method to add rule corresponding to specific property

The class exposes a AddRule() method taking in the property, a delegate that is a function that evaluates to bool, and the error message as string that is displayed if the rule fails. This delegate is added to ruleMap corresponding to the property name.

The functionality to add multiple rules for the same property is left to the discretion of the client and AddRule() will throw ArgumentException if property name (key) is present.

C#
private Dictionary<string, Binder> ruleMap = new Dictionary<string, Binder>();

public void AddRule<T>(Expression<Func<T>> expression, Func<bool> ruleDelegate, string errorMessage)
{
    var name = GetPropertyName(expression);
    
    ruleMap.Add(name, new Binder(ruleDelegate, errorMessage));
}

The implementation of the Binder class is straightforward, it exists only to encapsulate the functionality of data validation.

The Binder class has a IsDirty property that qualifies that the current values is dirty or not. This property is set whenever the property value is updated. Also an Update() method that evaluates the rule that was passed while registering the rule.

C#
internal string Error { get; set; }
internal bool HasError { get; set; }

internal bool IsDirty { get; set; }

internal void Update()
{
    if (!IsDirty)
        return;
        
    Error = null;
    HasError = false;
    try
    {
        if (!ruleDelegate())
        {
            Error = message;
            HasError = true;
        }
    }
    catch (Exception e)
    {
        Error = e.Message;
        HasError = true;
    }
}

The Update() method performs little optimization as not to reevaluate the ruleDelegate if the property is not dirty.

2. Override the Set method to set IsDirty flag

C#
protected override void Set<T>(Expression<Func<T>> path, T value, bool forceUpdate)
{
    ruleMap[GetPropertyName(path)].IsDirty = true;
    base.Set<T>(path, value, forceUpdate);
}

3. Global HasErrors to check validity of the entire view model state

C#
public bool HasErrors
{
    get
    {
        var values = ruleMap.Values.ToList();
        values.ForEach(b => b.Update());
        
        return values.Any(b => b.HasError);
    }
}

4. Implementation of IDataErrorInfo. The Error property concatenates the error messages into a single message.

C#
public string Error
{
    get
    {
        var errors = from b in ruleMap.Values where b.HasError select b.Error;
        
        return string.Join("\n", errors);
    }
}

public string this[string columnName]
{
    get
    {
        if (ruleMap.ContainsKey(columnName))
        {
            ruleMap[columnName].Update();
            return ruleMap[columnName].Error;
        }
        return null;
    }
}

This finishes my take on WPF validation.

The entire code essentially aggregates information and presents you with an encapsulated base class to work with your custom business rules.

Hope someone finds it useful.

Please leave your comments…

History

  • 25th September, 2014 - Added dependency DLL
  • 10th June, 2014 - First draft

License

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


Written By
Team Leader BHARTI Airtel
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionDifferent Error message for different error Pin
Kerketsos22-Oct-19 2:16
Kerketsos22-Oct-19 2:16 
QuestionGet n Set why do we need them Pin
Avi Farah3-Sep-17 5:41
Avi Farah3-Sep-17 5:41 
arpanmukherjee1,

Outstanding article, you separated the error tracking from the error reporting.

I have a question though. Why do you implement the Get<t> n Set<t> methods, what is the use case for using them?

Again i thank you for an outstanding job.
Avi
AnswerRe: Get n Set why do we need them Pin
lrhage17-Mar-18 10:21
lrhage17-Mar-18 10:21 
GeneralRe: Get n Set why do we need them Pin
Avi Farah20-Mar-18 21:06
Avi Farah20-Mar-18 21:06 
QuestionRelayCommand source Pin
Member 1208433626-Mar-16 10:24
Member 1208433626-Mar-16 10:24 
GeneralMy vote of 5 Pin
Arkitec25-Sep-14 14:38
professionalArkitec25-Sep-14 14:38 
QuestionMvvmFramework DLL Pin
hugh_jass24-Sep-14 5:30
hugh_jass24-Sep-14 5:30 
AnswerRe: MvvmFramework DLL Pin
arpanmukherjee124-Sep-14 9:45
arpanmukherjee124-Sep-14 9:45 

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.