Telerik blogs

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.

Why Client-Side Validation Shouldn't Be Relied On

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 devtools and fiddle around with a site's client-side validation to allow invalid data to be submitted to the data services. Additionally, the validation logic can be too complicated for the UI of the app to handle or even know about.

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.

Using Validation Attributes

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 of .NET's data annotation framework and out-of-the-box .NET already provides a few useful validation attributes. Using these attributes lets you put some of the burden of validating your data onto the framework. This allows you to cut down on the amount of code that you need to write, in order to validate the data that you're processing.

Let's take a look at the OrderViewModel, which uses some of these attributes.

OrderViewModel

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 model we use a few data validation attributes. In particular we set a Range constraint on a few properties, and we Require the ValueDate property to be set. These validation attributes are picked up by the framework when it processes the request to the controller.

Setting up the Editor Template

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.

OrderDetailViewModel Editor Template

@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.

Adding Server-Side Vaildation

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 works I'll be using the CustomerOrderDetailsController as the example.

Since we only really care about validation in the create/update methods of our controller, that's where we'll add our validation logic.

CustomerOrderDetailsController

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 default MVC will validate the parameters that are passed to your controllers method and will store the result of the validation in the ModelState property of our controller. This validation process also checks to see if there are any validation attributes that need to be applied to the data being passed in.

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.

Handling Server-Side Validation Errors

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 this we need to hook into the "error" event of the DataSource that the order details grid uses. The following code snippet shows you how we hook into this event using the MVC Extensions.

_Order.cshtml

@(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 devtools to see what the response was.

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.

Order details error template

<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.

Error event handler

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:

Conclusion

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.


About the Author

Thomas Mullaly

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.

Related Posts

Comments

Comments are disabled in preview mode.