For those of you familiar with the ViewModel (or MVVM) pattern, you are likely also familiar with a typical pain point regarding validation: you often need to duplicate your entity validation metadata onto your ViewModel classes.  This can lead to burdensome dual maintenance of your validation rules, and it can seem very frustrating that with the server to client metadata propagation that RIA Services offers, your ViewModel classes are left dangling out there for you to manage yourself.  In this post, I’ll illustrate a utility I created that allows a ViewModel to assume validation metadata from model classes or properties, eliminating the dual maintenance.

ModelPropertyValidator

Virtually every ViewModel will have properties that actually represent properties from your model.  Let’s use the meeting invitation model from earlier RIA Services Validation posts as an example.  My meeting object has a bunch of properties such as Title, Start, End, Location, MinimumAttendees, MaximumAttendees, and Details.  It’s easy to imagine a ViewModel that could be used for managing meetings where the UI would have a subset or superset of the meeting properties.  For each field on the screen that represents a meeting property, I should be able to indicate which model property is associated with each ViewModel property so that the model validation can be imported automatically.  This is where ModelPropertyValidator comes in.

Here’s a subset of the Meeting class with its data annotations in my Web project:

Code Snippet
  1. [CustomValidation(typeof(MeetingValidators), "PreventExpensiveMeetings")]
  2. [CustomValidation(typeof(MeetingValidators), "PreventDoubleBooking")]
  3. [MetadataType(typeof(Meeting.MeetingMetadata))]
  4. public partial class Meeting
  5. {
  6.     public class MeetingMetadata
  7.     {
  8.         [Key]
  9.         [Display(AutoGenerateField = false)]
  10.         public int MeetingId { get; set; }
  11.  
  12.         [Required]
  13.         [CustomValidation(typeof(MeetingValidators), "NoEarlyMeetings")]
  14.         [CompareValidator(CompareOperator.LessThan, "End",
  15.             ErrorMessage = "Meetings cannot result in time travel.")]
  16.         [DateValidator(DateValidatorType.Future)]
  17.         [Display(Order = 1)]
  18.         public DateTime Start { get; set; }
  19.  
  20.         [Required]
  21.         [CompareValidator(CompareOperator.GreaterThan, "Start",
  22.             ErrorMessage = "Meetings cannot result in time travel.")]
  23.         [Display(Order = 2)]
  24.         public DateTime End { get; set; }
  25.  
  26.         [Required]
  27.         [StringLength(80, MinimumLength = 5,
  28.             ErrorMessageResourceType = typeof(ValidationErrorResources),
  29.             ErrorMessageResourceName = "TitleStringLengthErrorMessage")]
  30.         // {0} must be at least {2} characters and no more than {1}.
  31.         [Display(Order = 0)]
  32.         public string Title { get; set; }

 

When a ViewModel is going to expose the Start property for a meeting, it would be a nightmare to have to keep all of the validation attributes in sync.  To alleviate this, I created an attribute called ModelPropertyValidatorAttribute that acts as a proxy from a ViewModel property to a Model property.  Here’s what it looks like to use it:

Code Snippet
  1. public class MeetingEntryViewModel : ViewModelBase
  2. {
  3.     private DateTime start;
  4.     private DateTime end;
  5.  
  6.     [ModelPropertyValidator(typeof(Meeting))]
  7.     public DateTime Start
  8.     {
  9.         get { return this.start; }
  10.         set
  11.         {
  12.             if (this.start != value)
  13.             {
  14.                 this.ValidateProperty("Start", value);
  15.                 this.start = value;
  16.                 this.RaisePropertyChanged("Start");
  17.             }
  18.         }
  19.     }
  20.  
  21.     [ModelPropertyValidator(typeof(Meeting))]
  22.     public DateTime End
  23.     {
  24.         get { return this.end; }
  25.         set
  26.         {
  27.             if (this.end != value)
  28.             {
  29.                 this.ValidateProperty("End", value);
  30.                 this.end = value;
  31.                 this.RaisePropertyChanged("End");
  32.             }
  33.         }
  34.     }
  35. }

 

With a simple attribute on each, the Start and End properties now import all of the validation attributes from their corresponding Model properties on the Meeting type; no more validation duplication!  When the property name is the same on the Model and the ViewModel, you can get away with only specifying the model type, but I also have an overload that accepts the model property name in case the property names differ.

ModelObjectValidator

Just as there are cases when you want a ViewModel property to import the validation attributes from a model property, there are also times when you want a ViewModel class to import a model’s object-level validation attributes.  That’s where ModelObjectValidatorAttribute comes in.  Very similarly to the ModelPropertyValidator, you just put the attribute on the ViewModel and specify the corresponding model type.

Code Snippet
  1. [ModelObjectValidator(typeof(Meeting))]
  2. public class MeetingEntryViewModel : ViewModelBase, IStartAndEnd
  3. {
  4.     private DateTime start;
  5.     private DateTime end;

 

That’s all there is to it.  Now the MeetingEntryViewModel class imports the object-level validation from the Meeting type.  You can even specify multiple ModelObjectValidator attributes if you want to import validation attributes from multiple classes.

How’d you do that?

Here’s how it works:

  1. ModelPropertyValidatorAttribute and ModelObjectValidatorAttribute both derive from ModelValidatorAttribute
  2. ModelValidatorAttribute is an abstract class that derives from ValidationAttribute
  3. ModelValidatorAttribute overrides IsValid and performs much of the same work that Validator performs, iterating over the attributes on the target and producing validation errors as ValidationResult instances
  4. If the ViewModel derives from the RIA Services ComplexObject class, then there’s a nice ValidationErrors property that can be used to add multiple error messages
  5. Otherwise, the first validation error is returned as the error message from this custom validator

Yes, I suggested that you make your ViewModel classes derive from ComplexObject.  ComplexObject is a really useful class and it exposes a great ValidationErrors collection that any consumer can manipulate, and all updates to that collection generate events from the INotifyDataErrorInfo interface.  This works perfectly for this scenario where I want my validation attribute to be able to push multiple validation results onto the target object, despite the fact that the IsValid method is only capable of returning a single error.

Here’s the full code for the ModelValidatorAttribute and the two derived forms.

Code Snippet
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel.DataAnnotations;
  4. using System.Linq;
  5. using System.Reflection;
  6. using System.ServiceModel.DomainServices.Client;
  7.  
  8. namespace RudeValidation.Helpers
  9. {
  10.     public class ModelPropertyValidatorAttribute : ModelValidatorAttribute
  11.     {
  12.         public ModelPropertyValidatorAttribute(Type modelType)
  13.             : base(modelType, ModelValidationMode.InferProperty) { }
  14.  
  15.         public ModelPropertyValidatorAttribute(Type modelType, string propertyName)
  16.             : base(modelType, propertyName) { }
  17.     }
  18.  
  19.     public class ModelObjectValidatorAttribute : ModelValidatorAttribute
  20.     {
  21.         public ModelObjectValidatorAttribute(Type modelType)
  22.             : base(modelType, ModelValidationMode.Object) { }
  23.     }
  24.  
  25.     [AttributeUsage(AttributeTargets.Property | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = true)]
  26.     public abstract class ModelValidatorAttribute : ValidationAttribute
  27.     {
  28.         public Type ModelType { get; private set; }
  29.         public ModelValidationMode ValidationMode { get; private set; }
  30.         public string ModelProperty { get; private set; }
  31.  
  32.         private object model;
  33.  
  34.         public ModelValidatorAttribute(Type modelType, ModelValidationMode validationMode)
  35.         {
  36.             this.ModelType = modelType;
  37.             this.ValidationMode = validationMode;
  38.         }
  39.  
  40.         public ModelValidatorAttribute(Type modelType, string modelPropertyName)
  41.         {
  42.             this.ModelType = modelType;
  43.             this.ValidationMode = Helpers.ModelValidationMode.SpecifiedProperty;
  44.             this.ModelProperty = modelPropertyName;
  45.         }
  46.  
  47.         protected override ValidationResult IsValid(object value, ValidationContext validationContext)
  48.         {
  49.             if (model == null)
  50.             {
  51.                 model = Activator.CreateInstance(this.ModelType);
  52.             }
  53.  
  54.             ValidationContext redirectedContext = new ValidationContext(model, validationContext, validationContext.Items);
  55.  
  56.             switch (this.ValidationMode)
  57.             {
  58.                 case ModelValidationMode.InferProperty:
  59.                     redirectedContext.MemberName = validationContext.MemberName;
  60.                     break;
  61.                 case ModelValidationMode.SpecifiedProperty:
  62.                     redirectedContext.MemberName = this.ModelProperty;
  63.                     break;
  64.                 case ModelValidationMode.Object:
  65.                     redirectedContext.MemberName = null;
  66.                     break;
  67.             }
  68.  
  69.             ComplexObject targetEntity = validationContext.ObjectInstance as ComplexObject;
  70.             var breakOnFirstError = (targetEntity == null);
  71.             IEnumerable<ValidationResult> validationResults = TryValidateProperty(value, validationContext, redirectedContext, breakOnFirstError);
  72.  
  73.             if (validationResults.Any())
  74.             {
  75.                 if (validationResults.Count() == 1)
  76.                 {
  77.                     return validationResults.Single();
  78.                 }
  79.  
  80.                 if (targetEntity != null)
  81.                 {
  82.                     foreach (ValidationResult result in validationResults.Skip(1))
  83.                     {
  84.                         targetEntity.ValidationErrors.Add(result);
  85.                     }
  86.                 }
  87.  
  88.                 return validationResults.First();
  89.             }
  90.  
  91.             return ValidationResult.Success;
  92.         }
  93.  
  94.         private static IEnumerable<ValidationResult> TryValidateProperty(object value, ValidationContext validationContext, ValidationContext modelValidationContext, bool breakOnFirstError)
  95.         {
  96.             ICustomAttributeProvider validatorProvider;
  97.  
  98.             if (!string.IsNullOrEmpty(modelValidationContext.MemberName))
  99.             {
  100.                 validatorProvider = modelValidationContext.ObjectType.GetProperty(modelValidationContext.MemberName);
  101.             }
  102.             else
  103.             {
  104.                 validatorProvider = modelValidationContext.ObjectType;
  105.             }
  106.  
  107.             IEnumerable<ValidationAttribute> validators = validatorProvider
  108.                 .GetCustomAttributes(typeof(ValidationAttribute), true)
  109.                 .Cast<ValidationAttribute>();
  110.  
  111.             IEnumerable<ValidationResult> results = GetValidationErrors(value, validationContext, validators, breakOnFirstError);
  112.             return results;
  113.         }
  114.  
  115.         private static IEnumerable<ValidationResult> GetValidationErrors(object value, ValidationContext validationContext, IEnumerable<ValidationAttribute> attributes, bool breakOnFirstError)
  116.         {
  117.             List<ValidationResult> errors = new List<ValidationResult>();
  118.             bool hasErrors = false;
  119.             ValidationResult result;
  120.  
  121.             foreach (RequiredAttribute required in attributes.OfType<RequiredAttribute>())
  122.             {
  123.                 result = required.GetValidationResult(value, validationContext);
  124.  
  125.                 if (result != ValidationResult.Success)
  126.                 {
  127.                     errors.Add(result);
  128.                     hasErrors = true;
  129.  
  130.                     if (breakOnFirstError)
  131.                     {
  132.                         return errors;
  133.                     }
  134.                 }
  135.             }
  136.  
  137.             if (hasErrors)
  138.             {
  139.                 return errors;
  140.             }
  141.  
  142.             foreach (ValidationAttribute attribute in attributes)
  143.             {
  144.                 if (attribute is RequiredAttribute)
  145.                 {
  146.                     continue;
  147.                 }
  148.  
  149.                 result = attribute.GetValidationResult(value, validationContext);
  150.  
  151.                 if (result != ValidationResult.Success)
  152.                 {
  153.                     errors.Add(result);
  154.                     hasErrors = true;
  155.  
  156.                     if (breakOnFirstError)
  157.                     {
  158.                         return errors;
  159.                     }
  160.                 }
  161.             }
  162.  
  163.             return errors;
  164.         }
  165.     }
  166.  
  167.     public enum ModelValidationMode
  168.     {
  169.         InferProperty,
  170.         SpecifiedProperty,
  171.         Object
  172.     }
  173. }

 

RIA Services Validation Recap

This was the 10th installment in a blog post series about RIA Services Validation.  We’ve learned about the standard validators, how to create different types of custom validators, how the validation attributes get propagated, how to perform common cross-field and cross-entity validation, and now we have a utility for importing model rules into ViewModel classes.  Here’s the full series:

    1. Standard Validators
    2. Custom Validation Methods
    3. Custom Reusable Validators
    4. Attribute Propagation
    5. Validation Triggers
    6. Cross-Field Validation
    7. Entity-Level Validation
    8. Providing ValidationContext
    9. Using ValidationContext (Cross-Entity Validation)
    10. ViewModel Validation with Entity Rules

The source code for everything shown during the series is available on GitHub.