We just covered Custom Validation Methods, where we learned how to use CustomValidationAttribute to invoke a static (VB: Shared) method to perform validation.  Let’s talk about an alternate approach to custom validation though: creating custom, reusable validators by deriving from ValidationAttribute.  While it’s true that the custom validation methods used by [CustomValidation] could certainly be reusable, I tend to think of that approach as a light-weight way to call specific business logic routines for validation.  The approach we’re about to see is what I use when I’m creating an inventory of validators to be reused throughout my project(s).

Deriving from ValidationAttribute

When you derive from ValidationAttribute, there’s only one method that you must override:
Override this version of IsValid

protected override ValidationResult IsValid(object value, ValidationContext validationContext)

This version of the IsValid method was added in .NET 4.0, as part of our work in RIA Services.  The method accepts a ValidationContext and returns a ValidationResult, both of which were also added as part of our efforts.  The validation context allows the validator to understand the context in which it’s being invoked.  Information such as the ObjectInstance (entity), its type, the MemberName being validated, and its DisplayName are now available.  Before ValidationContext was introduced, validators had no clue under what conditions they were being invoked—they merely had the value to validate.  Now that we have ValidationContext, it becomes possible to check other state on the object, which is a very common scenario I call cross-field validation.

Note that ValidationAttribute has another virtual IsValid method that has the following signature:
Don’t override this version of IsValid

public override bool IsValid(object value)

 

This is the legacy form (from waaay back in .NET 3.5 days <smirk>).  As you can see, that method cannot glean any state and therefore its applications are quite limited.  In fact, when I ported ValidationAttribute to Silverlight, I made the decision to make this overload of IsValid internal instead of protected.  This helps you avoid accidentally overriding this version of the method, while still allowing the standard validators (that don’t need ValidationContext) to override it.

Aside: For a fun programming challenge – Figure out how ValidationAttribute can determine which version of IsValid has been overridden and should therefore be called during validation.

Single-Field Validation Example: DateValidatorAttribute

While the standard validators cover many validation scenarios for scalar values, there are still plenty of single-value custom validation scenarios out there.  In the scenario of scheduling a meeting for example, we want to ensure that the meeting is scheduled in the future, and not the past.  Such a validator only needs to know the value being validated (and the current date)—it doesn’t need access to the rest of the entity, or any other context to decide whether or not the value is valid.

Date validation isn’t unique to meeting times though; many other dates need to be validated too.  Direct deposit change dates must also be in the future, while birth dates are always in the past.  When we recognize that date type validation is going to be common throughout our application, it’s time to write a custom, reusable validator.  Let’s derive from ValidationAttribute to do it.

using System;
using System.ComponentModel.DataAnnotations;

namespace RudeValidation.Web.Validators
{
/// <summary>
/// Support two types of date validation:
/// 1) Ensure dates are in the past
/// 2) Ensure dates are in the future
/// </summary>
/// <remarks>
/// No date can ever be the present for more
/// than an instant.
/// </remarks>
public enum DateValidatorType
{
Past,
Future
}

/// <summary>
/// Validate that a date value is either a Past or Future date
/// as appropriate.
/// </summary>
public class DateValidatorAttribute : ValidationAttribute
{
/// <summary>
/// The type of date expected.
/// </summary>
public DateValidatorType ValidatorType { get; private set; }

/// <summary>
/// Validate that a date value is either a Past or Future date
/// as appropriate.
/// </summary>
/// <param name="validatorType"></param>
public DateValidatorAttribute(DateValidatorType validatorType)
{
this.ValidatorType = validatorType;
}

/// <summary>
/// Conditionally validate that the date is either in the past
/// or in the future.
/// </summary>
/// <param name="value">The date to validate.</param>
/// <param name="validationContext">The validation context.</param>
/// <returns>
/// <see cref="ValidationResult.Success"/> when the date matches the
/// expected date type, otherwise a <see cref="ValidationResult"/>.
/// </returns>
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
DateTime date = (DateTime)value;
int comparison = date.CompareTo(DateTime.Now);

if (comparison < 0)
{
if (this.ValidatorType != Validators.DateValidatorType.Past)
{
return new ValidationResult(
string.Format("{0} cannot be in the past", validationContext.DisplayName),
new[] { validationContext.MemberName });
}
}
else if (comparison > 0)
{
if (this.ValidatorType != Validators.DateValidatorType.Future)
{
return new ValidationResult(
string.Format("{0} cannot be in the future", validationContext.DisplayName),
new[] { validationContext.MemberName });
}
}

return ValidationResult.Success;
}
}
}

 

Be sure to save this in a file with .shared.cs in its file name, to inform RIA Services that the file should be cross-compiled to Silverlight as well.

That’s a decent amount of code, but mostly because it has copious comments and line breaks.  As I’m sure you’ve decided though: it’s not rocket science.  You might have taken note that we did end up utilizing the ValidationContext within our validation though, so that we could generate a user-friendly error message that references the DisplayName of the property being validated.  Note, that’s the DisplayName, not the MemberName, that is being injected into the error message. The MemberName is used too, but not for user-facing display.

Cross-Field Validation Example: ConditionallyRequiredAttribute

When I get a meeting invite for a meeting that will last more than an hour, I expect to see some sort of agenda in the meeting details; don’t you?  And there’s a conference room on our floor that no one can ever find, it’s room 18/3367.  Anytime anyone schedules a meeting in that room, they should be required to put directions into the meeting invite.  These types of business rules come up often: “Anytime user does X, require them to do Y.”  We need a Conditionally Required validator.  Let’s see what we can come up with.

using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;

namespace RudeValidation.Web.Validators
{
/// <summary>
/// Make a member required under a certain condition.
/// </summary>
/// <remarks>
/// Override the attribute usage to allow multiple attributes to be applied.
/// This requires that the TypeId property be overridden on the desktop framework.
/// </remarks>
[AttributeUsage(
AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter,
AllowMultiple = true)]
public class ConditionallyRequiredAttribute : RequiredAttribute
{
private MemberInfo _member;

/// <summary>
/// The name of the member that will return the state that indicates
/// whether or not the validated member is required.
/// </summary>
public string ConditionMember { get; private set; }

/// <summary>
/// The condition value under which this validator treats
/// the affected member as required.
/// </summary>
public object RequiredCondition { get; private set; }

/// <summary>
/// Comma-separated list of additional members to
/// add to validation errors. By default, the
/// <see cref="ConditionMember"/> is added.
/// </summary>
public string ErrorMembers { get; set; }

/// <summary>
/// Conditionally require a value, only when the specified
/// <paramref name="conditionMember"/> is <c>true</c>.
/// </summary>
/// <param name="conditionMember">
/// The member that must be <c>true</c> to require a value.
/// </param>
public ConditionallyRequiredAttribute(string conditionMember)
: this(conditionMember, true) { }

/// <summary>
/// Conditionally require a value, only when the specified
/// <paramref name="conditionMember"/> has a value that
/// exactly matches the <paramref name="requiredCondition"/>.
/// </summary>
/// <param name="conditionMember">
/// The member that will be evaluated to require a value.
/// </param>
/// <param name="requiredCondition">
/// The value the <paramref name="conditionMember"/> must
/// hold to require a value.
/// </param>
public ConditionallyRequiredAttribute(string conditionMember, object requiredCondition)
{
this.ConditionMember = conditionMember;
this.RequiredCondition = requiredCondition;
this.ErrorMembers = this.ConditionMember;
}

/// <summary>
/// Override the base validation to only perform validation when the required
/// condition has been met. In the case of validation failure, augment the
/// validation result with the <see cref="ErrorMembers"/> as an additional
/// member names, as needed.
/// </summary>
/// <param name="value">The value being validated.</param>
/// <param name="validationContext">The validation context being used.</param>
/// <returns>
/// <see cref="ValidationResult.Success"/> if not currently required or if satisfied,
/// or a <see cref="ValidationResult"/> in the case of failure.
/// </returns>
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (this.DiscoverMember(validationContext.ObjectType))
{
object state = this.InvokeMember(validationContext.ObjectInstance);

// We are only required if the current state
// matches the specified condition.
if (Object.Equals(state, this.RequiredCondition))
{
ValidationResult result = base.IsValid(value, validationContext);

if (result != ValidationResult.Success && this.ErrorMembers != null && this.ErrorMembers.Any())
{
result = new ValidationResult(result.ErrorMessage,
result.MemberNames.Union(this.ErrorMembers.Split(',').Select(s => s.Trim())));
}

return result;
}

return ValidationResult.Success;
}

throw new InvalidOperationException(
"ConditionallyRequiredAttribute could not discover member: " + this.ConditionMember);
}

/// <summary>
/// Discover the member that we will evaluate for checking our condition.
/// </summary>
/// <param name="objectType"></param>
/// <returns></returns>
private bool DiscoverMember(Type objectType)
{
if (this._member == null)
{
this._member = (from member in objectType.GetMember(this.ConditionMember).Cast<MemberInfo>()
where IsSupportedProperty(member) || IsSupportedMethod(member)
select member).SingleOrDefault();
}

// If we didn't find 1 exact match, indicate that we could not discover the member
return this._member != null;
}

/// <summary>
/// Determine if a <paramref name="member"/> is a
/// method that accepts no parameters.
/// </summary>
/// <param name="member">The member to check.</param>
/// <returns>
/// <c>true</c> if the member is a parameterless method.
/// Otherwise, <c>false</c>.
/// </returns>
private bool IsSupportedMethod(MemberInfo member)
{
if (member.MemberType != MemberTypes.Method)
{
return false;
}

MethodInfo method = (MethodInfo)member;
return method.GetParameters().Length == 0
&& method.GetGenericArguments().Length == 0
&& method.ReturnType != typeof(void);
}

/// <summary>
/// Determine if a <paramref name="member"/> is a
/// property that has no indexer.
/// </summary>
/// <param name="member">The member to check.</param>
/// <returns>
/// <c>true</c> if the member is a non-indexed property.
/// Otherwise, <c>false</c>.
/// </returns>
private bool IsSupportedProperty(MemberInfo member)
{
if (member.MemberType != MemberTypes.Property)
{
return false;
}

PropertyInfo property = (PropertyInfo)member;
return property.GetIndexParameters().Length == 0;
}

/// <summary>
/// Invoke the member and return its value.
/// </summary>
/// <param name="objectInstance">The object to invoke against.</param>
/// <returns>The member's return value.</returns>
private object InvokeMember(object objectInstance)
{
if (this._member.MemberType == MemberTypes.Method)
{
MethodInfo method = (MethodInfo)this._member;
return method.Invoke(objectInstance, null);
}

PropertyInfo property = (PropertyInfo)this._member;
return property.GetValue(objectInstance, null);
}

#if !SILVERLIGHT
/// <summary>
/// The desktop framework has this property and it must be
/// overridden when allowing multiple attributes, so that
/// attribute instances can be disambiguated based on
/// field values.
/// </summary>
public override object TypeId
{
get { return this; }
}
#endif
}
}


Be sure to save this in a file with .shared.cs in its file name, to inform RIA Services that the file should be cross-compiled to Silverlight as well.

Well, that’s a relatively beefy validation attribute implementation.  Again, it’s not complicated though; this validator simply requires a little bit of code, and again there are lots of comments and much whitespace.  Let’s break it down.

  1. Inherit from RequiredAttribute (instead of ValidationAttribute directly), to reuse the logic already in place;
  2. Override the attribute usage to allow multiple instances of this attribute to be applied to a member;
  3. The ConditionMember property is the string member name that indicates what property to check for the required condition;
  4. The RequiredCondition property is the value that makes the field become required;
  5. The ErrorMembers property allows additional members to be added to the validation result (see “ValidationResult Member Names” below);
  6. The first constructor accepts only the condition member, and it defaults the required condition to true, which will be very common in practice;
  7. The second constructor accepts both the condition member and the required condition; (we’ll use this for specifying that meeting details are required for a specific location);
  8. The IsValid override tries to discover the condition member; if it cannot discover it, we throw an exception (see “Bad Attribute Declarations” below);
  9. If the member was discovered, the member is invoked and the current state is compared to the required condition
  10. If (and only if) the current state indicates that a value is required, we call the base validator;
  11. DiscoverMember, IsSupportedMethod, IsSupportedProperty, and InvokeMember are reflection methods for finding and invoking the specified member using the ConditionMember string provided, the ValidationContext.ObjectType, and ValidationContext.ObjectInstance;
  12. TypeId must be overridden on the desktop framework to tell TypeDescriptionProvider to disambiguate attribute instances using their fields, since we are now allowing multiple instances of the attribute type to be applied to a single member.

 

ValidationResult Member Names

As mentioned above, when this validation fails, we are adding the ErrorMembers to the MemberNames for the ValidationResult.  We do this for two reasons:

  1. It causes the ConditionMember (or other specified members) to be highlighted when an error occurs;
  2. It forces the validation error to be cleared when any field involved is updated.

The first reason is really just about usability, but the second point is more interesting.  Consider the following scenario:

  1. Enter a Location of “18/3367”
  2. Leave the Details field blank and get a validation error stating that a meeting in this room requires directions to be included in the details;
  3. Revise the Location and choose a different room;
  4. We expect the validation error to go away.  If Location wasn’t a member of the ValidationResult, then the error would persist.

Let’s consider our other scenario too.

  1. Enter a Start time of 12:00 PM;
  2. Enter an End time of 2:00 PM;
  3. Leave the Details field blank and get a validation error stating that an agenda is required;
  4. Revise either the Start time or End time, or fill in an agenda;
  5. We expect the validation error to go away regardless of how it was corrected. All three fields must be in the ValidationResult.MemberNames array to achieve that.

Bad Attribute Declarations

When creating custom validation attributes, you will often have scenarios where the developer did not declare the attribute properly, and your attribute code needs to deal with it.  Your instinct will be to throw exceptions from either your attribute constructor or the property setters.  However, doing either of those is bad news in an attribute.  You can bring lots of things down if exceptions are thrown trying to construct attribute instances at design-time.  For instance, when you build your project, RIA Services reads the metadata from your model and performs code generation to make your entities available in Silverlight.  If your project contains an attribute declaration that throws an exception while we’re trying to do this code generation, you will end up with misleading build errors—there’s just no way around it.

So, what you must do with bad attribute declarations, is throw exceptions at run-time.  In this example, I’m throwing an InvalidOperationException from the IsValid method, which will bring down the Silverlight application with an unhandled exception as soon as validation is performed against the field that has a bad validator applied.

Applying Custom Reusable Validators

Custom validators that derive from ValidationAttribute apply to your model in the same way as standard validators.  Let’s take a look at how the specific above attributes can be applied to our Meeting entity though.  This entity and its validation have been carried over from our previous post, but I’ve highlighted the newly added lines.

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")]
[DateValidator(DateValidatorType.Future)]
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; }

[ConditionallyRequired("IsLongMeeting",
ErrorMembers = "Start, End",
ErrorMessage = "If you're asking for more than an hour of time, provide an agenda.")]
[ConditionallyRequired("Location", "18/3367",
ErrorMessage = "No one can ever find this room; please be sure to include directions.")]
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; }
}
}

 

This scenario also requires a Meeting.shared.cs file that defines a custom property on Meeting for IsLongMeeting.  We need to include this in a separate file, with the .shared.cs name convention so that the property is available on both the server and the client.

using System;

namespace RudeValidation.Web.Models
{
public partial class Meeting
{
public bool IsLongMeeting
{
get { return this.End.Subtract(this.Start) > new TimeSpan(1, 0, 0); }
}
}
}

 

Executing Derived Validation Attributes on the Client

Just as we saw with our custom validation methods, we can easily share custom reusable validation attributes by naming the files .shared.cs (VB: .shared.vb).  Ultimately, RIA Services needs to be able to find the attribute type in Silverlight and be able to construct it from the instance discovered in your Web project, so it can also be achieved through class libraries in more advanced scenarios.

Here’s what we get in the UI with these new validators, by simply editing the form values:

Start cannot be in the past

If you're asking for more than an hour of time, provide an agenda.

Notice that all three fields light up with the single error.  Editing any of the three fields will make the errors clear from all three fields.

 

No one can ever find this room; please be sure to include directions.

And the same is true here; both Location and Details light up when the hard-to-find room is entered but no details are entered.

 

All errors cleared

Once Details are entered, all of the errors are cleared.

RIA Services Validation Recap:

In this post, we learned how to derive from ValidationAttribute to create custom, reusable validators.  Let’s take a look at related posts:

    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

Don’t worry, we are just getting started!  There are a plethora of topics to discuss for RIA Services validation.  In future posts, we’ll be exploring how RIA Services actually propagates your validators to the client, when/how RIA Services will invoke each kind of validator, how to really leverage ValidationContext, and much more.