Click here to Skip to main content
Click here to Skip to main content
Go to top

WPF MVVM Validation ViewModel using IDataErrorInfo

, 10 Jun 2014
Rate this:
Please Sign up or sign in to vote.
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.

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

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:

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

Override the default ErrorTemplate for changing the Background color:

            <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.

        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.

        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.

      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.

            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.

            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.

            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.

        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.

            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

        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

        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.

        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

  • 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)

Share

About the Author

arpanmukherjee1

United States United States
No Biography provided

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Mobile
Web03 | 2.8.140921.1 | Last Updated 10 Jun 2014
Article Copyright 2014 by arpanmukherjee1
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid