Sunday, March 12, 2017

WPF/MVVM - Validating ViewModels with IDataErrorInfo and models with annotations

I'm working on a personal WPF/MVVM project where I need validations at both the View Model and Model level.  For business rule validation I decided to implement the IDataErrorInfo interface in my view models.  It works pretty well and it is easy to find examples of how to do it.  However in my case I also have basic validations (like required fields, string length limits, etc.) in Annotations at the model level.  This is necessary because I'm using Entity Framework 6- Code First to generate my database.  The hard part was combining both levels of validation.  Here's how I ended up doing it:

All my model classes inherit from a base class that implements INotifyPropertyChanged and IDataErrorInfo.  OnValidate returns validation errors from the annotations.  Plenty of examples how to do this- just google IDataErrorInfo Annotations.

    public class ModelBase : INotifyPropertyChanged, IDataErrorInfo
    {
        #region " INotifyPropertyChanged "
        public event PropertyChangedEventHandler PropertyChanged;
        public void RaisePropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        #endregion

        #region " IDataErrorInfo "
        public string this[string propertyName]
        {
            get
            {
                return OnValidate(propertyName);
            }
        }

        /// <summary>
        /// Validates current instance properties using Data Annotations.
        /// </summary>
        /// <param name=“propertyName”>This instance property to validate.</param>
        /// <returns>Relevant error string on validation failure or <see cref=“System.String.Empty”/> on validation success.</returns>
        private string OnValidate(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName))
                throw new ArgumentException("Property may not be null or empty", propertyName);

            string error = string.Empty;

            var value = this.GetType().GetProperty(propertyName).GetValue(this, null);
            var results = new List<ValidationResult>();

            var context = new ValidationContext(this, null, null) { MemberName = propertyName };

            var result = Validator.TryValidateProperty(value, context, results);

            if (!result)
            {
                var validationResult = results.First();
                error = validationResult.ErrorMessage;
            }
            return error;
        }

        public string Error
        {
            get
            {
                throw new NotSupportedException("IDataErrorInfo.Error is not supported, use IDataErrorInfo.this[propertyName] instead.");
            }
        }
        #endregion
    }

The implementation in my model classes looks more or less like this:

    public class MyClass: ModelBase
    {
        private string someProperty;
       
        [Required]
        [StringLength(255, ErrorMessage = "Some Property maximum length is 255 characters")]
        public string SomeProperty
        {
            get { return someProperty; }
            set
            {
                if (value == someProperty) return;
                someProperty= value;
                RaisePropertyChanged("SomeProperty");
            }
        }
    }

And then in my view models I also implement IDataErrorInfo.  I first perform the view model validations, then if there are no errors I go to the model's IDataErrorInfo's implementations and check for errors there.  The basic idea came from http://stackoverflow.com/questions/13113779/implementing-idataerrorinfo-in-a-view-model

    public class MyViewModel: IDataErrorInfo
    {
        private MyClass _myClass { get; set; }

        public string SomeProperty
        {
            get { return _myClass.SomeProperty; }
            set
            {
                _myClass.SomeProperty= value;
                RaisePropertyChanged("SomeProperty");
            }
        }

        #region " Implementation of IDataErrorInfo "

        public string Error
        {
            get { throw new NotSupportedException("Not supported"); }
        }

        // Put business rule validation here
        public string this[string propertyName]
        {
            get
            {
                if (propertyName.Equals("SomeProperty"))
                {
                    if (SomeProperty == "FooBar")
                        return "SomeProperty cannot be FooBar.";
                }

                // If we get to here, ViewModel validation passed.  So do the Model validations- 
                return _selectedRacer[propertyName];
            }
        }
    #endregion
    }

And finally, the binding in the view looks like this:

<TextBox Text="{Binding SomeProperty, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True}">

No comments:

Post a Comment