This blog post will show you how to add server-side validation to an MVC data service whose CRUD operations are used by a Kendo UI Grid. While the server-side portions of the code will show how to add validation using the Kendo UI MVC Extensions, the basic premise of the client-side code can be applied to any existing Kendo UI project, without needing to use the Kendo UI MVC Extensions. This blog post shows how to handle server-side validation errors using a Kendo Grid that's using the PopUp editor.
The code samples in this post are from the Kendo UI SalesHub demo application. You can view the source code for SalesHub on GitHub.
Having client-side validation is great from a user's perspective. It provides rapid feedback on whether the data they've entered is valid or not. Unfortunately, client-side validation shouldn't be trusted from a data integrity standpoint. Anyone can open up their browser's
Server-side validation is out of the reach of normal users resulting in fewer opportunities for tampering. Since the server has access to all of the information that it needs it can perform more complex validation.
The first step toward having server-side validation is to add validation attributes to properties on your view models. These validation attributes are a part
Let's take a look at the OrderViewModel, which uses some of these attributes.
public class OrderDetailViewModel { [HiddenInput(DisplayValue = false)] public int OrderDetailId { get; set; } public string Origin { get; set; } [DisplayName("Net Wt")] [Range(0, 99999999.0)] public decimal NetWeight { get; set; } [DisplayName("Unit Weight")] [Range(0, 99999999.0)] public decimal UnitWeight { get; set; } [Range(0, int.MaxValue)] public int Units { get; set; } [DisplayName("Price")] [Range(0, 99999999.0)] public decimal PricePerUnitOfWeight { get; set; } [DisplayName("Value Date")] [DataType(DataType.Date)] [Required] public DateTime ValueDate { get; set; } public string Destination { get; set; } [DisplayName("Lot number")] public string LotNumber { get; set; } [DisplayName("Crop year")] public int? CropYear { get; set; } [HiddenInput(DisplayValue=false)] public decimal TotalAmount { get { return PricePerUnitOfWeight * Units; } } [HiddenInput(DisplayValue = false)] public int OrderId { get; set; } public string PackageTypeId { get; set; } }
On the view
Now that we have our view model set up, we need to create an Editor Template for it. Editor Templates are MVC features that allow you to declare snippets of HTML that are used any time the framework needs to generate markup for a particular Type. The Kendo UI MVC extensions integrate into the Editor Template framework, so if we don't specify a template for our view model, then the grids popup editor won't use any of the Kendo UI Widgets.
@using System.Collections @using Kendo.Mvc.UI; @using SalesHub.Client.ViewModels.Api; @model OrderDetailViewModel <ul class="errors"></ul> @Html.HiddenFor(model => model.OrderDetailId) <div class="editor-label"> @Html.LabelFor(model => model.Origin) </div> <div class="editor-field"> @Html.Kendo().DropDownListFor(m => m.Origin).BindTo((System.Collections.IEnumerable) ViewData["Origins"]).DataTextField("Name").DataValueField("Value").OptionLabel("Select an Origin") @Html.ValidationMessageFor(model => model.Origin) </div> <div class="editor-label"> @Html.LabelFor(model => model.PackageTypeId, "Package Type") </div> <div class="editor-field"> @Html.Kendo().DropDownListFor(m => m.PackageTypeId).BindTo((IEnumerable>SelectListItem<) ViewData["PackageTypes"]).OptionLabel("Select a Package Type") @Html.ValidationMessageFor(model => model.PackageTypeId) </div> <div class="editor-label"> @Html.LabelFor(model => model.NetWeight) </div> <div class="editor-field"> @Html.Kendo().NumericTextBoxFor(model => model.NetWeight).Decimals(2).Events(events => events.Change("window.SalesHub.OrderDetailsEdit_NetWeight_Change")) @Html.ValidationMessageFor(model => model.NetWeight) </div> <div class="editor-label"> @Html.LabelFor(model => model.UnitWeight) </div> <div class="editor-field"> @Html.Kendo().NumericTextBoxFor(model => model.UnitWeight).Decimals(2).Events(events => events.Change("window.SalesHub.OrderDetailsEdit_UnitWeight_Change")) @Html.ValidationMessageFor(model => model.UnitWeight) </div> <div class="editor-label"> @Html.LabelFor(model => model.Units) </div> <div class="editor-field"> @Html.Kendo().NumericTextBoxFor(model => model.Units).Format("n0").Events(events => events.Change("window.SalesHub.OrderDetailsEdit_Units_Change")) @Html.ValidationMessageFor(model => model.Units) </div> <div class="editor-label"> @Html.LabelFor(model => model.PricePerUnitOfWeight) </div> <div class="editor-field"> @Html.Kendo().NumericTextBoxFor(model => model.PricePerUnitOfWeight).Decimals(10) @Html.ValidationMessageFor(model => model.PricePerUnitOfWeight) </div> <div class="editor-label"> @Html.LabelFor(model => model.ValueDate) </div> <div class="editor-field"> @Html.Kendo().DatePickerFor(model => model.ValueDate) @Html.ValidationMessageFor(model => model.ValueDate) </div> <div class="editor-label"> @Html.LabelFor(model => model.Destination) </div> <div class="editor-field"> @Html.Kendo().DropDownListFor(model => model.Destination).BindTo((IEnumerable)ViewData["Destinations"]).DataValueField("Value").DataTextField("Name") @Html.ValidationMessageFor(model => model.Destination) </div> <div class="editor-label"> @Html.LabelFor(model => model.LotNumber) </div> <div class="editor-field"> @Html.TextBoxFor(model => model.LotNumber, new { @class = "k-textbox" }) @Html.ValidationMessageFor(model => model.LotNumber) </div> <div class="editor-label"> @Html.LabelFor(model => model.CropYear) </div> <div class="editor-field"> @Html.TextBoxFor(model => model.CropYear, new { @class = "k-textbox" }) @Html.ValidationMessageFor(model => model.CropYear) </div>
One thing to note in the Editor Template is that we declare a ul
with a class of "errors". We'll use this ul
to display any of the errors that we get from the server.
Now that our view model has validation attributes, we will set up our data service to make sure everything is valid in our parameters. To show how this
Since we only really care about validation in the create/update methods of our controller, that's where we'll add our validation logic.
public class CustomerOrderDetailsController : Controller { [HttpPut] public JsonResult CreateOrderDetail(int id, OrderDetailViewModel orderDetailViewModel, [DataSourceRequest] DataSourceRequest dataSourceRequest) { var order = _orderRepository.GetAllOrders().SingleOrDefault(o => o.OrderId == id); if (order.OrderDate > orderDetailViewModel.ValueDate) { ModelState.AddModelError("OrderDate", "Order detail can't pre-date order"); } if (ModelState.IsValid) { // Create the new order detail in the database. } var resultData = new[] { orderDetailViewModel }; return Json(resultData.AsQueryable().ToDataSourceResult(dataSourceRequest, ModelState)); } [HttpPost] public JsonResult UpdateOrderDetail([DataSourceRequest] DataSourceRequest dataSourceRequest, OrderDetailViewModel orderDetailViewModel) { var order = _orderRepository.GetOrderById(orderDetailViewModel.OrderId); if (order.OrderDate > orderDetailViewModel.ValueDate) { ModelState.AddModelError("OrderDate", "Order detail can't pre-date order"); } if (ModelState.IsValid) { // Persist changes to the database. } var resultData = new[] { orderDetailViewModel };- return Json(resultData.AsQueryable().ToDataSourceResult(dataSourceRequest, ModelState)); } }
By
In addition to the validation attributes, we also perform some custom validation that can't easily be expressed in an attribute. Our custom validation is to ensure that the OrderDetail does not predate the Order that contains it. If the ValueDate of the OrderDetail is before the Order's date, then we add a new error to the ModelState. We do this by calling the "AddModelError" method, which takes two parameters. The first parameter is the name of the property on the model that the error applies to and the second is the error message.
It is important that we only create/update the OrderDetail if the ModelState is valid. Since our controller's methods are being called by a Kendo DataSource we need to return a result from the controller that makes sense to the DataSource. This is where extension method "ToDataSourceResult" comes in handy. This extension method is provided by the Kendo MVC Extensions and it allows you to convert an IQueryable into an object which, when serialized, makes sense to the DataSource. In addition to converting an IQueryable into a DataSourceResult, it also allows you to pass in a ModelStateDictionary. If the ModelState has any errors, this function will copy them into the DataSourceResult.
Once we've gotten our DataSourceResult all we need to do is convert it into JSON and return it as the result.
Now that our server handles validation and will return any validation errors, we need to set up our Grid to handle those errors.
To do
@(Html.Kendo().Grid<OrderDetailViewModel>() .Name("orderDetailsGrid") /* Not relevant grid setup code... */ .DataSource(dataSource => dataSource .Ajax() .Read(builder => builder.Url("/api/CustomerOrderDetails/GetOrderDetails/" + Model.OrderId).Type(HttpVerbs.Get)) .Create(builder => builder.Url("/api/CustomerOrderDetails/CreateOrderDetail/" + Model.OrderId).Type(HttpVerbs.Put)) .Update(builder => builder.Url("/api/CustomerOrderDetails/UpdateOrderDetail").Type(HttpVerbs.Post)) .Destroy(builder => builder.Url("/api/CustomerOrderDetails/DeleteOrderDetail").Type(HttpVerbs.Delete)) .Model(model => { model.Id(x => x.OrderDetailId); model.Field(m => m.OrderDetailId).DefaultValue(0); }) .Events(events => events.Error("window.SalesHub.OrderDetails_Error")) ))
The important part here is that when we declare the DataSource to use our CustomerOrderDetailsController, we specify an event handler for any errors that occur.
Now that we've hooked into the error event of the DataSource, we need a way of displaying these errors to the user. One easy way of doing this is to create a Kendo template, into which we can pass the error data and display it to the user in a meaningful way.
Before we can create the template we need to know what the error data looks like when the server returns it. Simply cause a validation error using the grid and open your browser's
Here's what a validation error from the CustomerOrderDetailsController looks like:
Now we can set up our template which will display these error messages to the user.
<script type="text/x-kendo-template" id="orderDetailsValidationMessageTemplate"> # if (messages.length) { # <li>#=field# <ul> # for (var i = 0; i < messages.length; ++i) { # <li>#= messages[i] #</li> # } # </ul> </li> # } # </script>
This template expects to recieve an object which has field and messages properties. The field property is the name of the property on the view model that the error messages apply to. The messages property is an array of error messages that apply to the field. Our template simply outputs the name of the field and the error messages that apply to it.
Now that we have our template, we can wire up our event handler for the error event.
window.SalesHub.OrderDetails_Error = function(args) { if (args.errors) { var grid = $("#orderDetailsGrid").data("kendoGrid"); var validationTemplate = kendo.template($("#orderDetailsValidationMessageTemplate").html()); grid.one("dataBinding", function(e) { e.preventDefault(); $.each(args.errors, function(propertyName) { var renderedTemplate = validationTemplate({ field: propertyName, messages: this.errors }); grid.editable.element.find(".errors").append(renderedTemplate); }); }); } };
The error handler receives a parameter from the DataSource which contains the error information that was received from the server.
The first thing we do is check to see if we have any error messages from the server. We do this by checking the errors property of args. If we have errors from the server, we find the orderDetailsGrid and we create a Kendo UI template function around the template we declared earlier.
Using the orderDetailsGrid object, we hook into its dataBinding event. The reason we do this is because the grid will rebind and we need to cancel it (this prevents the editor dialog from disappearing). We can cancel the binding by calling preventDefault on the event args we receive.
Once we cancel the bind operation, we iterate over each error in the errors property. On each iteration we create an object using the propertyName and the array of error messages for that property. We pass this object into the Kendo UI template function that we created earlier. This function call returns the rendered HTML markup from our template. Using this markup we find the editor element of the Grid and find the <ul> that we can append our markup to.
As the end result the editor dialog for the grid looks something like this, when it gets a server-side validation error:
Server-side validation is an important part of any web application, and with the Kendo UI MVC extensions it is extremely easy to handle with the data services that a Kendo UI Grid uses.
Thomas Mullaly enjoys building rich, client-side apps using Kendo UI/HTML5 and loves learning about any new web technology. You can follow Thomas on Twitter at @thomas_mullaly.