Author : MD TAREQ HASSAN | Updated : 2020/06/23
Model binding Github repository: https://github.com/hovermind/AspNetCoreModelBinding
Validation Types
- Client side validation:
- JS validates form data before sending it to server
- Microsoft provides unobstrusive JavaScript for client side validation
- Server side validation:
- Annotations are used for Model Validation
- If incoming data does not conform to the contraints (Data Annotations), model state becomes invalid (MVC WebApp Controller)
- ASP.Net core (API Controller) sends http 400 (bad request) automatically
Client side validation
- ASP.NET core includes unobtrusive client-side validation libraries, which makes it easier to add client side validation code, without writing a single line of code
- Details: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation#client-side-validation
- How Unobtrusive Validation work?
- model properties have attributes/annotations
- validation related tag helpers generates HTML5
data-*
attributes based on model property attributes/annotations - Unobtrusive Validation script targets those HTML5
data-*
attributes and performs actions when present (i.e. show error message, coloring error message etc.)
_ValidationScriptsPartial.cshtml
includes validation related scripts
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
The jQuery Unobtrusive Validation script is a custom Microsoft front-end library that builds on
the popular jQuery Validate plugin. Without jQuery Unobtrusive Validation,
you would have to code the same validation logic in two places:
once in the server side validation attributes on model properties, and then again in client side scripts
(the examples for jQuery Validate’s validate()
method shows how complex this could become).
Instead, MVC’s Tag Helpers and HTML helpers are able to use the validation attributes and type metadata from model properties to render HTML 5 data- attributes in the form elements that need validation. MVC generates the data- attributes for both built-in and custom attributes. jQuery Unobtrusive Validation then parses the data- attributes and passes the logic to jQuery Validate, effectively “copying” the server side validation logic to the client. You can display validation errors on the client using the relevant tag helpers
Validation Message Tag Helper
There are two Validation Tag Helpers
asp-validation-for
: Validation Message Tag Helperasp-validation-summary
: Validation Summary Tag HelperValidationSummary.All
ValidationSummary.ModelOnly
ValidationSummary.None
<span asp-validation-for="Email"></span>
<div asp-validation-summary="ModelOnly"></div>
asp-validation-for
<h1>Create Hover Info</h1>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<!-- ... ... ... -->
<div class="form-group">
<label asp-for="Id" class="control-label"></label>
<input asp-for="Id" class="form-control" />
<span asp-validation-for="Id" class="text-danger"></span>
</div>
<!-- ... ... ... -->
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
sp-validation-summary
<h1>Create Hover Info</h1>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<!-- ... ... ... -->
<div class="form-group">
<label asp-for="Id" class="control-label"></label>
<input asp-for="Id" class="form-control" />
<span asp-validation-for="Id" class="text-danger"></span>
</div>
<!-- ... ... ... -->
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<!-- ... ... ... -->
Showing validation error
- Add annotations to model properties
- Check model state in Controller Action
- Add
_ValidationScriptsPartial
script to the view
HoverInputModel.cs
public class HoverInputModel
{
[Required]
[Display(Name = "First Name")]
[Range(5, 50)]
public int FirstName { get; set; }
[Required]
[Display(Name = "Last Name")]
[StringLength(50, MinimumLength = 5, ErrorMessage = "Last name must have at least 5 characters")]
public int LastName { get; set; }
[Required(ErrorMessage = "{0} is required")]
[StringLength(100, MinimumLength = 10, ErrorMessage = "Name must have at least 10 characters")]
[RegularExpression(@"^[a-zA-Z\s]+$", ErrorMessage = "Please, use letters in the name. Digits are not allowed.")]
[Display(Name = "Full Name")]
public string FullName { get; set; }
[Required(ErrorMessage = "{0} is required")]
[StringLength(100, MinimumLength = 1, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.")]
public string Email { get; set; }
// will be null
public string Bar { get; set; }
}
HoverController.cs
public class HoverController : Controller
{
// ... ... ...
// GET: Hover/Create
public IActionResult Create()
{
return View();
}
// POST: Hover/Create
// To protect from overposting attacks, enable the specific properties you want to bind to, for
// more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([FromFrom]HoverInputModel hoverInputModel)
{
if (ModelState.IsValid)
{
// perform EF core task here i.e. _context.Add(hoverInputModel);await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(hoverInputModel);
}
// ... ... ...
}
Create.cshtml
@model ModelBindingAndFormValidation.Models.HoverInputModel
@{
ViewData["Title"] = "Create";
}
<h1>Create Hover Info</h1>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Id" class="control-label"></label>
<input asp-for="Id" class="form-control" />
<span asp-validation-for="Id" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="FirstName" class="control-label"></label>
<input asp-for="FirstName" class="form-control" />
<span asp-validation-for="FirstName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="LastName" class="control-label"></label>
<input asp-for="LastName" class="form-control" />
<span asp-validation-for="LastName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="FullName" class="control-label"></label>
<input asp-for="FullName" class="form-control" />
<span asp-validation-for="FullName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Email" class="control-label"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
When form is posted to server, depending on annotations applied to input model, error messages will be shown.
Scaffolding view with client side validation
Create input model and add annotations to properties
- Right click on Models
- Add > Class (
FooInputModel.cs
)
Models/FooInputModel.cs
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
[Bind(nameof(FirstName), nameof(LastName), nameof(FullName), nameof(Email))]
public class FooInputModel
{
[Required]
[Display(Name = "First Name")]
[StringLength(50, MinimumLength = 5)]
public string FirstName { get; set; }
[Required]
[Display(Name = "Last Name")]
[StringLength(50, MinimumLength = 5, ErrorMessage = "{0} must have at least {2} characters")]
public string LastName { get; set; }
[Required(ErrorMessage = "{0} is required")]
[StringLength(100, MinimumLength = 10, ErrorMessage = "{0} must have at least {2} characters")]
[RegularExpression(@"^[a-zA-Z\s]+$", ErrorMessage = "Please, use letters in the name. Digits are not allowed.")]
[Display(Name = "Full Name")]
public string FullName { get; set; }
[Required(ErrorMessage = "{0} is required")]
[StringLength(100, MinimumLength = 1, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.")]
public string Email { get; set; }
// ignored (value will be null after model binding)
public string Bar { get; set; }
}
Scaffold controller with read/write actions
- Right click on Controllers folder > Add
- New Scaffolded Item… > MVC Controller with read/write action > Add
- Set controller class name:
FooController
> Add - For simplicity we are gonna keep index & create (GET & POST) actions and remove rest
Controllers/FooController.cs
public class FooController : Controller
{
// GET: FooController
public ActionResult Index()
{
return View();
}
// GET: FooController/Create
public ActionResult Create()
{
return View();
}
// POST: FooController/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(FooInputModel fooInputModel)
{
if (ModelState.IsValid)
{
// perform EF core task here i.e. _context.Add(hoverInputModel);await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(fooInputModel);
}
}
Scaffold view based on input model
- Create a folder in Views according to MVC convention
- MVC convention: if controller class name is
XxxController
then folder should beViews/Xxx
- we created
FooController
, so folder should beViews/Foo
- Right click on Views > Add > New folder > folder name: Foo
- MVC convention: if controller class name is
- Scaffolding view
- Build the project to make sure there is no error otherwise scaffolding would not work
- Right click on Foo folder > Add
- New Scaffolded Item.. > Razor View > Add
- View name: Create
- Template: Create
- Model class:
FooInputModel
- Options:
- check “reference script libraries”
- check “use layout page”
- input field below “use layout page”
- leave empty since
_viewstart
will insert layout - to use custom layout: click ellipses button on rigt and set that custom layout
- leave empty since
- Add
Views/Foo/Create.cshtml
@model ModelBindingAndFormValidation.Models.FooInputModel
@{
ViewData["Title"] = "Create";
}
<h1>Create</h1>
<h4>FooInputModel</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Id" class="control-label"></label>
<input asp-for="Id" class="form-control" />
<span asp-validation-for="Id" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="FirstName" class="control-label"></label>
<input asp-for="FirstName" class="form-control" />
<span asp-validation-for="FirstName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="LastName" class="control-label"></label>
<input asp-for="LastName" class="form-control" />
<span asp-validation-for="LastName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="FullName" class="control-label"></label>
<input asp-for="FullName" class="form-control" />
<span asp-validation-for="FullName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Email" class="control-label"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Bar" class="control-label"></label>
<input asp-for="Bar" class="form-control" />
<span asp-validation-for="Bar" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Startup.Configure()
- chnage default route pattern so that our
foo/create
is shown when application starts - app.UseEndpoints(…) =>
pattern: "{controller=Foo}/{action=Create}")
Add Index view
- Right click
Views/Foo
> Add > View.. - Razor view > Add
- View name: Index
- Template: Empty
- Add
Now run the application
- if form data is valid it will go to Index page
- if form data is not valid, errors will be shown