by Jeff Handley via Jeff Handley on 9/26/2010 10:39:42 AM
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).
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.
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.
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 }}
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.
As mentioned above, when this validation fails, we are adding the ErrorMembers to the MemberNames for the ValidationResult. We do this for two reasons:
The first reason is really just about usability, but the second point is more interesting. Consider the following scenario:
Let’s consider our other scenario too.
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.
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); } } }}
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:
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.
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.
Once Details are entered, all of the errors are cleared.
In this post, we learned how to derive from ValidationAttribute to create custom, reusable validators. Let’s take a look at related posts:
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.
Original Post: RIA Services Validation: Custom Reusable Validators
The content of the postings is owned by the respective author. Silverlight Feeds is not responsible for the contents of the postings. This site is automatically generated and cannot be reviewed for abusive content. If you find abusive content on Silverlight Feeds, please contact us. Designated trademarks and brands are the property of their respective owners. All rights reserved.