The System.ComponentModel.DataAnnotations.ValidationResult class was created for RIA Services, and it was introduced in Silverlight 3.0 and then added into .NET 4.0. A ValidationResult represents a validation error condition, specifying the error message and, optionally, any member names that the error corresponds to. If you use the [Required] attribute, the [Range] attribute, [StringLength], or any of the other provided ValidationAttribute classes, then ValidationResult instances are created to represent any validation failures. If you have used the [CustomValidation] attribute or if you have derived from ValidationAttribute yourself, you have likely returned an instance of ValidationResult in your code, or ValidationResult.Success if validation succeeds. Let’s look at an example—here’s code you might see from a “Business Day” validator:
using System;
using System.ComponentModel.DataAnnotations;
namespace DeliveryScheduler
{
public class BusinessDayAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value != null && value is DateTime)
{
DateTime date = (DateTime)value;
if (date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday)
{
return new ValidationResult("Saturdays and Sundays cannot be selected");
}
if (date.Month == 1 && date.Day == 1)
{
return new ValidationResult("New Year's Day cannot be selected");
}
...
if (date.Month == 12 && date.Day == 25)
{
return new ValidationResult("Christmas cannot be selected");
}
}
return ValidationResult.Success;
}
}
}
This validation attribute would be applied to a model very simply:
public class OrderDelivery
{
...
[BusinessDay]
public DateTime DeliveryDate { get; set; }
...
}
When an OrderDelivery.DeliveryDate property value is validated, the BusinessDayAttribute’s IsValid method will be executed, potentially resulting in one of the variants of ValidationResult that could be returned. Because the [BusinessDay] attribute was applied to the DeliveryDate property, one would certainly assume that the error would always be associated with the property, but of course that’s not the case, or else I wouldn’t be writing this blog post. Let’s take a look at how the BusinessDayAttribute code should be written, highlight the difference, and then discuss the anatomy of the bug at hand.
using System;
using System.ComponentModel.DataAnnotations;
namespace DeliveryScheduler
{
public class BusinessDayAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value != null && value is DateTime)
{
DateTime date = (DateTime)value;
string[] memberNames = new string[] { validationContext.MemberName };
if (date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday)
{
return new ValidationResult("Saturdays and Sundays cannot be selected", memberNames);
}
if (date.Month == 1 && date.Day == 1)
{
return new ValidationResult("New Year's Day cannot be selected", memberNames);
}
...
if (date.Month == 12 && date.Day == 25)
{
return new ValidationResult("Christmas cannot be selected", memberNames);
}
}
return ValidationResult.Success;
}
}
}
Do you see the difference? It’s subtle. This revised implementation specifies the memberNames for the ValidationResult. In this case, and in 99% of cases, we simply pass along the single ValidationContext.MemberName, but ValidationResult does have the capability to relate itself to multiple members on an object. When memberNames is not specified (the short constructor), when memberNames is null, empty, or contains an item that is null or an empty string, then the result is associated at the object (or entity) level. That means that in our first implementation where we were not specifying memberNames, the ValidationResult was associating itself with the object, and not with the property. This will lead to bugs in your applications, likely surfaced in your UI, because your validation errors will not relate to the properties that had the errors.
If you implement custom validation operations using the [CustomValidation] attribute, this same issue applies. I would like to warn you now, if you’ve written custom validation routines with either approach, you’ve likely omitted the member names, and you need to supply them. Go, fix your code now; I’ll wait here.
It’s a good thing it’s easy to fix, right? But you’re left wondering… Why haven’t I seen any problems with this? My applications are working just fine, associating the property-level errors with the properties just like I expected. So why make the change? What’s the big deal? The answer to all of these questions is something you never thought you’d hear—Thank goodness for exception-based validation!
WHAT!? Yes, that’s what I said… thank goodness for exception-based validation. It’s because of those dreaded ValidationExceptions that you have not seen any problems with the missing member names. With exception-based validation, when you return a ValidationResult from a validation attribute, that would result in an exception getting thrown from the property setter. When the property setter throws an exception, your {Binding} picks up on it and translates that exception into a validation error for the field. ValidationResult.MemberNames is never even referenced in that scenario.
Enter INotifyDataErrorInfo. This interface was introduced in Silverlight 4.0 Beta (and it does not exist in ASP.NET or WPF). INotifyDataErrorInfo was designed to allow objects to raise ErrorsChanged events whenever validation errors occur for the object or its properties. This lets you get rid of those pesky exceptions from your property setters. [Pause for applause] Now, consumers of your objects can subscribe to the ErrorsChanged event, and when you raise it, the consumers can call GetErrors to retrieve the updated list of errors—and {Binding} is a consumer that does this. But here’s the catch… when GetErrors is called, a propertyName argument is specified, and this is where ValidationResult.MemberNames comes into play. Without the member specified on your ValidationResult, it’s quite possible that you won’t be able to produce the errors associated with the propertyName requested. The net result is no property-level errors ever being returned from GetErrors unless the member names were specified on the ValidationResult instances.
Heads-up: This issue will surface in RIA Services when it implements INotifyDataErrorInfo.
I did identify a potential fix for this bug, and it would be made within the ValidationAttribute base class. The fix would be to automatically specify the ValidationContext.MemberName on any ValidationResult instance that has no members specified. This would work for both property- and object-level errors because object-level errors have a null ValidationContext.MemberName. However, the timing for fixing the bug just wasn’t right. With .NET 4.0 in RC, and Silverlight 4.0 in Beta, the bug would have to meet a pretty high bar to get fixed in either release, let alone both. Since there is such a simple workaround, it wasn’t worth the risk to either product to push the fix through. Instead, I take my lumps and write this blog post. If you have any custom validation code returning ValidationResult instances, I do recommend that you go ahead and supply the member name—it won’t affect your application now, but it will make your adoption of INotifyDataErrorInfo much less frustrating.