Nathan
Nathan

Reputation: 10784

How to add custom ClientValidationRules (unobtrusive validation) for a complex type on a model?

Say I have a custom validation attribute ValidateFooIsCompatibleWith model like so:

public class FooPart
{
    public string Foo { get; set; }

    public string Eey { get; set; }
}

public class FooableViewModel
{
    public FooPart Foo1 { get; set; }

    [ValidateFooIsCompatibleWith("Foo1")]
    public FooPart Foo2 { get; set; }
}

Let's say I also have custom EditorTemplates defined for FooPart:

 @Html.TextBoxFor(m => m.Foo)
 @Html.TextBoxFor(m => m.Eey)

And thus my view is essentially:

@Html.EditorFor(m => m.Foo1)
@Html.EditorFor(m => m.Foo2)

Server side, the validation works fine. However, no matter what I try, I can't get the rendered html to add the rule.

If I implement IClientValidatable, it turns out that GetClientValidationRules() never gets called. (I have successfully used IClientValidatable with "simple" fields before).

I also tried registering my own custom adapter by inheriting from DataAnnotationsModelValidator<TAttribute> and registering it in the global.asax with DataAnnotationsModelValidatorProvider.RegisterAdapter(...) That approach too fails to call GetClientValidationRules().

** Update ** If a add both a custom ModelMetadataProvider and a custom ModelValidatorProvider so that I can set breakpoints, I notice an interesting bit of behavior:

How can I get the MVC framework to register my custom client validation rules for complex types?

Upvotes: 1

Views: 828

Answers (2)

Nathan
Nathan

Reputation: 10784

I found a solution:

First, Create a custom model metadata provider (see https://stackoverflow.com/a/20983571/24954) that checks the attributes on the complex type, and stores a client validatable rule factory in the AdditionalValues collection, e.g. in the CreateMetadataProtoype override of the CachedDataAnnotationsModelMetadataProvider

 var ruleFactories = new List<Func<ModelMetadata, ControllerContext, IEnumerable<ModelClientValidationRules>>>();
 ...
 var clientValidatable = (IClientValidatable)attribute;
 ruleFactories.Add(clientValidatable.GetClientValidationRules);
 ...
 result.AdditionalValues.Add("mycachekey", ruleFactories);

Next, register this as the default metadata provider in the global.asax

protected void Application_Start()
{
    ModelMetadataProviders.Current = new MyCustomModelMetadataProvider();
    ....
}

Then I created an html helper that would process the modelmetata and create/merge the "data-val*" html attributes from each of AdditionalValues collection.

public static IDictionary<string, Object> MergeHtmlAttributes<TModel>(this HtmlHelper<TModel>, object htmlAttributes = null)
{
    var attributesDictionary = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
    //ensure data dictionary has unobtrusive validation enabled for the element
    attributesDictionary.Add("data-val", "true");

    //loop through all the rule factories, and execute each factory to get all the rules
    var rules = ruleFactory(helper.Html.ViewData.ModelMetadata, helper.Html.ViewContext);

    //loop through and execute all rules in the ruleFactory collection in the AdditionalValues
    //and add the data-val attributes for those.
    attributesDictionary.add("data-val-" + rule.ValidationType, ruleErrorMessage);

    //similarly for anything in the rule.ValidationParameters
    attributesDictionary.Add("data-val-" + rule.ValidationType + "-" + parameterName, parameterValue);

}

Finally, in my editor template, call the html helper (which has a model type of `FooPart1) for each complex type property, e.g.

 @Html.TextBoxFor(m => m.Foo, Html.MergeHtmlAttributes(new { @class="Bar"}))
 @Html.TextBoxFor(m => m.Eey, Html.MergeHtmlAttributes())

I actually ended up creating a second interface (with the same signature as IClientValidatable) that allowed me to customize rules (primarily for error messages) for the individual fields of a complex type. I also extended the helper to take a string argument that could be formatted with my custom rules.

Upvotes: 1

stink
stink

Reputation: 865

jQuery.validator.setDefaults({
  success: "valid"
});
$( "#foo" ).validate({
rules: 
{
   rule1: {required: true, min: 3},
   parent: 
   {
        required: function(element) {return $("#age").val() < 13;}    
   }
 }
});

Complex types seem to hassle me for no good reason so try the Jquery validator. Depending on what you're trying to validate it might get the job done.

Upvotes: 0

Related Questions