2011/06/19

ASP.Net MVC 3 partial form validation on server and client (unobtrusive) sides.

If your view has more than one submit button and each button must validate different fields of the same form then this guide is the solution.
You will learn how to:
- Choose different actions to each submit button
- Choose which model properties doesn't have to be validated on actions
- Skip client side validation when a submit button is clicked.

When you click a submit button, its name is sent to server.

<input type="submit" name="announce" value="Announce!" />

In the case above, we know the submit button announce was clicked because Request["announce"] is not null. It's value is "Announce!".

So let's create an ActionMethodSelector that will allow us to instruct our action method to answer only when the submit button is clicked.

public class WhenRequestContainsKeyAttribute : ActionMethodSelectorAttribute
{
    public string Key { get; set; }

    public WhenRequestContainsKeyAttribute(string key)
    {
        this.Key = key;
    }

    public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
    {
        return controllerContext.HttpContext.Request[this.Key] != null;
    }
}

And now we use that ActionMethodSelector on our controller action method:

[HttpPost]
[WhenRequestContainsKey("announce")]
public ViewResult Add(MyModel m)
{
    //do whatever
}

Ok. The first stage is complete. Now what if we want an action doesn't validate some model properties?
Once again we create an attribute:

public class IgnoreValidationAttribute : Attribute
{
    public string[] IgnoredProperties { get; set; }

    public IgnoreValidationAttribute(params string[] ignoredProperties)
    {
        this.IgnoredProperties = ignoredProperties;
    }
}

And decorate our controller action method:

[HttpPost]
[ActionName("Add")]
[WhenRequestContainsKey("savedraft")]
[IgnoreValidation("Description")]
public ViewResult SaveDraft(MyModel m)
{
    //do whatever
}

In this case we don't want Description to be validated. Observe that action name is Add although method name is SaveDraft. That's because we can't have two Add methods with the same signature.
But wait. IgnoreValidationAttribute is just a simple Attribute. MVC doesn't know what to do with it.
The class responsible for the validation process is DataAnnotationsModelValidatorProvider. It instantiates lots of ModelValidators. Each validator validates each model property. If Description is decorated with [Required] then a validator is created to require its value. We need to disable that validator. So we'll replace DataAnnotationsModelValidatorProvider with our CustomDataAnnotationsModelValidatorProvider:

public class CustomDataAnnotationsModelValidatorProvider : DataAnnotationsModelValidatorProvider
{
    protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
    {

        ReflectedControllerDescriptor controllerDescriptor = new ReflectedControllerDescriptor(context.Controller.GetType());
        ReflectedActionDescriptor actionDescriptor = (ReflectedActionDescriptor)controllerDescriptor.FindAction(context, context.RouteData.GetRequiredString("action"));
        object[] actionIgnoredAttributes = actionDescriptor.GetCustomAttributes(typeof(IgnoreValidationAttribute), true);

        List<string> ignoredProperties = new List<string>();

        foreach (Attribute attribute in actionIgnoredAttributes)
        {
            foreach (string ignoredProperty in ((IgnoreValidationAttribute) attribute).IgnoredProperties)
            {
                ignoredProperties.Add(ignoredProperty);
            }
        }

        bool performServerValidation = !ignoredProperties.Contains(metadata.PropertyName);
        var validators = base.GetValidators(metadata, context, attributes);

        foreach (var validator in validators)
            yield return new ModelValidatorInterceptor(validator, performServerValidation, metadata, context);
    }
}

It will replace the ordinary validators with our ModelValidatorInterceptor. This new validator knows whether server validation must be performed or not:

public class ModelValidatorInterceptor : ModelValidator
{
    ModelValidator Validator;
    bool PerformServerValidation;

    public ModelValidatorInterceptor(ModelValidator validator, bool performServerValidation, ModelMetadata metadata, ControllerContext controllerContext)
        : base(metadata, controllerContext)
    {
        this.Validator = validator;
        this.PerformServerValidation = performServerValidation;
    }

    public override IEnumerable<ModelValidationResult> Validate(object container)
    {
        if (this.PerformServerValidation)
        {
            foreach (var result in this.Validator.Validate(container))
                yield return result;
        }
        else
        {
            yield break;
        }
    }

    public override bool IsRequired
    {            
        get 
        {
            if (this.PerformServerValidation)
            {
                return this.Validator.IsRequired;
            }
            else
            {
                return false;
            }
        }
    }

    public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
    {
        foreach (var rule in Validator.GetClientValidationRules())
        {
            yield return rule;
        }
    }
}
Our CustomDataAnnotationsModelValidatorProvider is not working yet. We need to replace the DataAnnotationsModelValidatorProvider. Put this code in your Global.asax:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
    ReplaceDataAnnotationsModelValidatorProvider();
}

private void ReplaceDataAnnotationsModelValidatorProvider()
{
    var existingProvider = ModelValidatorProviders.Providers
    .Single(x => x is DataAnnotationsModelValidatorProvider);
    ModelValidatorProviders.Providers.Remove(existingProvider);
    ModelValidatorProviders.Providers.Add(new CustomDataAnnotationsModelValidatorProvider());
}

We are almost done. Client validation is missing. To instruct the field shouldn't be validated by a submit button we add an HTML property to it:

...
@using (Html.BeginForm()) {
    @Html.ValidationSummary(false, "Errors found. Please correct them.")
...
    <div class="editor-label">
        @Html.LabelFor(m => m.Description)
    </div>
    <div class="editor-field">
        @Html.TextBoxFor(m => m.Description, new { data_val_dontvalidatesubmitsnamed = "savedraft"})
        @Html.ValidationMessageFor(m => m.Description)
    </div>
...    
    <input type="submit" name="announce" value="Announce!" />
    <input type="submit" name="savedraft" value="Save Draft" />

In this case we don't want Description field to be validated when a submit button with name=savedraft is clicked. You can use as many names as you want. Separate them with spaces.
It's not working yet. We need to insert a java script that reads those attributes and skips client validation.

$(function () {
    $("input[type=submit]").click(function () {
        var inputElement = $(this);
        var formElement = $(inputElement.parents("form")[0]);
        var restore = false;

        //Get all fields those validation should be ignored when this submit element is clicked
        //Remove their validation rules
        $("*[data-val-dontvalidatesubmitsnamed~=" + inputElement.attr("name") + "]").each(function () {
            var rules = $(this).rules("remove");
            $(this).data("rulesBackup", rules);
            $(formElement).find("[data-valmsg-for='" + $(this).attr("name") + "']").addClass("field-validation-valid");
            $(this).removeClass("input-validation-error");
            restore = true;
        });
        //Restore rules after submit
        if (restore) {
            formElement.one('submit', function (eventObject) {
                $("*[data-val-dontvalidatesubmitsnamed~=" + inputElement.attr("name") + "]").each(function () {
                    $(this).rules("add", $(this).data("rulesBackup"));
                });
            });
        }
    });
});


Well. That's all. I hope Microsoft solves that limitation as soon as possible.

1 comment:

  1. This is an open source programming language by which they can provide cost-effective complete programming solutions.

    MVC developer in India

    ReplyDelete