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:

<!-- language: lang-cs -->
public class Person
{
    [Required]
    public string Name { get; set; }
}

Controller:

<!-- language: lang-cs -->
public ActionResult Edit()
{
    Person p = new Person();
    return View(p);
}

View:

<!-- language: lang-cs -->
@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:

<!-- begin snippet: js hide: false console: true babel: false --> <!-- language: lang-html -->
<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>
<!-- end snippet -->

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?

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:

<!-- language: lang-js -->
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:

<!-- language: lang-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

<!-- language: lang-js -->
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:

<!-- language: lang-cs -->
@Html.ValidationMessageFor(model => model.Name)

Which will render the following client side markup

<!-- language: lang-html -->
<span class="field-validation-valid" data-valmsg-for="Name" data-valmsg-replace="true"></span>
<!-- begin snippet: js hide: false console: true babel: false --> <!-- language: lang-html -->
<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>
<!-- end snippet -->