ASP.NET MVC · Forms & Validation

Client-Side Validation

Client-side validation shows errors the moment a user leaves a field, before the form is even sent. In MVC 5 it costs you three script tags — the rules come from your Data Annotations.

Intermediate 6 min Lesson 16 of 37Covers ASP.NET MVC 5.2 on .NET Framework 4.8.
Before you start:Data Annotations
0/37 in ASP.NET MVC
On this page · 6 min · Intermediate

Learning objectives#

  • Enable unobtrusive client-side validation
  • Read the data-val attributes MVC generates
  • Show messages with ValidationMessageFor and ValidationSummary
  • Know why the server must still validate

Explanation#

Two app settings switch it on (they are true in new projects): ClientValidationEnabled and UnobtrusiveJavaScriptEnabled in Web.config.

Strongly typed helpers such as Html.TextBoxFor read the Data Annotations and emit data-val-* attributes: data-val-required="…", data-val-length-max="20", data-val-range-min="13".

jquery.validate.js and jquery.validate.unobtrusive.js (plus jQuery) read those attributes and validate as the user types and submits. Load them in the scripts section of forms that need them, usually through the ~/bundles/jqueryval bundle.

Html.ValidationMessageFor(m => m.Email) renders the span where a field’s message appears; Html.ValidationSummary() lists all errors at the top.

Client validation is only a convenience. Anyone can disable JavaScript or send a request with Postman, so the POST action must still check ModelState.IsValid.

Syntax#

Razor / Markup Illustrative
@Html.LabelFor(m => m.Email)
@Html.TextBoxFor(m => m.Email)
@Html.ValidationMessageFor(m => m.Email)


@section scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Code example: The HTML that powers client-side validation#

C#Subscribe.cshtml Illustrative
// Models/SubscribeViewModel.cs
using System.ComponentModel.DataAnnotations;


namespace ShopSite.Models
{
    public class SubscribeViewModel
    {
        [Required(ErrorMessage = "Email is required")]
        [EmailAddress(ErrorMessage = "Enter a valid email")]
        public string Email { get; set; }


        [Range(13, 120, ErrorMessage = "Age must be 13 or over")]
        public int Age { get; set; }
    }
}


// Controllers/NewsletterController.cs
using System.Web.Mvc;
using ShopSite.Models;


namespace ShopSite.Controllers
{
    public class NewsletterController : Controller
    {
        public ActionResult Subscribe() => View(new SubscribeViewModel { Age = 18 });
    }
}


@* Views/Newsletter/Subscribe.cshtml *@
@model SubscribeViewModel
@using (Html.BeginForm())
{
    @Html.LabelFor(m => m.Email)
    @Html.TextBoxFor(m => m.Email)
    @Html.ValidationMessageFor(m => m.Email)


    @Html.LabelFor(m => m.Age)
    @Html.TextBoxFor(m => m.Age)
    @Html.ValidationMessageFor(m => m.Age)


    <button type="submit">Subscribe</button>
}
@section scripts {
    <script src="~/Scripts/jquery-3.7.1.min.js"></script>
    <script src="~/Scripts/jquery.validate.min.js"></script>
    <script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
}
Expected output
GET /Newsletter/Subscribe
200 OK
<form action="/Newsletter/Subscribe" method="post"><label for="Email">Email</label><input data-val="true" data-val-email="Enter a valid email" data-val-required="Email is required" id="Email" name="Email" type="text" value="" /><span class="field-validation-valid" data-valmsg-for="Email" data-valmsg-replace="true"></span><label for="Age">Age</label><input data-val="true" data-val-number="The field Age must be a number." data-val-range="Age must be 13 or over" data-val-range-max="120" data-val-range-min="13" data-val-required="The Age field is required." id="Age" name="Age" type="text" value="18" /><span class="field-validation-valid" data-valmsg-for="Age" data-valmsg-replace="true"></span>    <button type="submit">Subscribe</button>
</form>
    <script src="/Scripts/jquery-3.7.1.min.js"></script>
    <script src="/Scripts/jquery.validate.min.js"></script>
    <script src="/Scripts/jquery.validate.unobtrusive.min.js"></script>

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. TextBoxFor for Email adds data-val="true" plus one data-val-* attribute per rule: required and email, each with its message.
  2. Age is an int, so MVC adds data-val-number (must be a number) and data-val-required automatically, plus the range rule with min and max.
  3. ValidationMessageFor renders an empty span with data-valmsg-for; the unobtrusive script fills it with the message.
  4. The ~/ in the script src is resolved to the app’s root, so it also works when the site runs in a virtual directory.
  5. When the user leaves the Email box empty and tabs away, jQuery Validate shows “Email is required” without a round trip.

Real-world use case#

A checkout form used to post, wait two seconds and come back with “Phone is required”. Turning on unobtrusive validation shows the message instantly as the user leaves the field, and abandoned checkouts drop noticeably.

Common mistakes#

Scripts in the wrong order

jquery.validate.unobtrusive needs jquery.validate, which needs jQuery. Loading them out of order gives “$.validator is undefined” and no validation.

Writing inputs by hand

<input name="Email"> has no data-val attributes, so the field is never validated in the browser. Use the For helpers.

Dynamic forms not parsed

Forms loaded with AJAX after the page loads are not validated until you call $.validator.unobtrusive.parse(form).

Best practices#

  • Load the validation bundle only on pages with forms
  • Always validate again on the server
  • Use ValidationSummary(true) for model-level errors only
  • Re-parse forms that are inserted with AJAX

Remember this#

Data Annotations → data-val-* → jQuery Validate.

Three scripts, in order: jQuery, validate, unobtrusive.

Client checks are for comfort; server checks are for safety.

Practice exercise#

Your task: Render a login form for a LoginViewModel with [Required] UserName and Password and show both validation message spans and a validation summary.

Need a hint?
Use Html.ValidationSummary(true) plus ValidationMessageFor for each field and Html.PasswordFor for the password.
Show solution
C# Illustrative
// Models/LoginViewModel.cs
using System.ComponentModel.DataAnnotations;


namespace ShopSite.Models
{
    public class LoginViewModel
    {
        [Required] public string UserName { get; set; }
        [Required, DataType(DataType.Password)] public string Password { get; set; }
    }
}


// Controllers/LoginController.cs
using System.Web.Mvc;
using ShopSite.Models;


namespace ShopSite.Controllers
{
    public class LoginController : Controller
    {
        public ActionResult Index() => View(new LoginViewModel());
    }
}


@* Views/Login/Index.cshtml *@
@model LoginViewModel
@using (Html.BeginForm())
{
    @Html.ValidationSummary(true)
    @Html.TextBoxFor(m => m.UserName)
    @Html.ValidationMessageFor(m => m.UserName)
    @Html.PasswordFor(m => m.Password)
    @Html.ValidationMessageFor(m => m.Password)
}
Expected output
GET /Login
200 OK
<form action="/Login" method="post"><input data-val="true" data-val-required="The UserName field is required." id="UserName" name="UserName" type="text" value="" /><span class="field-validation-valid" data-valmsg-for="UserName" data-valmsg-replace="true"></span><input data-val="true" data-val-required="The Password field is required." id="Password" name="Password" type="password" /><span class="field-validation-valid" data-valmsg-for="Password" data-valmsg-replace="true"></span></form>

Mini quiz#

Client-Side Validation quizMultiple choice Question 1 of 3

Where do the data-val-* attributes come from?

Score so far: 0/0

Want more? Practise all ASP.NET MVC MCQs · ASP.NET MVC interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.