In our last installment, we learned about the standard validators that ship in the System.ComponentModel.DataAnnotations assembly.  RequiredAttribute, RangeAttribute, StringLengthAttribute, and RegularExpressionAttribute all derive from ValidationAttribute and cover many common validation scenarios.  Every system has its own business rules though, and therefore it’s imperative that RIA Services allows you to create custom validators to implement your own logic.  Moreover, your custom error messages must plug into the same framework that powers the standard validators.  To address this requirement, we added CustomValidationAttribute.

Using CustomValidationAttribute to Invoke a Method

System.ComponentModel.DataAnnotations.CustomValidationAttribute allows you to easily redirect a validator to a method you’ve defined in your code using a ValidatorType and Method name.  Like the standard validation attributes, it also allows you to specify an ErrorMessage or an ErrorMessageResourceType/ErrorMessageResourceName pair, but let’s focus on the ValidatorType and Method properties.  It’s actually quite straight-forward: you create a method that can perform validation for a value and you point a [CustomValidation] attribute to that method.  This can be done either at the property-level or at the entity-level.  Here’s a sample custom validation method that we will be applying to our Meeting entity:

using System;
using System.ComponentModel.DataAnnotations;
using RudeValidation.Web.Models;

namespace RudeValidation.Web.Validators
{
    /// <summary>
    /// Custom validators that apply to meetings.
    /// </summary>
    public static class MeetingValidators
    {
        /// <summary>
        /// Ensure that meetings aren't scheduled to start too early.
        /// </summary>
        /// <param name="meetingStartTime">
        /// The time the meeting it set to start.
        /// </param>
        /// <param name="validationContext">
        /// The context for the validation being performed.
        /// </param>
        /// <returns>
        /// A <see cref="ValidationResult"/> with an error or <see cref="ValidationResult.Success"/>.
        /// </returns>
        public static ValidationResult NoEarlyMeetings(
DateTime meetingStartTime,

            ValidationContext validationContext)
        {
            if (meetingStartTime.TimeOfDay.Hours < 9)
            {
                return new ValidationResult(
                    "While you may be an early bird, it's not fair to schedule a meeting before 9:00 AM."
                    , new[] { validationContext.MemberName });
            }

            return ValidationResult.Success;
        }
   }
}

Here are the 10 salient points for writing a custom validation method:

  1. The method’s class must be public;
  2. It does not matter if the method’s class is static (VB: Shared) or not (in fact, many choose to put their validation methods within the entity class itself);
  3. The method must be public;
  4. The method must be static (VB: Shared);
  5. The method must have a return type of ValidationResult;
  6. The method must accept a value parameter (it cannot be ref/ByRef) but the parameter name does not matter, and the parameter type can be as specific as desired;
  7. The method should accept a second parameter of type ValidationContext, though it’s not required; no other parameters can be accepted;
  8. When validation succeeds, return ValidationResult.Success;
  9. When validation fails, return a ValidationResult instance;
  10. ValidationResult takes an IEnumerable<string> of member names; you will almost always specify the single MemberName from the validationContext.

Applying the Validator

Here’s how we then apply this validation method to our entity, in this case to the meeting’s start time property, with the added line highlighted.

using System;
using System.ComponentModel.DataAnnotations;
using RudeValidation.Web.Resources;
using RudeValidation.Web.Validators;

namespace RudeValidation.Web.Models
{
    public partial class Meeting
    {
        [Key]
        public int MeetingId { get; set; }

        [Required]
        [CustomValidation(typeof(MeetingValidators), "NoEarlyMeetings")]
        public DateTime Start { get; set; }

        [Required]
        public DateTime End { get; set; }

        [Required]
        [StringLength(80, MinimumLength = 5,
            ErrorMessageResourceType = typeof(ValidationErrorResources),
            ErrorMessageResourceName = "TitleStringLengthErrorMessage")]
            // {0} must be at least {2} characters and no more than {1}.
        public string Title { get; set; }

        public string Details { get; set; }

        [Required]
        [RegularExpression(@"\d{1,3}/\d{4}",
            ErrorMessage = "{0} must be in the format of 'Building/Room'")]
        public string Location { get; set; }

        [Range(2, 100)]
        [Display(Name = "Minimum Attendees")]
        public int MinimumAttendees { get; set; }

        [Range(2, 100)]
        [Display(Name = "Maximum Attendees")]
        public int MaximumAttendees { get; set; }
    }
}

Executing Custom Validation Methods on the Client

The MeetingValidators class was created in a file named MeetingValidators.shared.cs.  By using the .shared.cs (or .shared.vb) convention on the file name, RIA Services will automatically copy this class to your Silverlight project, making this validation method available on the client as well as on the server.  Because this file will get automatically copied (and compiled into) your Silverlight project behind-the-scenes, it’s important to remove any using statements that aren’t needed, as they may refer to namespaces that don’t exist in Silverlight, like System.Web.

Now, let’s take a look at the result we get in the UI:

image

There, now no one can schedule meetings before 9:00am!

Let’s recap what was involved in getting this custom validator implemented, end to end:

  1. Create MeetingValidators.shared.cs in the ASP.NET project;
  2. Write the NoEarlyMeetings method, abiding by the guidelines laid out above;
  3. Add a [CustomValidation] attribute to our Start property, specifying the MeetingValidators type and the NoEarlyMeetings method name;

That’s it! I don’t know if it could be much simpler.  This custom validation logic will now be executed on both the client and the server.

RIA Services Validation Recap:

    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

[9/6/2011] The source code for everything shown during the series is available on GitHub.

Digging Deeper

I will continue digging deeper into RIA Services Validation.  In future posts, we’ll be exploring how RIA Services actually propagates your validators to the client, how you can create custom, reusable validators by deriving from ValidationAttribute directly, as well as when/how RIA Services will invoke each kind of validator, and much more!