Tags

, , , ,

Background

When developing Silverlight apps, we used exclusively INotifyDataErrorInfo on the Models or even the VM if you’d liked.  The INotifyDataErrorInfo interface is more flexible than the IDataErrorInfo interface. It supports multiple errors for a property, asynchronous data validation, and the ability to notify the view if the error state changes for an object. However, INotifyDataErrorInfo is currently only supported in Silverlight 4+ and WPF 4.5 and is not available in WPF4.0.  For those of us maintaining WPF4.0 codebase or developing on WPF 4.0 still, Roll up the sleeves!  How can we provide a flexible validation mechanism that that fits in with the IDataErrorInfo on the Model?

Approach

A validation mechanism that is suitable to WPF binding engine on ValidatesOnDataErrors operational on the binding source that implements IDataErrorInfo, easily used by the ViewModel in instances where the encapsulated model needs a validation check for internal logic.  An example could be dependence of an ICommand property on a model validity. 


Viewmodel

The ViewModel can monitor the model ValidationChanged event and take appropriate actions . 

To keep things simple, I’ll remove INotifyPropertyChanged and other interfaces from the model and implement only IDataErrorInfo and the custom Model Interface IModelValidable

Scenario

WPF binding engine checks the validity of the binding source properties as it retrieves their values, whereas the ViewModel is concerned about the validity of the model in question in one straight shot.  to prevent unnecessary overly reruns of the validation logic, each property under validation has a manager that attest to the execution of its ValidationRules.  By framing the rules under a Manager supervision, the whole validation mechanism retains its agility and maintainability.

public static class SharedRules 
    {
        private static Dictionary<Type, PropertiesRulesDictionary> _cache = new Dictionary<Type, PropertiesRulesDictionary>();
        private static ReaderWriterLockSlim _slmLock = new ReaderWriterLockSlim();

        /// <summary>
        /// Adds the specified key and value to the internal cache
        /// </summary>
        /// <param name="key">key used to store/index the value</param>
        /// <param name="value"> value to store in cache</param>
        /// <exception cref="System.ArgumentNullException"></exception>
        /// <exception cref="System.Threading.SynchronizationLockException"></exception>
        public Boolean TryAdd(Type key, PropertiesRulesDictionary value)
        {
            var added = false;
            try
            {
                _slmLock.EnterReadLock();
                try
                {
                    _cache.Add(key, value);
                    added = true;
                }
                catch (ArgumentException) { }
                finally
                {
                    _slmLock.ExitReadLock();
                }

            }
            catch (LockRecursionException) { }
            return added;
        }

        /// <summary>
        /// empties the cache
        /// </summary>
        /// <exception cref="System.Threading.SynchronizationLockException"></exception>
        public void Clear()
        {
            try
            {
                _slmLock.EnterWriteLock();
                try
                {
                    _cache.Clear();
                }
                finally
                {
                    _slmLock.ExitWriteLock();
                }
            }
            catch (LockRecursionException) { }        
        }

        /// <summary>
        /// Removes Entry from cache
        /// </summary>
        /// <param name="key">identifies the entry to remove</param>
        /// <exception cref="System.ArgumentNullException:"></exception>
        /// <exception cref="System.Threading.SynchronizationLockException"></exception>
        public void Remove(Type key)
        {
            try
            {
                _slmLock.EnterWriteLock();
                try
                {
                    _cache.Remove(key);
                }
                finally
                {
                    _slmLock.ExitWriteLock();
                }
            }
            catch (LockRecursionException) { }
        }

        /// <summary >
        /// Gets the number of entries in the cache 
        /// </summary>
        /// <exception cref="System.Threading.SynchronizationLockException"></exception>
        public Int32 Count
        {
            get
            {
                Int32 val = 0;
                try {
                    _slmLock.EnterReadLock();

                    try
                    {
                        val = _cache.Count;
                    }
                    finally
                    {
                        _slmLock.ExitReadLock();
                    }
                }
                catch (LockRecursionException) { }
                return val;
            }
        }

        /// <summary>
        /// Checks the existance of the key inside the cache
        /// </summary>
        /// <param name="key"> key used for search of existance</param>
        /// <returns cref="System.Boolean"> Returns true or false depending on the existance of the key inside the internal cache</returns>
        /// <exception cref="System.Threading.SynchronizationLockException"></exception>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public bool Contains(Type key)
        {
            if (key == null)
                throw new ArgumentNullException();
            var containsKey = false;
            try
            {
                _slmLock.EnterReadLock();
                try {
                    containsKey = _cache.ContainsKey(key);
                }
                finally
                {
                    _slmLock.ExitReadLock();
                }
            }
            catch (LockRecursionException) { }
            return containsKey;
        }

    }
/// <summary>
    /// Defines a property and one of the property attributed ValidationRule
    /// </summary>
    public class ValidationRule
    {
        /// <summary>
        /// Instantiate a ValidatonRule with the specified property name and validation attribute
        /// </summary>
        /// <param name="propertyName"> The name of the property</param>
        /// <param name="valAttr" cref="System.ComponentModel.DataAnnotations.ValidationAttribute"> the validationattribute representing the rule to apply </param>
        public ValidationRule(String propertyName, ValidationAttribute valAttr)
        {
            PropertyName = propertyName;
            ValidationRule = valAttr;
        }
        /// <summary>
        /// Represents the name of the concerned property
        /// </summary>
        public String PropertyName { get; private set; }
        /// <summary>
        /// Represent the validation attribute representing the rule to apply
        /// </summary>
        public ValidationAttribute ValidationRule { get; private set; }

    }
/// <summary>
   /// Indexes list of ValidationRule using strings
   /// </summary>
   public class PropertiesRulesDictionary
   {
       private Dictionary<string, IEnumerable<ValidationRule>> _propertiesRulescache;
       private ReaderWriterLockSlim _slmLock;

       public PropertiesRulesDictionary()
       {
           _propertiesRulescache = new Dictionary<string, IEnumerable<ValidationRule>>();
           _slmLock = new ReaderWriterLockSlim();
       }

       /// <summary>
       /// Returns the list of ValidationRule for the specified propertyName
       /// </summary>
       /// <param name="propertyName"> Specifies the name of property for which the list of ValidationRule belongs</param>
       /// <returns> List of ValidationRule</returns>
       ///<exception cref="System.ArgumentNullException"> Key is not allowed to be null</exception>
       /// <exception cref="System.Collections.Generic.KeyNotFoundException">key was not found in get operation</exception> 
       /// <exception cref="System.NotSupportedException">The set accessor operation is not supported in this version</exception>
       public IEnumerable<ValidationRule> this[String propertyName]
       {
           get{
               IEnumerable<ValidationRule> lst = null;
               try
               {

                   if (String.IsNullOrEmpty(propertyName))
                       throw new ArgumentNullException();
                   if (!_propertiesRulescache.ContainsKey(propertyName))
                       throw new KeyNotFoundException();

                   _slmLock.EnterReadLock();
                   try
                   {
                       lst = _propertiesRulescache[propertyName];
                   }
                   finally
                   {
                       _slmLock.ExitReadLock();
                   }
               }
               catch (System.ArgumentNullException) { }
               catch(LockRecursionException){}

               return lst;
           }
           set{
               // look into _slmLock.TryEnterUpgradeableReadLock(); if wanting to implement the set functionality
               throw new NotSupportedException();
           }          

       }

       /// <summary>
       /// Adds the specified key and value to the dictionary.
       /// </summary>
       /// <param name="propertyName">The key of the element to add.</param>
       /// <param name="ruleLst">The value of the element to add</param>
       /// <exception cref="System.ArgumentNullException"> Key is not allowed to be null</exception>
       /// <exception cref="System.ArgumentException">key already exist</exception>
       public void Add(string propertyName, IEnumerable<ValidationRule> ruleLst)
       {
           if (String.IsNullOrEmpty(propertyName))
               throw new ArgumentNullException("propertyName");
           if (_propertiesRulescache.ContainsKey(propertyName))
               throw new System.ArgumentException("key already exist", propertyName);
           try
           {
               _slmLock.EnterWriteLock();
               try
               {
                   _propertiesRulescache.Add(propertyName, ruleLst);
               }
               finally
               {
                   _slmLock.ExitWriteLock();
               }

           }
           catch (LockRecursionException) { }
       }

       /// <summary>
       /// Enumerates over all ValidationRule inserted into the dictionary
       /// </summary>
       /// <exception cref="System.Threading.LockRecursionException">exception may be raised in for reentrant threads depending on the recursion policy</exception>
       IEnumerable<ValidationRule> Rules
       {
           get
           {

                   _slmLock.EnterReadLock();

                   try
                   {
                       foreach (var propRuleEntry in _propertiesRulescache)
                       {
                           foreach (var ruleLst in propRuleEntry.Value)
                               yield return ruleLst;
                       }
                   }
                   finally
                   {
                       _slmLock.ExitReadLock();
                   }                
           }
       }
   }
/// <summary>
    /// Container for a property error message
    /// </summary>
    public sealed class ValidationFault
    {
        /// <summary>
        /// Construct a faulting for the specified property with the provided message
        /// </summary>
        /// <param name="PropertyName">Specifies the propety name</param>
        /// <param name="message"> The property error message</param>
        /// <exception cref="System.ArgumentException">A parameter passed to the constructor is null or empty</exception>
        public ValidationFault(string PropertyName, string message)
        {
            if (string.IsNullOrEmpty(PropertyName) || string.IsNullOrEmpty(message))
                throw new System.ArgumentException("Neither parameter can be null");
            Property = PropertyName;
            Message = message;
        }

        /// <summary>
        /// Property name
        /// </summary>
        public string Property { get; private set; }

        /// <summary>
        /// Property error message
        /// </summary>
        public string Message { get; private set; }
    }
/// <summary>
    /// Serves as the validation manager on an object instance property
    /// </summary>
    public class ValidationRuleManager
    {
        private Boolean _checked;
        private Infrastructure.SharedRules _sharedRules;
        private Boolean _previouslyValid;
        private List<ValidationFault> _validationFaults;

        /// <summary>
        /// Initializes a new instance of the ValidationRuleManager specifying the ValidationContext and the property name
        /// used by ValidationContext.MemberName during validation
        /// </summary>
        /// <param name="propertyName">the name of the property to be validated</param>
        /// <param name="context">References the ValidationContext used during validation</param>
        public ValidationRuleManager(string propertyName, System.ComponentModel.DataAnnotations.ValidationContext context)
        {
            PropertyName = propertyName;
            Context = context;
            _checked = false;
            _previouslyValid = true;
            _validationFaults = new List<ValidationFault>();

        }

        /// <summary>
        /// Gets the name of the property to validate
        /// </summary>
        public String PropertyName { get; private set; }

        /// <summary>
        /// Gets the ValidationContext used for validation
        /// </summary>
        public ValidationContext Context { get; private set; }

        /// <summary>
        /// Gets the list of Validation fault for the property in context
        /// </summary>
        public IEnumerable<ValidationFault> ValidationFaults { 
            get
            {
                return _validationFaults;
            }
        }

        /// <summary>
        /// Gets a value indicating whether the property under context is valid
        /// </summary>
        public Boolean IsValid
        {
            get
            {
                if (!_checked)
                    ValidateProperty();
                return ValidationFaults.Count().Equals(0);                    
            }
        }

        /// <summary>
        /// Validates the property under context
        /// </summary>
        public void ValidateProperty()
        {
            var value = Context.ObjectType.GetProperty(PropertyName).GetValue(Context.ObjectInstance, null);
            ValidateProperty(value);
        }

        /// <summary>
        /// Checks whether the specified value is valid with respect to the property under context
        /// </summary>
        /// <param name="value">The value to validate</param>
        public void ValidateProperty(object value)
        {
            var val = value;
            _previouslyValid = ValidationFaults.Count().Equals(0);
            _validationFaults.Clear();
            foreach (var rule in SharedPropertyRules)
            {
                Context.MemberName = PropertyName;
                ValidationResult result = rule.ValidationRule.GetValidationResult(val, Context);
                if (!String.IsNullOrEmpty(result.ErrorMessage))
                    _validationFaults.Add(new ValidationFault(PropertyName, result.ErrorMessage));
            }
            if(_checked.Equals(false))
                _checked = true;
        }

        /// <summary>
        /// Keeps SharedRules close by!
        /// Returns an enumerable list of validationRule
        /// </summary>
        public  IEnumerable<ValidationRule> SharedPropertyRules
        {
            get
            {
                return SharedRules.GetRules(Context.ObjectType)[PropertyName];
            }
        }

        /// <summary>
        /// Compares the current validation state to the previous validation state
        /// and returns a value indicating whether a change occured
        /// </summary>
        public Boolean HasValidatonStateChanged
        {
            get
            {
                return !IsValid.Equals(_previouslyValid);
            }
        }
    }
/// <summary>
/// Represents a mechanism that handles most validation operation.
/// Model entities should delegate their validation operation to the ModelValidator
/// </summary>
/// <typeparam name="T"></typeparam>
public sealed class ModelValidator<T>
{
    private T _model;
    private bool? _isValid;
    private bool? _prevState;
    private StringBuilder _strBldr;
    private ValidationContext _context;

    /// <summary>
    /// notify subscriber on Model validation state changed.
    /// notification is raised only when there exist one or more subscriber.
    /// </summary>
    public event EventHandler<ValidationStateArg> ValidationStateChanged;

    /// <summary>
    /// Initialized a new instance of ModelValidator for the model specified
    /// </summary>
    /// <param name="model">Context of on which the validaion will occur</param>
    public ModelValidator(T model)
    {
        _model = model;
        _isValid = null;
        _context = new ValidationContext(model, null, null);
        //Managers = new Dictionary<string, ValidationRuleManager>();
        var modelType = typeof(T);
        if (!SharedRules.Contains(modelType)) // expensive operation done once per type on request instead of per instance
        {
            var vrType = typeof(ValidationAttribute);
            var props = typeof(T).GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance).
                Where(prop => { return prop.GetCustomAttributes(vrType, true).Count() > 0; });
            var propsRulesDict = new PropertiesRulesDictionary();
            foreach (var prop in props)
            {
                var ruleLst = ConstructManagerRules(prop, vrType);
                propsRulesDict.Add(prop.Name, ruleLst);
                Managers.Add(prop.Name, new ValidationRuleManager(prop.Name, _context));
            }
            SharedRules.TryAdd(modelType, propsRulesDict);
        }
    }

    private IEnumerable<ValidationRule> ConstructManagerRules(System.Reflection.PropertyInfo prop, Type baseValidationRuleType)
    {
        var attrbts = prop.GetCustomAttributes(baseValidationRuleType, true).Cast<ValidationAttribute>();

        var valRuleLst = new List<ValidationRule>();
        foreach (var valAttr in attrbts)
        {
            var valRule = new ValidationRule(prop.Name, valAttr);
            valRuleLst.Add(valRule);
        }
        return valRuleLst;
    }

    /// <summary>
    /// Validates the property with the given name and load its error messages into the message argument
    /// </summary>
    /// <param name="propertyName">The name of the property to validate</param>
    /// <param name="message"> the Store for the error messages</param>
    public void Validate(string propertyName, out string message)
    {
        var manager = Managers[propertyName];
        if (!manager.Checked)
            manager.ValidateProperty();

        if (_strBldr == null)
                _strBldr = new StringBuilder();
        else
                _strBldr.Clear();
        if (!manager.IsValid)
        {
            if (!_isValid.HasValue || (_isValid.HasValue && _isValid.Value.Equals(true)))
            {
                _isValid = false;
                CompareValidationState(_isValid.Value);
            }
            foreach (var fault in manager.ValidationFaults)
                _strBldr.AppendLine(String.Format("Property: {0} Message: {1}", fault.Property, fault.Message));
        }
        message = _strBldr.ToString();
    }

    /// <summary>
    /// Checks whether the model is valid or not
    /// </summary>
    /// <returns>Boolean(true/false) indicates whether the model is valid or not</returns>
    public bool IsValid()
    {
        if (_isValid == null)
        {
            _isValid = Validate();
            CompareValidationState(_isValid.Value);
        }
        return _isValid.Value;
    }

    private bool Validate()
    {
        var isValid = true;
        foreach (var manager in Managers.Values)
        {
            if (!manager.Checked)
            {
                manager.ValidateProperty();
            }

            if (!manager.IsValid)
            {
                isValid = false;
            }
        }
        return isValid;
    }

    /// <summary>
    /// Validates a property given its name, a store to hold the validated value, the value to validate on the property, and an action to execute
    /// only if the property is valid
    /// </summary>
    /// <typeparam name="T1">The type of the property to validate</typeparam>
    /// <param name="propertyName">The name of property to validate</param>
    /// <param name="backupField">Field to store the value</param>
    /// <param name="value"> the value to validate on the property</param>
    /// <param name="action"> Could serve as a delegate to NotifyPropertyChanged </param>
    public void Validate<T1>(string propertyName, T1 backupField, object value, Action action)
    {
        if (!(value is T1))
        {
            throw new ArgumentException("the type for the value and the backup field do not match");
        }

        var manager = Managers[propertyName];
        manager.ValidateProperty(value);
        backupField = (T1)value;
        if (action != null && manager.IsValid)
            action();

        CompareValidationState(manager.IsValid);
    }

    /// <summary>
    /// Compares the previous and current validation status of the model
    /// and raises event on change and also on first occurence if there are any subscriber
    /// </summary>
    /// <param name="proposedState">The newly proposed state.  Currently has no effect on algorithm.</param>
    private void CompareValidationState(bool proposedState)
    {
        if (!_prevState.HasValue)
        {
            CheckModelCurrentState();
            OnValidationStateChange(_prevState.Value);
        }
        else
        {
            var previousState = _prevState.Value;
            CheckModelCurrentState();
            var currentState = _prevState.Value;
            var stateChanged = !previousState.Equals(currentState);
            if (stateChanged)
                OnValidationStateChange(currentState);
        }
    }

    private void OnValidationStateChange(bool currentState)
    {
        var state = currentState;

        var notifyer = ValidationStateChanged;

        if (notifyer != null)
        {
            notifyer(this, new ValidationStateArg(state));
        }
    }

    private void CheckModelCurrentState()
    {
        _prevState = true;
        foreach (var manager in Managers.Values.Where(mngr => { return mngr.Checked; }).ToList())
        {
            if (!manager.IsValid)
            {
                _prevState = false;
                break;
            }
        }
    }

    public string Error 
    { 
        get
        {
            if (_strBldr == null)
                _strBldr = new StringBuilder();
            else
                _strBldr.Clear();
            foreach (var ruleManager in Managers.Values)
            {
                if(!ruleManager.IsValid)
                    foreach(var fault in ruleManager.ValidationFaults)
                        _strBldr.AppendLine(String.Format("Property: {0} Message: {1}", fault.Property, fault.Message));
            }
            return _strBldr.ToString();
        }
    }
    public Dictionary<String, ValidationRuleManager> Managers { get; private set; }
}

Finally inside your model or the base from which your models inherit, delegate all validation operations to the ModelValidator as illustrated below.  Inside your model public property set accessors, use ValidateProperty<p>(propName, store, value, action)

public string Error
       {
           get { return ModelValidator.Error; }
       }

       public string this[string propertyName]
       {
           get {
               var message = String.Empty;
               ModelValidator.Validate(propertyName, out message);
               return message;
           }
       }

       /// <summary>
       /// Query the validity of the entire model
       /// </summary>
       public bool IsValid
       {
           get { return ModelValidator.IsValid(); }
       }

       /// <summary>
       /// Validator is only constructed when needed on first request
       /// </summary>
       protected ModelValidator<T> ModelValidator
       {
           get{
               if (_validator == null)
               {
                   _validator =  new WeakReference(new ModelValidator<T>(this as T)); // see if this becomes null inside the model validator
               }
               else if (!_validator.IsAlive)
               {
                   _validator.Target = new ModelValidator<T>(this as T);
               }
               return _validator.Target as ModelValidator<T>;
           }
       }

       public void ValidateProperty<P>(string propertyName, P backupField, object value, Action action = null)
       {
           ModelValidator.Validate<P>(propertyName, backupField, value, action);
       }

       /// <summary>
       /// Notify subscriber of model validation state changes
       /// </summary>
       public event EventHandler<ValidationStateArg> ValidationStateChanged
       {
           add{
               lock (_validatorLock)
               {
                   ModelValidator.ValidationStateChanged += value;
               }
           }
           remove {
               lock (_validatorLock)
               {
                   ModelValidator.ValidationStateChanged -= value;
               }
           }
       }

Example:

public string Name
        {
            get
            {
                return this._name;
            }
            set
            {
                if ((this._name != value))
                {
                    this.OnNameChanging(value);
                    this.SendPropertyChanging();
                    ValidateProperty<String>("Name", this._name, value, () => { 
                        this.SendPropertyChanged("Name");
                        this.OnNameChanged();
                    });
                }
            }
        }

Your model itself or any container housing your model such as a ViewModel, can subscribe to the model ValidationStateChanged and take the appropriate action base on the argument passed to the listener handler.  In this example, I’ve omitted the implementation of IDisposable pattern on classes that hold on system resources such as the SlimReaderWriter class and did not make use of the weak event pattern to stay on the objective; which should not be the case for production codebase.

Advertisements