• Home
  • Inspiratie
  • Blogs
  • Asp-net-mvc4-form-validation-with-bootstrap-on-client-and-server-using-a-single-ruleset

ASP.NET MVC4 Form Validation with Bootstrap on client and server using a single ruleset

[Update 29-9-2013: We published a NuGet package Bootstrap.MVC.EditorTemplates containing the controls and validation setup as rel="noopener noreferrer" described in this post. Read more in this follow-up post.]

ASP.NET MVC provides built-in support for data validation. The nice thing about it is that this validation can happen both server-side and client-side, and those can be managed from one single place in code. Validation rules are defined by setting attributes on the fields of a Model class, like this:


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;
using SqlFu;

namespace Auction.Web.Domain.Models
{
[Table("Auction", PrimaryKey = "Id"/*default*/, AutoGenerated = true /*default*/)]
public class Auction : Entity<int>
{
public Auction(Auction auction) : base(auction.Id)
{
this.Title = auction.Title;
this.Start = auction.Start;
this.OpeningTime = auction.OpeningTime;
this.ClosingTime = auction.ClosingTime;
this.OpenOnSaturdays = auction.OpenOnSaturdays;
this.OpenOnSundays = auction.OpenOnSundays;
this.End = auction.End;
this.Type = auction.Type;
this.State = auction.State;
}
public Auction()
{
}

[Required(ErrorMessage = "A title is required.")]
public string Title { get; set; }

[Required]
public DateTime Start { get; set; }
public DateTime? End { get; set; }

[Display(Name = "Dagelijkse openingstijd")]
[Required(ErrorMessage = "Dit is een verplicht veld. Vul 00:00 in als de veiling dag en nacht doorgaat")]
[DataType(DataType.Time)]
[DisplayFormat(DataFormatString = "{0:hh\\:mm}", ApplyFormatInEditMode = true)]
public TimeSpan OpeningTime { get; set; }

[Display(Name = "Dagelijkse sluitingstijd")]
[Required(ErrorMessage = "Dit is een verplicht veld. Vul 00:00 in als de veiling dag en nacht doorgaat")]
[DataType(DataType.Time)]
[DisplayFormat(DataFormatString = "{0:hh\\:mm}", ApplyFormatInEditMode = true)]
public TimeSpan ClosingTime { get; set; }

[Display(Name = "Open op zaterdag")]
public bool OpenOnSaturdays { get; set; }

[Display(Name = "Open op zondag")]
public bool OpenOnSundays { get; set; }

[Required]
public AuctionState State { get; set; }

[Required]
public AuctionType Type { get; set; }
}


public enum AuctionState : int
{
[Description("Inactive")]
Inactive = 0,
[Description("Planned")]
Planned = 1,
[Description("Running")]
Running = 2,
[Description("Stopped")]
Stopped = 3,
[Description("Finished")]
Finished = 4
}


public enum AuctionType : int
{
[Description("Fire Sale")]
FireSale = 0,
[Description("Exit Auction")]
ExitAuction = 1
}
}
view rawAuction.cs hosted with ❤ by GitHub


There are many different attributes and validation expressions available, including regular expressions. Some examples:


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Web.Security;

namespace Auction.Web.Models
{
using System.Web.Mvc;

public class RegisterModel
{
[Key]
[ReadOnly(true)]
public Guid Id { get; set; }

[ReadOnly(true)]
[Display(Name = "Identity Provider")]
public string IdentityProvider;

[ReadOnly(true)]
[Display(Name = "Name Identifier")]
public string NameIdentifier { get; set; }

[Required]
[Display(Name = "Name")]
public string Name { get; set; }

[Required]
[DataType(DataType.EmailAddress)]
[Display(Name = "Email Address")]
[RegularExpression("^([a-zA-Z0-9_\\-\\.]+)@[a-z0-9-]+(\\.[a-z0-9-]+)*(\\.[a-z]{2,3})$", ErrorMessage = "Email is not a valid e-mail address.")]
public string Email { get; set; }

[Required]
[Display(Name = "Allow cookies")]
public bool AllowCookies { get; set; }

[ReadOnly(true)]
public IEnumerable<ClaimModel> Claims { get; set; }
}
}
view rawRegisterModel.cs rel="noopener noreferrer" hosted with ❤ by GitHub


In the Cloud Auction project we are using the rel="noopener rel="noopener noreferrer" noreferrer" Twitter rel="noopener noreferrer" Bootstrap framework and several components that come with it. We also use a few additional components that are not part of the standard Bootstrap component set, such as a datepicker and timepicker control. There is a rich ecosystem for additional Bootstrap components and many existent jQuery components have been modified rel="noopener noreferrer" so they look consistent with Bootstrap out of the rel="noopener noreferrer" box, without adding additional styling.

Reusable controls in MVC can be created as a Partial View. A special kind of Partial Views are DisplayTemplates and EditorTemplates. These templates can be applied automatically every time you want to display (or edit) a specific data type or Model, using the Html.DisplayFor and Html.EditorFor helpers. Templates are defined as normal Views that are placed in a subfolder “DisplayTemplates” and “EditorTemplates” under any View folder, (such as Views\Shared or a specific folder where you want to use these templates). MVC has built-in templates for standard data types such as strings, but it is possible to override these and add templates for custom types. By default, MVC uses the type name to search for a matching Editor/DisplayTemplate.

For instance, we use the following template for a string:


@using Auction.Web.Utility
@model object

@{
var htmlAttributes = new RouteValueDictionary();
if (ViewBag.@class != null)
{
htmlAttributes.Add("class", ViewBag.@class);
}
if (ViewBag.@type != null)
{
htmlAttributes.Add("type", ViewBag.@type);
}
if (ViewBag.placeholder != null)
{
htmlAttributes.Add("placeholder", ViewBag.placeholder);
}
}


<div class="control-group@(Html.ValidationErrorFor(m => m, " error"))">
@Html.LabelFor(m => m, new { @class = "control-label" })
<div class="controls">
@Html.TextBox(
"",
ViewData.TemplateInfo.FormattedModelValue,
htmlAttributes)
@Html.ValidationMessageFor(m => m, null, new { @class = "help-inline" })
</div>
</div>
view rawString.cshtml hosted with ❤ by GitHub


Now, to get an edit box for a string on a page, I can do that in just one line in the view:


@Html.EditorFor(model => model.Title)
view rawSomeView.cshtml hosted with ❤ by GitHub


And it will render like this, complete with label, with a nice Bootstrap design:

image

Because the Title field has an attribute “required”, there should be an error when we leave it empty. And indeed:

image

This error message appears immediately when we clear the input field. How does this work? Look at the DOM that was generated by the control:


<div class="control-group error">
<label class="control-label" for="Title">Title</label>
<div class="controls">
<input class="input-block-level" data-val="true" data-val-required="Dit is een verplicht veld." id="Title" name="Title" type="text" value="Test Veiling">
<span class="help-inline field-validation-error" data-valmsg-for="Title" data-valmsg-replace="true">
<span for="Title" generated="true" class="" style="">Dit is een verplicht veld.</span>
</span>
</div>
</div>
view rawresult-client-error.html hosted with ❤ by GitHub


The Html.TextBox() has added a “data-val-required” attribute to the input box. The Html.ValidationMessageFor() has added the error message that was declared in a [Required] attribute on the Title field in the Auction Domain Model.

All data-val-* attributes are picked up by two javascript files that we included on this page (using a bundle):

jquery.validate.js
jquery.validate.unobtrusive.js
site/validation.js

The site validation.js script initializes jquery validation to work in a bootstrap based forms design:


$.validator.setDefaults({
highlight: function (element) {
$(element).closest(".control-group").addClass("error");
},
unhighlight: function (element) {
$(element).closest(".control-group").removeClass("error");
}
});
view rawvalidation.js hosted with ❤ by GitHub


These jquery plugins will recognize the attributes and do client-side evaluation of the validation rules. If an error is detected, they will call our highlight function that sets an “error” attribute on the containing <div class=”control-group”> and place the error message inside the <span> element. The bootstrap styling will react to the error class by highlighting all the elements in the control group in red. Also the submit of data will be blocked as long as there are errors.

So our client-side validation works. That very user friendly, but of course we can never trust this for our business logic on the server. A malicious user might disable or bypass javascript validation and can post anything to the server. Before we accept any values posted by a user, we have to do server-side validation. The Controller receiving the data is here:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using Auction.Web.Areas.Seller.Models;
using Auction.Web.Domain;
using Auction.Web.Domain.Commands;
using Auction.Web.Domain.Models;
using Auction.Web.Domain.Queries;
using Auction.Web.Security;

namespace Auction.Web.Areas.Seller.Controllers
{
[RequireHttps(Order = 1)]
[Authorize(Roles = "Seller", Order=2)]
public class AuctionsController : Auction.Web.Controllers.BaseController
{
//
// GET: /Seller/Auction/
public ActionResult Index()
{
var auctions = Query(new GetAllAuctions());
return View(auctions.ToList());
}

//
// GET: /Seller/Auction/Details/5
public ActionResult Details(int id = 0)
{
var auction = Query(new GetSingleAuction(id));
if (auction == null)
{
return HttpNotFound();
}
return View(auction);
}

//
// GET: /Seller/Auction/Create
public ActionResult Create()
{
return View(new AuctionViewModel());
}

//
// POST: /Seller/Auction/Create
[HttpPost]
public ActionResult Create(AuctionViewModel model)
{
if (ModelState.IsValid)
{
Domain.Models.Auction auction = new Domain.Models.Auction(model);
ExecuteCommand(new InsertNewAuction(auction));
return RedirectToAction("Index");
}

return View(model);
}

//
// GET: /Seller/Auction/Edit/5
public ActionResult Edit(int id = 0)
{
Domain.Models.Auction auction = Query(new GetSingleAuction(id));
AuctionViewModel model = new AuctionViewModel(auction);
if (auction == null)
{
return HttpNotFound();
}
return View(model);
}

//
// POST: /Seller/Auction/Edit/5
[HttpPost]
public ActionResult Edit(AuctionViewModel model)
{
if (ModelState.IsValid)
{
Domain.Models.Auction auction = new Domain.Models.Auction(model);
ExecuteCommand(new UpdateAuction(auction));
return RedirectToAction("Index");
}
return View(model);
}

//
// GET: /Seller/Auction/Delete/5
public ActionResult Delete(int id = 0)
{
Domain.Models.Auction auction = Query(new GetSingleAuction(id));
if (auction == null)
{
return HttpNotFound();
}
return View(auction);
}

//
// POST: /Seller/Auction/Delete/5
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id)
{
ExecuteCommand(new DeleteAuction(id));
return RedirectToAction("Index");
}
}
}
view rawAuctionsController.cs hosted with ❤ by GitHub


Validation is checked with the ModelState.IsValid property. The MVC framework automatically applies the validation rules, and sets this property to false if it finds invalid data. To test this out, we can disable javascript and post an empty value for the Title field. Now we see a page refresh happening, the same page returns with an error message:

image

This looks exactly the same as before, when we created a client-side validation error. Internally however, something else has happened because now the server has rendered the error. The rendered markup is almost, but not exactly the same:


<div class="control-group error">
<label class="control-label" for="Title">Title</label>
<div class="controls">
<input class="input-validation-error input-block-level" data-val="true" data-val-required="Dit is een verplicht veld." id="Title" name="Title" type="text" value="">
<span class="field-validation-error help-inline" data-valmsg-for="Title" data-valmsg-replace="true">Dit is een verplicht veld.</span>
</div>
</div>
view rawresult-server-error.html hosted with ❤ by GitHub


The ValidationErrorFor is just a small Helper function that is used to additionally set the class=”error” on the control-group (just like the client-side validation does) so that bootstrap styling will highlight the whole control by making it red.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Web;
using System.Web.Mvc;

namespace Auction.Web.Utility
{
public static class Validation
{
/// <summary>
/// Checks the ModelState for an error, and returns the given error string if there is one, or null if there is no error
/// Used to set class="error" on elements to present the error to the user
/// </summary>
/// <typeparam name="TModel"></typeparam>
/// <typeparam name="TProperty"></typeparam>
/// <param name="htmlHelper"></param>
/// <param name="expression"></param>
/// <param name="error"></param>
/// <returns></returns>
public static MvcHtmlString ValidationErrorFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string error)
{
if (HasError(htmlHelper, ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData),ExpressionHelper.GetExpressionText(expression)))
return new MvcHtmlString(error);
else
return null;
}


private static bool HasError(this HtmlHelper htmlHelper, ModelMetadata modelMetadata, string expression)
{
string modelName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(expression);
FormContext formContext = htmlHelper.ViewContext.FormContext;
if (formContext == null)
return false;

if (!htmlHelper.ViewData.ModelState.ContainsKey(modelName))
return false;

ModelState modelState = htmlHelper.ViewData.ModelState[modelName];
if (modelState == null)
return false;

ModelErrorCollection modelErrors = modelState.Errors;
if (modelErrors == null)
return false;

return (modelErrors.Count > 0);
}
}
}
view rawValidationErrorFor.cs hosted with ❤ by GitHub


The nice thing about all this is that now both client-side and server-side validation rules are set declaratively, using attributes on the Model. The MVC framework, together with the jquery validation javascript plugin are able to validate the Model data both client-side and server-side. By creating EditorTemplates for all the data types that we use, we can create reusable and good-looking forms using one-liners for every data rel="noopener noreferrer" field on the form.

This concept becomes even more powerful when used with more rel="noopener noreferrer" complex data types. For instance, for Date fields, we use a datepicker control and for TimeSpan fields a timepicker. When I looked for a datepicker and timepicker components for Boostrap, I found many. The biggest problem is finding the best one. Here is an EditorTemplate for a combined Date and Time field, for which I created a special class that takes one DateTime value and maps that to a separate Date and Time property, that can easily be mapped to two html controls in a single EditorTemplate. Click the gist link to also see the way the ViewModel is subclasses from the Domain Model.


@model Auction.Web.Areas.Seller.Models.DateAndTime

<div class="control-group@(Html.ValidationErrorFor(m => m, " error"))">
@Html.LabelFor(m => m, new { @class = "control-label" })
<div class="controls">
@Html.TextBoxFor(m => m.Date, new { @class="datepicker", data_date=Model.Date, data_date_format=System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern.Replace("M", "m") })
@Html.TextBoxFor(m => m.Time, new { @class="timepicker", data_provider="timepicker" } )

@Html.ValidationMessageFor(m => m.Date, null, new { @class="help-inline" })
@Html.ValidationMessageFor(m => m.Time, null, new { @class="help-inline" })
</div>
</div>
view rawDateAndTime.cshtml hosted with ❤ by GitHub


Once you have rel="noopener noreferrer" found good controls, set up the javascript and created EditorTemplates, using them becomes extremely simple and the result look quite nice.

image

[This post is part of a series: Development of a mobile website with apps and social features]