EF and buddy classes with jQuery validation

Apr 23, 2009 at 7:01 PM
I am trying to get jQuery validation working (the server side validation is working), I am using DataAnnotations for server side validation.

My classes are generated with Entity Framework and I use partial classes to add metadata.

My User class

    [MetadataType(typeof(Metadata))]
    public partial class User
    {
        private class Metadata
        {
            [Required]
            [StringLength(20, ErrorMessage = "Username is too long")]
            public string Username { get; set; }

            [Required]
            public string Email { get; set; }

            [Required]
            public string Name { get; set; }
        }
    }

This works for server side validation and I get the correct error messages.

When I call Html.ClientSideValidation<User>() in my markup, this is the output.
<script type="text/javascript">xVal.AttachValidator(null, {"Fields":[{"FieldName":"Id","FieldRules":[{"RuleName":"DataType","RuleParameters":{"Type":"Integer"}}]}]})</script>
It only adds validation for Id, because it is an integer.. should not even add that validation.


I took a look at the xVal code
In PropertyAttributeRuleProviderBase<TAttribute> class, in method GetRulesFromTypeCore(Type type)
typeDescriptor.AssociatedMetadataType = null

But I think that should be my buddy class.
typeDescriptor is of type AssociatedMetadataTypeTypeDescriptionProvider

Seems that the problem is that the code is not finding the buddy class. Any ideas?
Apr 25, 2009 at 8:36 PM
What did you do to get buddy classes to work in server-side?  I took the Booking demo, change the Booking class to a partial and moved the attributes into another partial class file without any success.  Even taking the client-side validation out, the only thing that is validated are the integers, and they come back with a "A value is required" message instead of a working message.  I played around with adding and removing attributes and nothing done in the buddy class had any effect.

Here was my buddy class...
    [MetadataType(typeof(Metadata))]
    public partial class Booking {
        private class Metadata {
            [Required]
            [StringLength(15)]
            public string ClientName { get; set; }

            //[Range(1, 20)]
            public int NumberOfGuests { get; set; }

            //[Required]
            //[DataType(DataType.Date)]
            public DateTime ArrivalDate { get; set; }
        }
    }
Apr 26, 2009 at 11:25 AM
I had to add this to Application_Start in global.asax:
binders.DefaultBinder = new Microsoft.Web.Mvc.DataAnnotations.DataAnnotationsModelBinder();
Apr 26, 2009 at 11:29 AM
And this is my controller method:
        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Register(User user)
        {
            if (ModelState.IsValid)
            {
                // Create user in our database
                return Login(user);
            }
            return View(user);
        }
Apr 26, 2009 at 7:28 PM
Oh, I didn't realize you used the Microsoft.Web.Mvc.DataAnnotations dll with xVal.  I thought xVal was meant to completely replace that framework.  However, since the DefaultModelBinder from System.Web.Mvc validates and fills the ModelState object when it binds anyway, we might as well use it to take advantage of its ability to use buddy classes.  I think my best course of action currently is to get client-side validation working with the Mvc.DataAnnotations dll and worry about moving the timing of the validation to the model Save at a later date.  I simply can't do without buddy classes, and its looking like it would take more work (at least for me) to get xVal to work for me than it will to extend the other.  Perhaps I'll end up with a hacked-together conglomeration of the two.
Apr 27, 2009 at 1:52 AM
Ok, thanks. I will then stop using xVal since it does not support buddy classes. I will use server side validation only for now. Please let me know if you get it working.
Apr 27, 2009 at 4:57 PM
Well, I extended the Mvc.DataAnnotations dll to have RunWhen functionality, and I altered some attributes to treat Int32.MinVal and DateTime.MinVal as null/not provided.  I don't have any client-side validation working and I'm still in total agreement with Steve that the Mvc.DataAnnotations has some serious flaws.  The timing the validation occurs is one, but the biggest is the fact that it is totally user-input driven.  If there isn't a FormCollection key in the POST for the field, its validation does not get executed!  What is that?  Just because I don't allow the user to input a value for a property doesn't mean the property's validation should be ignored.  The model's integrity shouldn't depend on what the user is allowed to input.  Still, I'm able to limp along with the Mvc.DataAnnotations for now.

If Steve can get me past some of my initial issues with xVal (lack of buddy class support and the fact that the default ModelBinder executes and messes with ModelState regardless of xVal usage), I'd rather spend my time extending xVal.
Coordinator
Apr 27, 2009 at 5:13 PM
Hi guys

Really sorry that I haven't responded to this discussion before now. I am rushing madly with all kinds of things at present and will get to this as soon as I can, which will be early next week.

xVal certainly is supposed to support buddy classes - and did last time I checked - though I guess there must be something not working as you expect. I will check this out as soon as I can and let you know what went wrong and how to fix it. In the meantime, if you can post a minimal demo project showing buddy classes not working, I'll know for sure that I've understood your scenario correctly.

Steve
Apr 27, 2009 at 11:50 PM
Just take the Booking example from your site, comment out the attributes on the Booking class, make the class partial, and create the buddy class I posted above.  The attribute on the ClientName is the only one that matters because the integer and the DateTime are going to get validated no matter what.  I also need to figure out an elegant way to prevent the default binder from mucking up ModelState when it binds the entity.  Is your best practice to go ahead and let it do its thing, or is that just in there because it's a pain to remove?  I can see where the controller might want to know if the input from the user is faulty, but it seems redundant in the majority of cases if your model is going to do it on Save.

We're crazy busy here too, or I would have spent more time trying to figure out xVal.  I'm ok for the moment, but would prefer a much more well-rounded solution in the near future.  I bet we can get xVal there long before MS fixes their solution.
May 19, 2009 at 3:40 AM

I'm actually running into this problem also.  I was thinking that the problem was indeed that buddy classes were not being automatically processed.  In xVal.RuleProviders.PropertyAttributeRuleProviderBase I found GetRulesFromTypeCore which looks to query only the properties on the object, ignoring any Metadata classes. 

What is has

(from prop in typeDescriptor.GetProperties().Cast<PropertyDescriptor>()
from rule in GetRulesFromProperty(prop)
select new KeyValuePair<string, Rule>(prop.Name, rule));

I have expanded this to

protected override RuleSet GetRulesFromTypeCore(Type type)
{
var typeDescriptor = metadataProviderFactory(type).GetTypeDescriptor(type);
var rules = (from prop in typeDescriptor.GetProperties().Cast<PropertyDescriptor>()
from rule in GetRulesFromProperty(prop)
select new KeyValuePair<string, Rule>(prop.Name, rule));

var metadataAttrib = type.GetCustomAttributes(typeof(MetadataTypeAttribute), true).OfType<MetadataTypeAttribute>().FirstOrDefault();
var buddyClassOrModelClass = metadataAttrib != null ? metadataAttrib.MetadataClassType : type;
var buddyClassProperties = TypeDescriptor.GetProperties(buddyClassOrModelClass).Cast<PropertyDescriptor>();
var modelClassProperties = TypeDescriptor.GetProperties(type).Cast<PropertyDescriptor>();

var buddyRules = from buddyProp in buddyClassProperties
join modelProp in modelClassProperties on buddyProp.Name equals modelProp.Name
from rule in GetRulesFromProperty(buddyProp)
select new KeyValuePair<string, Rule>(buddyProp.Name, rule);

rules = rules.Union(buddyRules);
return new RuleSet(rules.ToLookup(x => x.Key, x => x.Value));
}

Which is doing a good job of getting all the buddy class rules as well. Amusingly I'm also working on a User data type and in the javascript I now have

<script type="text/javascript">xVal.AttachValidator("user", {"Fields":[{"FieldName":"userName","FieldRules":[{"RuleName":"StringLength","RuleParameters":{"MaxLength":"50"}},{"RuleName":"Required","RuleParameters":{}},{"RuleName":"Required","RuleParameters":{}},{"RuleName":"StringLength","RuleParameters":{"MaxLength":"50"}}]},{"FieldName":"firstName","FieldRules":[{"RuleName":"StringLength","RuleParameters":{"MaxLength":"50"}},{"RuleName":"Required","RuleParameters":{}},{"RuleName":"Required","RuleParameters":{}},{"RuleName":"StringLength","RuleParameters":{"MaxLength":"50"}}]},{"FieldName":"lastName","FieldRules":[{"RuleName":"StringLength","RuleParameters":{"MaxLength":"50"}},{"RuleName":"Required","RuleParameters":{}},{"RuleName":"Required","RuleParameters":{}},{"RuleName":"StringLength","RuleParameters":{"MaxLength":"50"}}]},{"FieldName":"description","FieldRules":[{"RuleName":"StringLength","RuleParameters":{"MaxLength":"500"}},{"RuleName":"StringLength","RuleParameters":{"MaxLength":"500"}}]},{"FieldName":"userEMail","FieldRules":[{"RuleName":"StringLength","RuleParameters":{"MaxLength":"100"}},{"RuleName":"Required","RuleParameters":{}},{"RuleName":"Required","RuleParameters":{}},{"RuleName":"StringLength","RuleParameters":{"MaxLength":"100"}}]}]})</script>

Is there some way to submit patches to codeplex projects?

Jun 14, 2009 at 12:59 PM

Thanks Stimms,

That made it work! I hope you're code gets submited to Xval.

Coordinator
Jun 18, 2009 at 9:09 PM

I'm a little confused by this discussion, because support for buddy classes (i.e., [MetadataType]) was added in xVal 0.8 and definitely appears to be working.

See http://xval.codeplex.com/WorkItem/View.aspx?WorkItemId=1666 or http://goneale.com/2009/03/04/using-metadatatype-attribute-with-aspnet-mvc-xval-validation-framework/ for more details.

If I've misunderstood, or if there's a bug in xVal, please send me a demo project that reproduces the problem and I'll try to fix it ASAP, using the code posted by Stimms if appropriate.

Jun 18, 2009 at 10:28 PM

I was able to reproduce the problem using your sample app - Booking example, with the partial class I posted above.  I haven't tried it in awhile because I haven't been messing with validation, but I'll look into it again.

Jun 19, 2009 at 7:30 AM

I've removed the patch code and reverted to the original XVal 0.8 code (not the binary) and buddy classes seem to work properly with XVal.

Jun 23, 2009 at 4:11 AM
Edited Jun 23, 2009 at 4:13 AM

 

Okay let's duplicate the problem.  If we download your demo application and modify it like this:
Booking.cs:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;

namespace DomainModel
{
    [MetadataType(typeof(BookingValidator))]
    public class Booking
    {
        
        public string ClientName { get; set; }
        
    
        public int NumberOfGuests { get; set; }

        
        public DateTime ArrivalDate { get; set; }
    }
}
BookingValidator.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.DataAnnotations;

namespace DomainModel
{
    class BookingValidator
    {

        [Required]
        [StringLength(15)]
        public string ClientName { get; set; }

        [Range(1, 20)]
        public int NumberOfGuests { get; set; }

        [Required]
        [DataType(DataType.Date)]
        public DateTime ArrivalDate { get; set; }
    }
}
Now running the demo we get basically no validation server side or client side but this is because the demo doesn't support buddy classes in the DataAnnotationsValidationRunner.  So it becomes:
internal static class DataAnnotationsValidationRunner
    {
        /// Warning: For some reason, DataTypeAttribute.IsValid() always returns "true", regardless of whether
        /// it is actually valid. Need to improve this test runner to fix that.
        public static IEnumerable<ErrorInfo> GetErrors(object instance)
        {
            var metadataAttrib = instance.GetType().GetCustomAttributes(typeof(MetadataTypeAttribute), true).OfType<MetadataTypeAttribute>().FirstOrDefault();
            var buddyClassOrModelClass = metadataAttrib != null ? metadataAttrib.MetadataClassType : instance.GetType();
            var buddyClassProperties = TypeDescriptor.GetProperties(buddyClassOrModelClass).Cast<PropertyDescriptor>();
            var modelClassProperties = TypeDescriptor.GetProperties(instance.GetType()).Cast<PropertyDescriptor>();

            return from buddyProp in buddyClassProperties
                   join modelProp in modelClassProperties on buddyProp.Name equals modelProp.Name
                   from attribute in buddyProp.Attributes.OfType<ValidationAttribute>()
                   where !attribute.IsValid(modelProp.GetValue(instance))
                   select new ErrorInfo(buddyProp.Name, attribute.FormatErrorMessage(string.Empty), instance);  
        }
    }
Now we have server side validation but still no client side validation.  
<script type="text/javascript">xVal.AttachValidator(null, {"Fields":[{"FieldName":"Id","FieldRules":[{"RuleName":"DataType","RuleParameters":{"Type":"Integer"}}]}]})</script>
Apply my patch above to the xVal library I get:
<script type="text/javascript">xVal.AttachValidator("booking", {"Fields":[{"FieldName":"ClientName","FieldRules":[{"RuleName":"Required","RuleParameters":{}},{"RuleName":"StringLength","RuleParameters":{"MaxLength":"15"}},{"RuleName":"Required","RuleParameters":{}},{"RuleName":"StringLength","RuleParameters":{"MaxLength":"15"}}]},{"FieldName":"NumberOfGuests","FieldRules":[{"RuleName":"Range","RuleParameters":{"Min":"1","Max":"20","Type":"decimal"}},{"RuleName":"DataType","RuleParameters":{"Type":"Integer"}},{"RuleName":"Range","RuleParameters":{"Min":"1","Max":"20","Type":"decimal"}},{"RuleName":"DataType","RuleParameters":{"Type":"Integer"}}]},{"FieldName":"ArrivalDate","FieldRules":[{"RuleName":"Required","RuleParameters":{}},{"RuleName":"DataType","RuleParameters":{"Type":"Date"}},{"RuleName":"Required","RuleParameters":{}},{"RuleName":"DataType","RuleParameters":{"Type":"Date"}}]}]})</script> 
A much richer validation.  

 

Jun 23, 2009 at 4:17 AM

I've zipped up my project and put it here:

http://wopsle.net/demo/xValDemo2.zip

if you open it up the xVal.dll is one with my patch and the xVal.dll.dist is the original xVal dll.  I find that if you replace the dll you have to clean the project to get it to pick up the new dll.  

Coordinator
Jun 23, 2009 at 7:08 AM
Edited Jun 23, 2009 at 7:10 AM

Hi Stimms

Thanks very much for taking the time to provide steps to reproduce the problem, and for sending in a demo project showing the problem. That makes it very easy to diagnose the issue unambiguously.

The problem is that you're using xVal 0.5, whereas support for buddy classes was introduced in version 0.8. I believe you got this demo project from my blog post dated January 10th which accompanied the original 0.5 release. I referenced the newer 0.8 version in a blog post dated February 27th.
To confirm this, open Windows Explorer and try right-clicking on your xVal.dll assembly (the original, unpatched one) and choosing "Properties". On the "Details" tab, you'll see it states "Product version: 0.5.0.0".

Please get the updated 0.8 release - either from http://xval.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=23946, or compile the latest source code downloaded from http://xval.codeplex.com/SourceControl/ListDownloadableCommits.aspx. You'll find that your Booking/BookingValidator classes work fine.

Note that you'll still need your buddy-class-aware server-side DataAnnotations runner, since xVal doesn't include any server-side DataAnnotations runner (just like it doesn't include server-side runners for other validation frameworks such as Castle Validator or NHibernate Validation).

Cheers!

Steve

Jun 23, 2009 at 3:33 PM

Ah well that could be it.  Would it be possible to update the sample application with 0.8?  It might also be handy to put in another example which uses buddy classes.  I can modify the project I have to do just that if you would be willing to update the sample linked to from http://blog.codeville.net/2009/01/10/xval-a-validation-framework-for-aspnet-mvc/.  It is the first hit from google and might be nice to keep up to date.

Coordinator
Jun 24, 2009 at 7:33 AM

I've now updated the blog post you mention with a notice to indicate that it isn't the latest version.

As for the old demo project - I'd rather not be trying to keep that up-to-date every time xVal changes (typically I regard old blog posts as historical documents), but will consider putting an "official demo project" on xVal's Codeplex pages which would be kept up-to-date.

Thanks!

Aug 10, 2009 at 8:34 PM

@thorsteinsson et al.: I was having the same problem until I removed the DataAnnotationsModelBinder, I suspect this item is not compatible with xVal.

http://aspnet.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=24471