LyraLearn AI Learning Platform
Exams
← Module 3 Β· Razor, jQuery and Bootstrap
🎧 Listen

Forms and Validation End-to-End

This lesson assembles the last three modules into the single flow you'll build hundreds of times: a form that validates on the client, re-validates on the server, submits via AJAX, and displays errors properly. One rule governs the whole design: never trust the client. Client-side validation is UX; server-side validation is security. Anyone with dev tools open can delete your JavaScript, edit your data-val-* attributes, or POST straight to the endpoint with curl.

The model defines the rules β€” once

Start with the ViewModel; the annotations are the single source of truth for both sides:

public class PermitApplicationViewModel
{
    [Required, StringLength(100)]
    public string ApplicantName { get; set; }
    [Required, Range(1, 5000), Display(Name = "Site area (mΒ²)")]
    public int SiteArea { get; set; }
}

The view renders with tag helpers (<label asp-for>, <input asp-for>, <span asp-validation-for>, plus <div asp-validation-summary="ModelOnly"> for model-level errors) inside a <form asp-action="Apply"> β€” and the form tag helper emits the hidden anti-forgery CSRF field automatically. Because the helpers read the annotations, the generated inputs carry data-val-required, data-val-length-max, and friends β€” the client rules are derived from the server rules, never written twice.

Server side: the only validation that counts

The POST action is Module 2's canonical shape: [HttpPost, ValidateAntiForgeryToken], check ModelState.IsValid, redisplay on failure, PRG on success. Rules annotations can't express β€” "permit number must be unique", "end date after start date" β€” run in the action or service and are reported the same way: ModelState.AddModelError("PermitNumber", "Already in use."), keyed to the field name so the message lands next to the right input (empty key β†’ validation summary). This is why redisplaying View(model) "just works": helpers read ModelState and re-render the user's values plus every error.

Client side: the same rules, for free β€” then AJAX

With jquery.validate + the unobtrusive adapter loaded (Lesson 2), the browser enforces those data-val-* rules before any request is sent β€” instant feedback, and Bootstrap error styling from Lesson 3 lights up the failing fields. That's the whole client story for a plain submit.

For an AJAX submit, intercept and check validity first:

$('#permit-form').on('submit', function (e) {
    e.preventDefault();
    if (!$(this).valid()) return;
    $.post(this.action, $(this).serialize(), handleResponse).fail(showError);
});

serialize() includes the anti-forgery hidden field, so [ValidateAntiForgeryToken] stays satisfied. The controller returns JSON { success: true, redirectUrl: ... } on success; on validation failure the cleanest pattern is returning the form as a partial view with ModelState errors rendered, replacing the form's HTML, and re-running $.validator.unobtrusive.parse() on it.

The checklist

Every form you ship: annotations on the ViewModel β€’ asp-for tag helpers so names and data-val-* line up β€’ anti-forgery token + attribute β€’ ModelState.IsValid checked server-side, always β€’ errors keyed to fields β€’ PRG on success, redisplay on failure. When validation "doesn't work," diff your form against this list β€” the culprit is on it.

🧠 Quiz yourself on this lesson →

Ask the AI Tutor

Grounded in the course lessons β€” it cites its sources and says when it doesn't know.