Search code examples
asp.netasp.net-mvcjquery-validateunobtrusive-validation

ASP.NET MVC - prevent submit of invalid form using jQuery unobtrusive validation


I have an ASP.NET project that automatically wires up client side validation using jQuery.Validate and the unobtrusive wrapper built by ASP.NET.

a) I definitely have the appropriate libraries: jquery.js, jquery.validate.js, & jquery.validate.unobtrusive.js

b) And the MVC rendering engine is definitely turned on (ClientValidationEnabled & UnobtrusiveJavaScriptEnabled in the appSettings section of the web.config)

Here's a trivial example where things are broken:

Model:

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

Controller:

public ActionResult Edit()
{
    Person p = new Person();
    return View(p);
}

View:

@model validation.Models.Person

@using (Html.BeginForm()) {
    @Html.ValidationSummary(false)

    @Html.LabelFor(model => model.Name)
    @Html.EditorFor(model => model.Name)
}

This generates the following client side markup:

<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.15.1/jquery.validate.js"></script>
<script type="text/javascript" src="https://ajax.aspnetcdn.com/ajax/mvc/3.0/jquery.validate.unobtrusive.js"></script>
    
<form action="/Person" method="post">
  <div class="validation-summary-valid" data-valmsg-summary="true">
    <ul><li style="display:none"></li></ul>
  </div>
  <label for="Name">Name</label>
  <input data-val="true" data-val-required="The Name field is required." id="Name" name="Name" type="text" value="" />
  <input type="submit" value="Save" />
</form>

When run it will perform the client side validation, noting that some form elements are invalid, but then also post back to the server.

Why is it not preventing postback on a form with an invalid state?


Solution

  • The Problem

    It turns out this happens when you don't include a @Html.ValidationMessageFor placeholder for a given form element.

    Here's a deeper dive into where the problem occurs:

    When a form submits, jquery.validate.js will call the following methods:

    validate: function( options ) {
      form: function() {
        showErrors: function(errors) {
          defaultShowErrors: function() {
            showLabel: function(element, message) {
              this.settings.errorPlacement(label, $(element) )
    

    Where errorPlacement will call this method in jquery.validate.unobtrusive.js:

    function onError(error, inputElement) { 
       var container = $(this).find("[data-valmsg-for='" + escapeAttributeValue(inputElement[0].name) + "']"),
           replace = $.parseJSON(container.attr("data-valmsg-replace")) !== false;
    

    When we don't add a placeholder for the validation message, $(this).find(...) won't find anything.

    Meaning container.attr("data-valmsg-replace") will return undefined

    This poses a problem is when we try to call $.parseJSON on an undefined value. If an error is thrown (and not caught), JavaScript will stop dead in its tracks and never reach the final line of code in the original method (return false) which prevents the form from submitting.

    The Solution

    Upgrade jQuery Validate Unobtrusive

    Newer versions of jQuery Validate handle this better and check for nulls before passing them to $.parseJSON

    function onError(error, inputElement) {  // 'this' is the form element
        var container = $(this).find("[data-valmsg-for='" + escapeAttributeValue(inputElement[0].name) + "']"),
            replaceAttrValue = container.attr("data-valmsg-replace"),
            replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) !== false : null;
    

    Add ValidationMessageFor

    To address the core problem, for every input on your form, make sure to include:

    @Html.ValidationMessageFor(model => model.Name)
    

    Which will render the following client side markup

    <span class="field-validation-valid" data-valmsg-for="Name" data-valmsg-replace="true"></span>
    

    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.js"></script>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.15.1/jquery.validate.js"></script>
    <script type="text/javascript" src="https://ajax.aspnetcdn.com/ajax/mvc/3.0/jquery.validate.unobtrusive.js"></script>
        
    <form action="/Person" method="post">
      <div class="validation-summary-valid" data-valmsg-summary="true">
        <ul><li style="display:none"></li></ul>
      </div>
      <label for="Name">Name</label>
      <input data-val="true" data-val-required="The Name field is required." id="Name" name="Name" type="text" value="" />
      <span class="field-validation-valid" data-valmsg-for="Name" data-valmsg-replace="true"></span>
      <input type="submit" value="Save" />
    </form>