Thursday 30 October 2014

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 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:
3132
[Required(ErrorMessage = "A title is required.")]
public string Title { get; set; }
view rawAuction.cs hosted with ❤ by GitHub

There are many different attributes and validation expressions  available, including regular expressions. Some examples:
12131415161718192021222324252627282930313233343536373839404142
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 hosted with ❤ by GitHub

In the Cloud Auction project we are using the Twitter Bootstrap framework and several componentsthat 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 so they look consistent with Bootstrap out of the 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:
21222324252627282930
<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:
1
@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:
123456789
<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>

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:
12345678
$.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:
7576777879808182838485
[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);
}

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:
1234567
<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>

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.
22232425262728
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;
}

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 field on the form.
This concept becomes even more powerful when used with more 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.
123456789101112
@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 found good controls, set up the javascript and created EditorTemplates, using them becomes extremely simple and the result look quite nice.
image 

No comments:

Post a Comment

Angular Tutorial (Update to Angular 7)

As Angular 7 has just been released a few days ago. This tutorial is updated to show you how to create an Angular 7 project and the new fe...