3
\$\begingroup\$

An abstract base class which derives from the ObservableObject (MvvmLight), implementing the INotifyDataErrorInfo interface.

The model (exposed via the ViewModel) binds directly to the view, to avoid duplication of properties.

A couple of notes:

a) I chose the HashSet<string>> to avoid duplicate errors, e.g. "The number of allowed characters has been exceeded." on every keystroke when UpdateSourceTrigger=PropertyChanged.

b) The RaisePropertyChanged of the MvvmLight's ObservableObject has been overridden to validate individual properties.

c) The method IsValidated() validates the object and provides visual feedback to the user when e.g. the "Save" button has been pressed.

using GalaSoft.MvvmLight;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;

namespace WPF.MVVM.UI.Data.Model.Base
{
    public abstract class ValidatedObservableObject : ObservableObject, INotifyDataErrorInfo
    {
        private readonly IDictionary<string, HashSet<string>> _errors = new Dictionary<string, HashSet<string>>();

        public bool HasErrors
        {
            get => _errors.Count > 0;
        }

        public IEnumerable GetErrors(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName) || !_errors.ContainsKey(propertyName))
                return null;

            return _errors[propertyName];
        }

        public bool IsValidated()
        {
            var results = new List<ValidationResult>();
            var valid = Validator.TryValidateObject(this, new ValidationContext(this), results, true);

            if (valid)
                return true;

            AppendOrUpdateErrors(results);

            return false;
        }

        public override void RaisePropertyChanged(string propertyName = null)
        {
            base.RaisePropertyChanged(propertyName);

            if (!string.IsNullOrEmpty(propertyName))
            {
                ValidateProperty(propertyName);
            }
        }

        private void ValidateProperty(string propertyName)
        {
            var results = new List<ValidationResult>();
            var valid = Validator.TryValidateProperty(GetType().GetProperty(propertyName)?.GetValue(this), new ValidationContext(this) { MemberName = propertyName }, results);

            if (valid)
            {
                RemoveFromErrors(propertyName);
            }
            else
            {
                AppendOrUpdateErrors(results);
            }
        }

        private void RemoveFromErrors(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName) || !_errors.ContainsKey(propertyName))
                return;

            _errors.Remove(propertyName);

            RaiseErrorsChanged(propertyName);
        }

        private void AppendOrUpdateErrors(IReadOnlyCollection<ValidationResult> results)
        {
            if (results == null)
                return;

            foreach (var memberName in results.SelectMany(r => r.MemberNames).Distinct())
            {
                var memberErrors = new HashSet<string>(results.Where(r => r.MemberNames.Contains(memberName)).Select(r => r.ErrorMessage));

                if (_errors.ContainsKey(memberName))
                {
                    _errors[memberName] = memberErrors;
                }
                else
                {
                    _errors.Add(memberName, memberErrors);
                }

                RaiseErrorsChanged(memberName);
            }
        }

        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        private void RaiseErrorsChanged(string propertyName)
        {
            ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
        }
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.