views:

475

answers:

2

I'm using xVal in my ASP.NET MVC application, which is great in general. Following Steve Sanderson's blog post, I created a DataAnnotationsValidationRunner to do server-side validation of attributed objects. This works great for a simple class. e.g. Person:

public static class DataAnnotationsValidationRunner
{
    public static IEnumerable<ErrorInfo> GetErrors(object o)
    {
        return from prop in TypeDescriptor.GetProperties(o).Cast<PropertyDescriptor>()
               from attribute in prop.Attributes.OfType<ValidationAttribute>()
               where !attribute.IsValid(prop.GetValue(o))
               select new ErrorInfo(prop.Name, attribute.FormatErrorMessage(string.Empty), o);
    }
}

public class Person
{
    [Required(ErrorMessage="Please enter your first name")]
    public string FirstName { get; set; }

    [Required(ErrorMessage = "Please enter your last name")]
    public string LastName { get; set; }
}

However, if I add an Address property to this person, and mark the Address class with DataAnnotation attributes, they will not be validated. e.g.

public class Person
{
    [Required(ErrorMessage="Please enter your first name")]
    public string FirstName { get; set; }

    [Required(ErrorMessage = "Please enter your last name")]
    public string LastName { get; set; }

    public Address Address { get; set; }
}

public class Address 
{
    [Required(ErrorMessage="Please enter a street address")]
    public string Street { get; set; }

    public string StreetLine2 { get; set; }

    [Required(ErrorMessage = "Please enter your city")]
    public string City { get; set; }

    [Required(ErrorMessage = "Please enter your state")]
    public string State { get; set; }

    [Required(ErrorMessage = "Please enter your zip code")]
    public string Zip { get; set; }

    public string Country { get; set; }
}

One problem is that the DataAnnotationValidationRunner doesn't walk down the complex child properties. Also, if those errors are added to an errors collection, they still need to get prefixed correctly when added to the model state. For example. The Person errors are added like this:

    catch (RulesException ex)
    {
        ex.AddModelStateErrors(ModelState, "person");
    }

I think the Address rules exceptions would need to be prefixed with "person.address". Is there a supported way of handling child object validation with xVal, or would creating a flattened data transfer object be the only solution?

+1  A: 

First of all, you need to differ between the Steve Sanderson's DataAnnotationsModelBinder and

Regarding your first question ("DataAnnotationValidationRunner doesn't walk down the complex child properties"):

Are you perhaps referring to Brad Wilson's DataAnnotationModelBinder? If so, it really should validate complex ViewModels down to the last properties. If not, try using that instead of the DataAnnoationsModelRunner you are using. This blog article blog article on Client-Side Validation with xVal shows how.

The first version of the DataAnnotationModelBinder had a bug that would make it crash when used with complex viewmodels. Perhaps there is a new version out which fixes the crash but ignores complex models?

In any case, I'd suggest giving the version of the DataAnnotationModelBinder used in the demo project of the blog article linked above a try. I am using it in my own real-world project and it does work on complex viewmodels.

Regarding your second question "Is there a supported way of handling child object validation with xVal":

You haven't posted any code residing on ASPX forms, but you might also be referring to the fact that a <%= Html.ClientSideValidation()%> only adds client-side validation to immediate properties of that Model Type, but not properties of child objects. You can simply circumvent the problem by using multiple ClientSideValidation statements, for example:

<%= Html.ClientSideValidation<ModelType>()%>
<%= Html.ClientSideValidation<ChildModelType>("ChildModelPropertyName")%>
Adrian Grigore
The second answer does work, but breaks down if you have multiple properties on the parent object that all associate the same child type, but with different propeties. ie.User BillingAddress ShippingAddressIt looks as if you could tweak the xval.jquery validation utility to fix this however.
Sean Chambers
+1  A: 

I had the same problem. I needed to validate complex objects that can appear as a property of another object. I haven't gotten into client side validation (yet), but the idea from Adrian Grigore about multiple html.ClientSideValidation() seems like it may be the ticket there.

I ended up creating a marker interface that marks all the classes that I need to validate. It could be an attribute or you could use this idea for all properties of a class.

Basically it validates the object using the DataAnnotationsValidationRunner you mention above and then iterates over the properties of the object and runs the DataAnnotationsValicationRunner over all of those until there are no more to check.

Here's pseudo code for what I did:

IEnumarable<ValidationError> GetErrors(object instance) {
    List<ValidationError> errors = new List<ValidationError>();
    errors.AddRange(GetDataAnnotationErrors(instance));
    errors.AddRange(GetPropertyErrors(instance));
    return errors;
}
IEnumerable<ValidationError> GetDataAnnotationErrors(object instance) {
    // code very similar to what you have above
}
IEnumearable<ValidationError> GetPropertyErrors(object instance)
{
     var errors = new List<ValidationError>();
     var objectsToValidate = instance.GetType().GetProperties().Where(p => p.PropertyType.GetInterface().Contains(typeof(IMarkerInterface)));
     // the call above could do any type of reflecting over the properties you want
     // could just check to make sure it isn't a base type so that all custom 
     // object would be checked
     if(objectsToValidate == null) return errors;
     foreach(object obj in objectsToValidate)
     {
          errors.AddRange(GetDataAnnotationErrors(obj));
          errors.AddRange(GetPropertyErrors(obj));
     }
     return errors;
}

I hope this is clear. I've been testing this system on domain objects and so far so good. Working out a few kinks here and there, but the idea has proven sound for what I'm doing.

Rob Sutherland