MVC 4, Web API, OData, Entity Framework, Kendo UI, Binding a Form to Datasource (CRUD) with MVVM

This will be part five of a six part series of blog posts.

  1. Modern Web Application Layered High Level Architecture with SPA, MVC, Web API, EF, Kendo UI, OData
  2. Generically Implementing the Unit of Work & Repository Pattern with Entity Framework in MVC & Simplifying Entity Graphs
  3. MVC 4, Kendo UI, SPA with Layout, View, Router & MVVM
  4. MVC 4, Web API, OData, EF, Kendo UI, Grid, Datasource (CRUD) with MVVM
  5. MVC 4, Web API, OData, EF, Kendo UI, Binding a Form to Datasource (CRUD) with MVVM
  6. Upgrading to Async with Entity Framework, MVC, OData AsyncEntitySetController, Kendo UI, Glimpse & Generic Unit of Work Repository Framework v2.0

Update: 09/09/2013 – Sample application and source code has been uploaded to CodePlex: https://genericunitofworkandrepositories.codeplex.com, updated Visual 2013, Twitter Bootstrap, MVC 5, EF6, Kendo UI Bootstrap theme, project redeployed to Windows Azure Website.

Update: 06/20/2013 – Bug fix: productEdit View intermittently failing to update. Enhancement: Added state management for Grid, after productEdit View updates (syncs), will auto navigate back to Grid and re-select the last selected row. Updated blog, sample app download, and live demo.

Just a quick recap on the last post, we wired up the Kendo UI Grid, DataSource with MVVM with all the traditional CRUD functionality. In this blog we’ll cover editing with a form in a Kendo UI View that is remotely loaded into our SPA that is bound the Kendo UI Datasource using MVVM. The View will be loaded in from with a click of a button on the row your are trying to edit from the Kendo UI Grid.

This will be part three of a five part series of blog posts.

  1. Generically Implementing the Unit of Work & Repository Pattern with Entity Framework in MVC & Simplifying Entity Graphs
  2. MVC 4, Kendo UI, SPA with Layout, View, Router & MVVM
  3. MVC 4, Web API, OData, EF, Kendo UI, Grid, Datasource (CRUD) with MVVM
  4. MVC 4, Web API, OData, EF, Kendo UI, Binding a Form to Datasource (CRUD) with MVVM
  5. Upgrading to Async with Entity Framework, MVC, OData AsyncEntitySetController, Kendo UI, Glimpse & Generic Unit of Work Repository Framework v2.0

Taking a look at a high level architecture of this three part series blog: Modern Web Application Layered High Level Architecture with SPA, MVC, Web API, EF, Kendo UI.

Live demo: http://longle.azurewebsites.net, courtesy of Windows Azure free 10 Website’s

Spa.Controllers.ProductController.cs


    public class ProductController : EntitySetController<Product, int>
    {
        private readonly IUnitOfWork _unitOfWork;

        public ProductController(IUnitOfWork unitOfWork)
        {
            _unitOfWork = unitOfWork;
        }

        public override IQueryable<Product> Get()
        {
            return _unitOfWork.Repository<Product>().Query().Get();
        }

        protected override Product GetEntityByKey(int key)
        {
            return _unitOfWork.Repository<Product>().FindById(key);
        }

        protected override Product UpdateEntity(int key, Product update)
        {
            update.State = ObjectState.Modified;
            _unitOfWork.Repository<Product>().Update(update);
            _unitOfWork.Save();
            return update;
        }
        
        public override void Delete([FromODataUri] int key)
        {
            _unitOfWork.Repository<Product>().Delete(key);
            _unitOfWork.Save();
        }
        
        protected override void Dispose(bool disposing)
        {
            _unitOfWork.Dispose();
            base.Dispose(disposing);
        }
    }

Our Web Api OData ProductsController, pretty much the same Controller used in the previous blog to hydrate our Grid along with the other CRUD actions. This Controller will also share the same duties as it did before, for the Grid. Here it handle hydrating the Form, perform updates and deletes.

Adding a custom command Edit button to the Grid (line 32)

Spa/Content/Views/products.html


<script type="text/x-kendo-template" id="products">
    <section class="content-wrapper main-content clear-fix">    
        <h3>Technlogy Stack</h3>
        <ol class="round">
            <li class="one">
                <h5>.NET</h5>
                ASP.NET MVC 4, Web API, OData, Entity Framework        
            </li>
            <li class="two">
                <h5>Kendo UI Web Framework</h5>
                MVVM, SPA, Grid, DataSource
            </li>
            <li class="three">
                <h5>Patterns</h5>
                Unit of Work, Repository, MVVM
            </li>
        </ol>
        <h3>View Products</h3><br/>
            <div class="k-content" style="width:100%">    
            <div id="productsForm">
            <div id="productGrid" 
                data-role="grid"
                data-sortable="true"
                data-pageable="true"
                data-filterable="true"
                data-bind="source: dataSource, events:{dataBound: dataBound, change: onChange}"
                data-editable = "inline"
                data-selectable="true" 
                data-columns='[
                    { field: "ProductID", title: "Id", width: "50px" }, 
                    { field: "ProductName", title: "Name", width: "300px" }, 
                    { field: "UnitPrice", title: "Price", format: "{0:c}", width: "100px" },                    
                    { field: "Discontinued", width: "150px" }, 
                    { command : [ "edit", "destroy", { text: "Edit Details", click: editProduct } ], title: "Action",  } ]'>
            </div>
            </div>
            </div>
    </section>    
</script>

<script>
    function editProduct(e) {
        e.preventDefault();
        var tr = $(e.currentTarget).closest("tr");
        var dataItem = $("#productGrid").data("kendoGrid").dataItem(tr);
        window.location.href = '#/productEdit/' + dataItem.ProductID;
    }

    var lastSelectedProductId;
                
    var crudServiceBaseUrl = "/odata/Product";
    var productsModel = kendo.observable({
        dataSource: dataSource = new kendo.data.DataSource({
            type: "odata",
            transport: {
                read: {
                    url: crudServiceBaseUrl,
                    dataType: "json"
                },
                update: {
                    url: function (data) {
                        return crudServiceBaseUrl + "(" + data.ProductID + ")";
                    },
                    dataType: "json"
                },
                destroy: {
                    url: function (data) {
                        return crudServiceBaseUrl + "(" + data.ProductID + ")";
                    },
                    dataType: "json"
                }
            },
            batch: false,
            serverPaging: true,
            serverSorting: true,
            serverFiltering: true,
            pageSize: 10,
            schema: {
                data: function (data) {
                    return data.value;
                },
                total: function (data) {
                    return data["odata.count"];
                },
                errors: function (data) {
                },
                model: {
                    id: "ProductID",
                    fields: {
                        ProductID: { type: "number", editable: false, nullable: true },
                        ProductName: { type: "string", validation: { required: true } },
                        UnitPrice: { type: "number", validation: { required: true, min: 1 } },
                        Discontinued: { type: "boolean" },
                        UnitsInStock: { type: "number", validation: { min: 0, required: true } }
                    }
                }
            },
            error: function (e) {
                var message = e.xhr.responseJSON["odata.error"].message.value;
                var innerMessage = e.xhr.responseJSON["odata.error"].innererror.message;
                alert(message + "\n\n" + innerMessage);
            }
        }),
        dataBound: function (arg) {
            if (lastSelectedProductId == null) return; // check if there was a row that was selected
            var view = this.dataSource.view(); // get all the rows
            for (var i = 0; i < view.length; i++) { // iterate through rows
                if (view[i].ProductID == lastSelectedProductId) { // find row with the lastSelectedProductd
                    var grid = arg.sender; // get the grid
                    grid.select(grid.table.find("tr[data-uid='" + view[i].uid + "']")); // set the selected row
                    break;
                }
            }
        },
        onChange: function (arg) {
            var grid = arg.sender;
            var dataItem = grid.dataItem(grid.select());
            lastSelectedProductId = dataItem.ProductID;
        }
    });

    $(document).bind("viewSwtichedEvent", function (e, args) { // subscribe to the viewSwitchedEvent
        if (args.name == "products") { // check if this view was switched too
            if (args.isRemotelyLoaded) { // check if this view was remotely loaded from server
                kendo.bind($("#productsForm"), productsModel); // bind the view to the model
            } else {// view already been loaded in cache
                productsModel.dataSource.fetch(function() {}); // refresh grid
            }
        }
    });

</script>

Additions to Client-Side

editProduct


function editProduct(e) {
    e.preventDefault();
    var tr = $(e.currentTarget).closest("tr");
    var dataItem = $("#productGrid").data("kendoGrid").dataItem(tr);
    window.location.href = '#/productEdit/' + dataItem.ProductID;
}

The editProduct method will handle extracting the ProductID of the row we clicked “Edit Details” and navigating to the ProductEdit View.

Additions to the Observable Model

dataBound


dataBound: function (arg) {
    if (lastSelectedProductId == null) return; // check if there was a row that was selected
    var view = this.dataSource.view(); // get all the rows
    for (var i = 0; i < view.length; i++) { // iterate through rows
        if (view[i].ProductID == lastSelectedProductId) { // find row with the lastSelectedProductd
            var grid = arg.sender; // get the grid
            grid.select(grid.table.find("tr[data-uid='" + view[i].uid + "']")); // set the selected row
            break;
        }
    }

The dataBound (delgate) event handler will be responsible for re-selecting the last selected row in the Grid before we navigated away from the Grid to the productEdit View.

onChange


onChange: function (arg) {
    var grid = arg.sender;
    var dataItem = grid.dataItem(grid.select());
    lastSelectedProductId = dataItem.ProductID;
}

The onChange (delegate) event handler will be responsible for saving off the last selected rows ProductID so that if we navigate to the ProductEdit View and back we can maintain the last selected row state.

6-19-2013 6-27-27 PM

Creating the ProductEdit View

Spa/Content/Views/productEdit.html
(styles have been omitted, for clarity)


<!-- styles remove for clarity -->

<script type="text/x-kendo-template" id="productEdit">
    <section class="content-wrapper main-content clear-fix">                
        <div class="k-block" style="width:600px; margin-top:35px">            
            <div class="k-block k-info-colored">
                <strong>Note: </strong>Please fill out all of the fields in this form.
            </div>
            <div id="product-edit-form">
                <dl>
                    <dt>
                        <label for="firstName">Product Name:</label></dt>
                    <dd>
                        <span class="k-textbox k-space-right">
                            <input id="productName" type="text" data-bind="value: ProductName" />
                            <a href="#" data-field="productName" data-bind="click: clear" class="k-icon k-i-close">&nbsp;</a>
                        </span>
                    </dd>
                    <dt>
                        <label for="lastName">English Name:</label></dt>
                    <dd>
                        <span class="k-textbox k-space-right">
                            <input id="englishName" type="text" data-bind="value: EnglishName" />
                            <a href="#" data-field="englishName" data-bind="click: clear" class="k-icon k-i-close">&nbsp;</a>
                        </span>
                    </dd>
                    <dt>
                        <label for="quanityPerUnit">Quanity Per Unit:</label></dt>
                    <dd>
                        <span class="k-textbox k-space-right">
                            <input id="quanityPerUnit" type="text" data-bind="value: QuantityPerUnit" />
                            <a href="#" data-field="quanityPerUnit" data-bind="click: clear" class="k-icon k-i-close">&nbsp;</a>
                        </span>
                    </dd>
                    <dt>
                        <label for="unitPrice">Unit Price:</label></dt>
                    <dd>
                        <span class="k-textbox k-space-right">
                            <input id="unitPrice" type="text" data-bind="value: UnitPrice" />
                            <a href="#" data-field="unitPrice" data-bind="click: clear" class="k-icon k-i-close">&nbsp;</a>
                        </span>
                    </dd>
                    <dt>
                        <label for="unitPrice">Unit In Stock:</label></dt>
                    <dd>
                        <span class="k-textbox k-space-right">
                            <input id="unitsInStock" type="text" data-bind="value: UnitsInStock" />
                            <a href="#" data-field="unitsInStock" data-bind="click: clear" class="k-icon k-i-close">&nbsp;</a>
                        </span>
                    </dd>
                    <dt>
                        <label for="unitsOnOrder">Unit On Order:</label></dt>
                    <dd>
                        <span class="k-textbox k-space-right">
                            <input id="unitsOnOrder" type="text" data-bind="value: UnitsOnOrder" />
                            <a href="#" data-field="unitsOnOrder" data-bind="click: clear" class="k-icon k-i-close">&nbsp;</a>
                        </span>
                    </dd>
                    <dt>
                        <label for="reorderLevel">Reorder Level:</label></dt>
                    <dd>
                        <span class="k-textbox k-space-right">
                            <input id="reorderLevel" type="text" data-bind="value: ReorderLevel" />
                            <a href="#" data-field="reorderLevel" data-bind="click: clear" class="k-icon k-i-close">&nbsp;</a>
                        </span>
                    </dd>
                    <dt>
                        <label for="discontinued">Discontinued:</label></dt>
                    <dd>
                        <select id="discontinued" data-role="dropdownlist">
                            <option value="1">Yes</option>
                            <option value="2">No</option>
                        </select>
                    </dd>
                    <dt>
                        <label for="Recieved">Recieved:</label></dt>
                    <dd>
                        <input data-role="datepicker" id="recieved">
                    </dd>
                </dl>
                <a class="k-button" data-bind="click: saveProduct"><span span class="k-icon k-i-tick"></span> Submit</a>
                <a class="k-button" data-bind="click: cancel"><span span class="k-icon k-i-tick"></span> Cancel</a>
            </div>
        </div>
    </section>
</script>

<script>
    var getProductId = function () { // parse for ProductId from url
        var array = window.location.href.split('/');
        var productId = array[array.length - 1];
        return productId;
    };
    
    var crudServiceBaseUrl = "/odata/Product";
    
    $(document).bind("viewSwtichedEvent", function (e, args) { // subscribe to viewSwitchedEvent
        if (args.name == "productEdit") { // check if this view was switched to
            var productModel = kendo.data.Model.define({ // we want to refresh this view anytime its switched to
                id: "ProductID",
                fields: {
                    ProductID: { type: "number", editable: false, nullable: true },
                    ProductName: { type: "string", validation: { required: true } },
                    EnglishName: { type: "string", validation: { required: true } },
                    UnitPrice: { type: "number", validation: { required: true, min: 1 } },
                    Discontinued: { type: "boolean" },
                    UnitsInStock: { type: "number", validation: { min: 0, required: true } }
                },
                saveProduct: function (e) {
                    e.preventDefault();
                    dataSource.sync();
                    window.location.href = '/index.html#/products';
                },
                cancel: function (e) {
                    e.preventDefault();
                    window.location.href = '/index.html#/products';
                }
            });

            var dataSource = new kendo.data.DataSource({
                type: "odata",
                transport: {
                    read: {
                        url: function (data) {
                            return crudServiceBaseUrl + "(" + getProductId() + ")";
                        },
                        dataType: "json"
                    },
                    update: {
                        url: function (data) {
                            delete data.guid;
                            delete data["odata.metadata"];
                            return crudServiceBaseUrl + "(" + getProductId() + ")";
                        },
                        contentType: "application/json",
                        type: "PUT",
                        dataType: "json"
                    },
                    create: {
                        url: crudServiceBaseUrl,
                        dataType: "json"
                    },
                    destroy: {
                        url: function (data) {
                            return crudServiceBaseUrl + "(" + getProductId() + ")";
                        },
                        dataType: "json"
                    },
                    parameterMap: function (data, operation) {
                        if (operation == "update") {
                            delete data.guid;
                            delete data["odata.metadata"];
                            data.UnitPrice = data.UnitPrice.toString();
                        }
                        return JSON.stringify(data);
                    }
                },
                sync: function (e) {
                    window.location.href = '/index.html#/products';
                },
                batch: false,
                schema: {
                    type: "json",
                    data: function (data) {
                        delete data["odata.metadata"];
                        return data;
                    },
                    total: function (data) {
                        return 1;
                    },
                    model: productModel
                }
            });
            dataSource.fetch(function() {
                if (dataSource.view().length > 0) {
                    kendo.bind($("#product-edit-form"), dataSource.at(0));
                }
            });
        }
    });

</script>

The ProductEdit View will be bound to the Kendo Observable Model that the Datasource will return. It’s called Observable Model because the there is two-binding between the Form and the Model, meaning when a change happens in the Form, it is automatically synced with the Model, which is bound to the Datasource.

6-19-2013 6-28-33 PM

You can essentially setup auto-sync on the Datasource so that when there are changes, it will automatically sync back to our OData Web API ProductsController. However for purposes of this post we will stick to a manual sync when we are ready to send our updates to our Controller.

product-edit-form (DIV)


            <div id="product-edit-form">
                <dl>
                    <dt>
                        <label for="firstName">Product Name:</label></dt>
                    <dd>
                        <span class="k-textbox k-space-right">
                            <input id="productName" type="text" data-bind="value: ProductName" />
                            <a href="#" data-field="productName" data-bind="click: clear" class="k-icon k-i-close">&nbsp;</a>
                        </span>
                    </dd>
                    <dt>
                        <label for="lastName">English Name:</label></dt>
                    <dd>
                        <span class="k-textbox k-space-right">
                            <input id="englishName" type="text" data-bind="value: EnglishName" />
                            <a href="#" data-field="englishName" data-bind="click: clear" class="k-icon k-i-close">&nbsp;</a>
                        </span>
                    </dd>
                    <dt>
                        <label for="quanityPerUnit">Quanity Per Unit:</label></dt>
                    <dd>
                        <span class="k-textbox k-space-right">
                            <input id="quanityPerUnit" type="text" data-bind="value: QuantityPerUnit" />
                            <a href="#" data-field="quanityPerUnit" data-bind="click: clear" class="k-icon k-i-close">&nbsp;</a>
                        </span>
                    </dd>
                    <dt>
                        <label for="unitPrice">Unit Price:</label></dt>
                    <dd>
                        <span class="k-textbox k-space-right">
                            <input id="unitPrice" type="text" data-bind="value: UnitPrice" />
                            <a href="#" data-field="unitPrice" data-bind="click: clear" class="k-icon k-i-close">&nbsp;</a>
                        </span>
                    </dd>
                    <dt>
                        <label for="unitPrice">Unit In Stock:</label></dt>
                    <dd>
                        <span class="k-textbox k-space-right">
                            <input id="unitsInStock" type="text" data-bind="value: UnitsInStock" />
                            <a href="#" data-field="unitsInStock" data-bind="click: clear" class="k-icon k-i-close">&nbsp;</a>
                        </span>
                    </dd>
                    <dt>
                        <label for="unitsOnOrder">Unit On Order:</label></dt>
                    <dd>
                        <span class="k-textbox k-space-right">
                            <input id="unitsOnOrder" type="text" data-bind="value: UnitsOnOrder" />
                            <a href="#" data-field="unitsOnOrder" data-bind="click: clear" class="k-icon k-i-close">&nbsp;</a>
                        </span>
                    </dd>
                    <dt>
                        <label for="reorderLevel">Reorder Level:</label></dt>
                    <dd>
                        <span class="k-textbox k-space-right">
                            <input id="reorderLevel" type="text" data-bind="value: ReorderLevel" />
                            <a href="#" data-field="reorderLevel" data-bind="click: clear" class="k-icon k-i-close">&nbsp;</a>
                        </span>
                    </dd>
                    <dt>
                        <label for="discontinued">Discontinued:</label></dt>
                    <dd>
                        <select id="discontinued" data-role="dropdownlist">
                            <option value="1">Yes</option>
                            <option value="2">No</option>
                        </select>
                    </dd>
                    <dt>
                        <label for="Recieved">Recieved:</label></dt>
                    <dd>
                        <input data-role="datepicker" id="recieved">
                    </dd>
                </dl>
                <a class="k-button" data-bind="click: saveProduct"><span span class="k-icon k-i-tick"></span> Submit</a>
                <a class="k-button" data-bind="click: cancel"><span span class="k-icon k-i-tick"></span> Cancel</a>
            </div>

Notice how everything that needs to bound is using the attributes prefixed with “data-“. This is what the Kendo Web MVVM Framework will scan for when when binding a View with a Model, long story short, this is how you specify the binding mapping options for the following:

  • Widget Type (e.g. Grid, TreeView, Calendar, DropDownList, etc.)
  • Widget Properties (Attributes)
  • Binding Type (e.g. value, click, text, etc.)
  • Binding Property from Model (e.g. firstName, lastName, productDatasource, etc.)
  • Binding Methods from Model (e.g. openWindow, cancel, sendEmail, etc.)

Client Side & Product Datasource


<script>
    var getProductId = function () { // parse for ProductId from url
        var array = window.location.href.split('/');
        var productId = array[array.length - 1];
        return productId;
    };
    
    var crudServiceBaseUrl = "/odata/Product";
    
    $(document).bind("viewSwtichedEvent", function (e, args) { // subscribe to viewSwitchedEvent
        if (args.name == "productEdit") { // check if this view was switched to
            var productModel = kendo.data.Model.define({ // we want to refresh this view anytime its switched to
                id: "ProductID",
                fields: {
                    ProductID: { type: "number", editable: false, nullable: true },
                    ProductName: { type: "string", validation: { required: true } },
                    EnglishName: { type: "string", validation: { required: true } },
                    UnitPrice: { type: "number", validation: { required: true, min: 1 } },
                    Discontinued: { type: "boolean" },
                    UnitsInStock: { type: "number", validation: { min: 0, required: true } }
                },
                saveProduct: function (e) {
                    e.preventDefault();
                    dataSource.sync();
                    window.location.href = '/index.html#/products';
                },
                cancel: function (e) {
                    e.preventDefault();
                    window.location.href = '/index.html#/products';
                }
            });

            var dataSource = new kendo.data.DataSource({
                type: "odata",
                transport: {
                    read: {
                        url: function (data) {
                            return crudServiceBaseUrl + "(" + getProductId() + ")";
                        },
                        dataType: "json"
                    },
                    update: {
                        url: function (data) {
                            delete data.guid;
                            delete data["odata.metadata"];
                            return crudServiceBaseUrl + "(" + getProductId() + ")";
                        },
                        contentType: "application/json",
                        type: "PUT",
                        dataType: "json"
                    },
                    create: {
                        url: crudServiceBaseUrl,
                        dataType: "json"
                    },
                    destroy: {
                        url: function (data) {
                            return crudServiceBaseUrl + "(" + getProductId() + ")";
                        },
                        dataType: "json"
                    },
                    parameterMap: function (data, operation) {
                        if (operation == "update") {
                            delete data.guid;
                            delete data["odata.metadata"];
                            data.UnitPrice = data.UnitPrice.toString();
                        }
                        return JSON.stringify(data);
                    }
                },
                sync: function (e) {
                    window.location.href = '/index.html#/products';
                },
                batch: false,
                schema: {
                    type: "json",
                    data: function (data) {
                        delete data["odata.metadata"];
                        return data;
                    },
                    total: function (data) {
                        return 1;
                    },
                    model: productModel
                }
            });
            dataSource.fetch(function() {
                if (dataSource.view().length > 0) {
                    kendo.bind($("#product-edit-form"), dataSource.at(0));
                }
            });
        }
    });

</script>

The Product Datasource is responsible for loading the Product details and providing a Observable Model the Form can bind to. It will handle all the rest of the CRUD activities such as updating and deleting the Product. All of the CRUD activities handled by the Datasource will happen over REST using the OData protocol asynchronously.

Client Side Code

Parsing the ProductId From the URL


    var getProductId = function () { // parse for ProductId from url
        var array = window.location.href.split('/');
        var productId = array[array.length - 1];
        return productId;
    };

This code pretty much speaks for itself, we are simply parsing the Url to get the ProductId of the Product we are loading and binding to the View.

6-19-2013 6-30-21 PM

productModel (Observable Model)


            var productModel = kendo.data.Model.define({ // we want to refresh this view anytime its switched to
                id: "ProductID",
                fields: {
                    ProductID: { type: "number", editable: false, nullable: true },
                    ProductName: { type: "string", validation: { required: true } },
                    EnglishName: { type: "string", validation: { required: true } },
                    UnitPrice: { type: "number", validation: { required: true, min: 1 } },
                    Discontinued: { type: "boolean" },
                    UnitsInStock: { type: "number", validation: { min: 0, required: true } }
                },
                saveProduct: function (e) {
                    e.preventDefault();
                    dataSource.sync();
                    window.location.href = '/index.html#/products';
                },
                cancel: function (e) {
                    e.preventDefault();
                    window.location.href = '/index.html#/products';
                }
            });

This is how we set up our Observable Product Model that will be returned from the Datasource and bound to the View. We can see here we define the primary key field (property), fields, and methods that are View buttons will bind to. When the saveProduct method is invoked, we will perform a sync, meaning all changes in the dataSource will be sent back to the server side for processing when this is invoked. Because our Model is an Observable Model, and there is two-way binding between the Model and the Datasource (as mentioned earlier), the Datasource is keeping track and knows of all the changes that are happening.

Notice how the cancel method is a redirect with the hash (#) in it, so when the redirect happens the Kendo Router will process this and have our SPA load in the Products View which is the Grid with the Product listing.

Decomposing the Datasource Configuration

paramaterMap


                        parameterMap: function (data, operation) {
                            if (operation == "update") {
                                delete data.guid;
                                delete data["odata.metadata"];
                                data.UnitPrice = data.UnitPrice.toString();
                            }
                            return JSON.stringify(data);
                        }


The parameterMap purpose is so that we can intercept and perform any pre-processing on the payload before it is sent to our Controller.

We are deleting all the properties that are not needed by our Controller, more importantly, we are doing this so that we don’t have any extra properties that are not on our Product Model or Entity, so that the MVC ModelBinder will recognize our payload and bind it to the Product parameter on our UpdateEntity(int key, Product update) method on our ProductController.

We are also converting the UnitPrice to a string before we sending back to the server side, because the UnitPrice type is decimal, and currently when using Web Api and OData, the MVC out of the box ModelBinder is not smart enough (yet) to convert a number to decimal in the ModelBinding process. Ironically, it is smart enough to convert to decimal if we send it as a string, so that’s what we’ll send of for now.

sync


sync: function (e) {
    window.location.href = '/index.html#/products';
},

The sync event is raised after the changes have been saved on the server side, once this is complete we simply navigate back to the Products Grid.

schema


schema: {
    type: "json",
    data: function (data) {
        delete data["odata.metadata"];
        return data;
    }

Here we are simply transforming the payload before binding it to the Form, we are removing the data.odata.metadata property since it’s really not needed and unpacking the data.

total


total: function (data) {
    return 1;
}

The total defined method here is simply returning the count of how many records where returned from the server, we are always returning 1 here, since this is a form bound to a single Product at all times. You can add some null checking here to return 0 or 1.

dataSource.fetch(callback)


            dataSource.fetch(function() {
                if (dataSource.view().length > 0) {
                    kendo.bind($("#product-edit-form"), dataSource.at(0));
                }
            });

This will invoke the Datasource to make a call to our Controller Get() method, and load the Product, notice how we are passing in a callback so that when the loading is complete (because it’s happening asynchronously) we are then setting to the variable productEditModel because this is the convention we need to follow mentioned in the previous posts (e.g. view, viewModel, view.html). Because we are following these conventions, our implantation in the Index.html view will work off of these conventions and bind the View to the correct Model for us.

There you have it, MVC 4, Web API, OData, EF, Kendo UI, Binding a Form to Datasource (CRUD) with MVVM – Part 3.

Live demo: http://longle.azurewebsites.net

Happy Coding…! :)

Download sample application: https://genericunitofworkandrepositories.codeplex.com

MVC 4, Kendo UI, SPA with Layout, View, Router & MVVM

This will be part three of a six part series of blog posts.

  1. Modern Web Application Layered High Level Architecture with SPA, MVC, Web API, EF, Kendo UI, OData
  2. Generically Implementing the Unit of Work & Repository Pattern with Entity Framework in MVC & Simplifying Entity Graphs
  3. MVC 4, Kendo UI, SPA with Layout, View, Router & MVVM
  4. MVC 4, Web API, OData, EF, Kendo UI, Grid, Datasource (CRUD) with MVVM
  5. MVC 4, Web API, OData, EF, Kendo UI, Binding a Form to Datasource (CRUD) with MVVM
  6. Upgrading to Async with Entity Framework, MVC, OData AsyncEntitySetController, Kendo UI, Glimpse & Generic Unit of Work Repository Framework v2.0

Update: 09/09/2013 – Project has been uploaded to CodePlex: https://genericunitofworkandrepositories.codeplex.com, updated Visual 2013, Twitter Bootstrap, MVC 5, EF6, Kendo UI Bootstrap theme, project redeployed to Windows Azure Website.

Update: 06/20/2013 – Bug fix(es): Fixed View being loaded duplicate times. Enhancement(s): Added View caching. Updated blog, sample app download, and live demo

Wondering how to setup a SPA with MVC 4 project using Kendo UI Web…?! Well let’s cut to the chase and get started. We will start off with a plain old regular ASP.NET MVC 4 Internet project template and convert to a SPA, for the most part this implementation will keep in mind many of us are familiar with MVC 4 so we’ll try to adapt a lot things we are intimate with like the usage and organization of views, layouts, conventions, etc.

This will be part one five of a five part series of blog posts.

  1. Generically Implementing the Unit of Work & Repository Pattern with Entity Framework in MVC & Simplifying Entity Graphs
  2. MVC 4, Kendo UI, SPA with Layout, View, Router & MVVM
  3. MVC 4, Web API, OData, EF, Kendo UI, Grid, Datasource (CRUD) with MVVM
  4. MVC 4, Web API, OData, EF, Kendo UI, Binding a Form to Datasource (CRUD) with MVVM
  5. Upgrading to Async with Entity Framework, MVC, OData AsyncEntitySetController, Kendo UI, Glimpse & Generic Unit of Work Repository Framework v2.0

Taking a look at a high level architecture of this three part series blog: Modern Web Application Layered High Level Architecture with SPA, MVC, Web API, EF, Kendo UI.

Live demo: http://longle.azurewebsites.net.

You’ll need KendoUIWeb NuGet package.

6-18-2013 1-40-51 AM

First let’s create the SPA landing page (index.html), this will be the default page when the site is loaded up and where our Kendo UI Layout object will reside, and where we will swap our different views in.

Index.html


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>MVC 4 SPA</title>
    <link href="/Content/kendo/2013.1.319/kendo.common.min.css" rel="stylesheet" />
    <link href="/Content/kendo/2013.1.319/kendo.metro.min.css" rel="stylesheet" />
    <link href="/Content/Site.css" rel="stylesheet" />
    <script src="/Scripts/jquery-2.0.2.min.js"></script>
    <script src="/Scripts/kendo/2013.1.319/kendo.web.min.js"></script>
    <script>
        var templateLoader = (function ($, host) {
            return {
                loadExtTemplate: function (name, path) {
                    $.ajax({
                        async: false,
                        url: path,
                        cache: false,
                        success: function (result) {
                            $("body").append(result);
                        },
                        error: function (result) {
                            alert("Error Loading View/Template -- TODO: Better Error Handling");
                        }
                    });
                }
            };
        })(jQuery, document);

        $(function () {
            var views = {};
            templateLoader.loadExtTemplate("layout", "/content/views/layout.html");
            var layout = new kendo.Layout($('#layout').html());
            layout.render($("#app"));

            var router = new kendo.Router();
            var addRoute = function (route, name, path, forceRemoteLoad) {
                forceRemoteLoad = typeof forceRemoteLoad !== "undefined" ? forceRemoteLoad : false;
                router.route(route, function () {
                    kendo.fx($("#body")).slideInRight().reverse().then(function () { // transition, slide view left
                        var isRemotelyLoaded = false;
                        if (views[name] == null || forceRemoteLoad)
                        {   // check if we have already loaded in cache, could store this in browser local storage for larger apps
                            isRemotelyLoaded = true;
                            templateLoader.loadExtTemplate(name, path); // load the view
                            views[name] = new kendo.View(('#' + name)); // add the view to cache
                        }
                        layout.showIn("#body", views[name]); // switch view
                        $(document).trigger("viewSwtichedEvent", { route: route, name: name, path: path, isRemotelyLoaded: isRemotelyLoaded }); // publish event view has been loaded (EventAggregator pattern)
                        kendo.fx($("#body")).slideInRight().play(); // transition, slide view back to the right (center)
                    });
                });
            };

            addRoute("/", "home", "/content/views/home.html");
            addRoute("/about", "about", "/content/views/about.html");
            addRoute("/contact", "contact", "/content/views/contact.html");
            addRoute("/categories", "categories", "/content/views/categories.html");
            addRoute("/customers", "customers", "/content/views/customers.html");
            addRoute("/products", "products", "/content/views/products.html");
            addRoute("/productEdit/:id", "productEdit", "/content/views/productEdit.html");

            router.start();
        });
    </script>
</head>
<body>
    <div id="app"></div>
</body>
</html>

I’ve deliberately left much of the JavaScript code on each of the pages in the interest of easily understanding what’s happening on each of the pages or views, obviously you can extract them into their own .js files however you see fit.

TemplateLoader Helper


        var templateLoader = (function ($, host) {
            return {
                loadExtTemplate: function (name, path) {
                    $.ajax({
                        async: false,
                        url: path,
                        cache: false,
                        success: function (result) {
                            $("body").append(result);
                        },
                        error: function (result) {
                            alert("Error Loading View/Template -- TODO: Better Error Handling");
                        }
                    });
                }
            };
        })(jQuery, document);

The templateLoader helper method was used from here, and has been slightly modified so that we can load Kendo Views remotely from the server in our SPA when navigating through the application.

  1. It’s taking a path to a file
  2. Grabbing the contents with jQuery Ajax
  3. Appending the contents to the body of our document
  4. Then notifying the application that the template has loaded

Kendo UI Web Router


        $(function () {
            var views = {};
            templateLoader.loadExtTemplate("layout", "/content/views/layout.html");
            var layout = new kendo.Layout($('#layout').html());
            layout.render($("#app"));

            var router = new kendo.Router();
            var addRoute = function (route, name, path, forceRemoteLoad) {
                forceRemoteLoad = typeof forceRemoteLoad !== "undefined" ? forceRemoteLoad : false;
                router.route(route, function () {
                    kendo.fx($("#body")).slideInRight().reverse().then(function () { // transition, slide view left
                        var isRemotelyLoaded = false;
                        if (views[name] == null || forceRemoteLoad)
                        {   // check if we have already loaded in cache, could store this in browser local storage for larger apps
                            isRemotelyLoaded = true;
                            templateLoader.loadExtTemplate(name, path); // load the view
                            views[name] = new kendo.View(('#' + name)); // add the view to cache
                        }
                        layout.showIn("#body", views[name]); // switch view
                        $(document).trigger("viewSwtichedEvent", { route: route, name: name, path: path, isRemotelyLoaded: isRemotelyLoaded }); // publish event view has been loaded (EventAggregator pattern)
                        kendo.fx($("#body")).slideInRight().play(); // transition, slide view back to the right (center)
                    });
                });
            };

            addRoute("/", "home", "/content/views/home.html");
            addRoute("/about", "about", "/content/views/about.html");
            addRoute("/contact", "contact", "/content/views/contact.html");
            addRoute("/categories", "categories", "/content/views/categories.html");
            addRoute("/customers", "customers", "/content/views/customers.html");
            addRoute("/products", "products", "/content/views/products.html");
            addRoute("/productEdit/:id", "productEdit", "/content/views/productEdit.html");

            router.start();
        });

We are also putting the Kendo UI Router to good use so that we can navigate between the different views in our SPA while syncing with the browser history so even though we are never refreshing, and our app never does any post backs, the user is still able to navigate back and forward using their browser buttons.

We can see that we are also using the Kendo FX API, this is to polish the UX in our SPA by adding slide transitions when swapping views. We are simply remotely loading the requested View (if it hasn’t already been loaded for the first time) from the server, sliding the #body div to the left, swapping the View with the requested one, sliding the #body div back it’s place (you can click on the live demo link to see this in action). If the view has already been loaded we cache the view in a dictionary, the next time this View is navigated to, we will check cache it it already exist then will load it from there, if not, we’ll load it from the server. Yes, we could have done the transitions this with a jQuery plug-in or some other tool or framework, however the intent was here was to accomplish this (along with Part 2 and 3 of this blog series) with the use of only framework, Kendo UI Web.

addRoute Helper


            var addRoute = function (route, name, path, forceRemoteLoad) {
                forceRemoteLoad = typeof forceRemoteLoad !== "undefined" ? forceRemoteLoad : false;
                router.route(route, function () {
                    kendo.fx($("#body")).slideInRight().reverse().then(function () { // transition, slide view left
                        var isRemotelyLoaded = false;
                        if (views[name] == null || forceRemoteLoad)
                        {   // check if we have already loaded in cache, could store this in browser local storage for larger apps
                            isRemotelyLoaded = true;
                            templateLoader.loadExtTemplate(name, path); // load the view
                            views[name] = new kendo.View(('#' + name)); // add the view to cache
                        }
                        layout.showIn("#body", views[name]); // switch view
                        $(document).trigger("viewSwtichedEvent", { route: route, name: name, path: path, isRemotelyLoaded: isRemotelyLoaded }); // publish event view has been loaded (EventAggregator pattern)
                        kendo.fx($("#body")).slideInRight().play(); // transition, slide view back to the right (center)
                    });
                });
            };

The addRoute is simply a helper method to register our routes and the with the Router along with the delegate we want invoke when navigating between our Views. Our delegate simply handles the animation transitions, switching out the Views and caching of our Views.

Now our next step is just converting the regular ASP.NET views to pure html which is pretty straight forward. For now, since MVC ignores routes for anything under /Content folder, we’ll just put our views in Spa/Content/views/, you can place them anywhere you see fit, and just have the MVC runtime ignore that path using the routes.IgnoreRoute([path-goes-here]) method in your RoutConfig.cs.

6-17-2013 10-41-19 PM

Spa/Content/views/layout.html


<script type="text/x-kendo-template" id="layout" highlight="31">
    <header>
        <div class="content-wrapper">
            <div class="float-left">
                <p class="site-title"><a href="/">CBRE Architecture & Design Team</a></p>
            </div>
            <div class="float-right">
                <section id="login">
                    <ul>
                        <li><a href="/Account/Register" id="registerLink">Register</a></li>
                        <li><a href="/Account/Login" id="loginLink">Log in</a></li>
                    </ul>
                </section>
                <nav>
                    <ul id="menu">
                        <li><a href="#/" >Home</a></li>
                        <li><a href="#/about" >About</a></li>
                        <li><a href="#/contact">Contact</a></li>
                    </ul>
                </nav>
                <nav>
                    <ul id="menu">
                        <li><a href="#/customers" >Customers</a></li>
                        <li><a href="#/categories" >Categories</a></li>
                        <li><a href="#/products" >Products</a></li>
                    </ul>
                </nav>
            </div>
        </div>
    </header>
    <div id="body">
    </div>
    <footer>
        <div class="content-wrapper">
            <div class="float-left">
                <p>&copy; 2013 - My ASP.NET MVC Application</p>
            </div>
        </div>
    </footer>
</script>

The layout.html View is loaded first, this is pretty much the layout of the application, all the Views will be loaded into the div:

<div d="body">

Spa/Content/views/home.html


<script type="text/x-kendo-template" id="home">
<section class="featured">
    <div class="content-wrapper">
        <hgroup class="title">
            <h1>Home Page.</h1>
            <h2>Modify this template to jump-start your ASP.NET MVC application.</h2>
        </hgroup>
        <p>
            To learn more about ASP.NET MVC visit
                <a href="http://asp.net/mvc" title="ASP.NET MVC Website">http://asp.net/mvc</a>.
                The page features <mark>videos, tutorials, and samples</mark> to help you get the most from ASP.NET MVC.
                If you have any questions about ASP.NET MVC visit
                <a href="http://forums.asp.net/1146.aspx/1?MVC" title="ASP.NET MVC Forum">our forums</a>.
        </p>
    </div>
</section>

<section class="content-wrapper main-content clear-fix">

    <h3>We suggest the following:</h3>
    <ol class="round">
        <li class="one">
            <h5>Getting Started</h5>
            ASP.NET MVC gives you a powerful, patterns-based way to build dynamic websites that
        enables a clean separation of concerns and that gives you full control over markup
        for enjoyable, agile development. ASP.NET MVC includes many features that enable
        fast, TDD-friendly development for creating sophisticated applications that use
        the latest web standards.
        <a href="http://go.microsoft.com/fwlink/?LinkId=245151">Learn more…</a>
        </li>

        <li class="two">
            <h5>Add NuGet packages and jump-start your coding</h5>
            NuGet makes it easy to install and update free libraries and tools.
        <a href="http://go.microsoft.com/fwlink/?LinkId=245153">Learn more…</a>
        </li>

        <li class="three">
            <h5>Find Web Hosting</h5>
            You can easily find a web hosting company that offers the right mix of features
        and price for your applications.
        <a href="http://go.microsoft.com/fwlink/?LinkId=245157">Learn more…</a>
        </li>
    </ol>

</section>
</script>
<script>
    var homeModel = new kendo.data.ObservableObject({});
</script>

Spa/Content/views/contact.html


<script type="text/x-kendo-template" id="contact">
<section class="content-wrapper main-content clear-fix">
    <hgroup class="title">
        <h1>Contact.</h1>
        <h2>Your contact page.</h2>
    </hgroup>

    <section class="contact">
        <header>
            <h3>Phone</h3>
        </header>
        <p>
            <span class="label">Main:</span>
            <span>425.555.0100</span>
        </p>
        <p>
            <span class="label">After Hours:</span>
            <span>425.555.0199</span>
        </p>
    </section>

    <section class="contact">
        <header>
            <h3>Email</h3>
        </header>
        <p>
            <span class="label">Support:</span>
            <span><a href="mailto:Support@example.com">Support@example.com</a></span>
        </p>
        <p>
            <span class="label">Marketing:</span>
            <span><a href="mailto:Marketing@example.com">Marketing@example.com</a></span>
        </p>
        <p>
            <span class="label">General:</span>
            <span><a href="mailto:General@example.com">General@example.com</a></span>
        </p>
    </section>

    <section class="contact">
        <header>
            <h3>Address</h3>
        </header>
        <p>
            One Microsoft Way<br />
            Redmond, WA 98052-6399
        </p>
    </section>
</section>
</script>
<script>
    var contactModel = new kendo.data.ObservableObject({});
</script>

Spa/Content/views/about.html


<script type="text/x-kendo-template" id="about">
<section class="content-wrapper main-content clear-fix">
    <hgroup class="title">
        <h1>About.</h1>
        <h2>Your app description page.</h2>
    </hgroup>

    <article>
        <p>
            Use this area to provide additional information.
        </p>

        <p>
            Use this area to provide additional information.
        </p>

        <p>
            Use this area to provide additional information.
        </p>
    </article>

    <aside>
        <h3>Aside Title</h3>
        <p>
            Use this area to provide additional information.
        </p>
        <ul>
            <li><a href="/">Home</a></li>
            <li><a href="/Home/About">About</a></li>
            <li><a href="/Home/Contact">Contact</a></li>
        </ul>
    </aside>
</section>
</script>
<script>
    var aboutModel = new kendo.data.ObservableObject({});
</script>

We see that each and every View is responsible for the Model that it will use and bind to, so every view will need to have a Model. The convention we are using here is name of View and viewModel, e.g. so if your View is name Product then it should have a model named ProductModel. The code in the SPA/Index.html will handle binding them together, we just need to follow the convention and everything else is handled.

For live demo: http://longle.azurewebsites.net

Download sample application: https://genericunitofworkandrepositories.codeplex.com

Stay tune for Part 2 and 3…

Part 2 – MVC 4, Web API, OData, EF, Kendo UI, Grid, Datasource with MVVM
Part 3 – MVC 4, Web API, OData, EF, Kendo UI, Binding a Form to Datasource (CRUD) with MVVM

Telerik’s HTML5 Kendo UI Web Validator Framework

I recently wrote a blog on Telerik’s Kendo UI Web MVVM Framework. This blog post was intended to illustrate the power of MVVM binding on the client side (declaritive binding).

In this blog post I wanted to demonstrate the ability to do some nice validation with the Kendo UI Web Validator Framework which also let’ you do a quite bit of validtion declaritively. With that beind said let’s get right into it using the MVVM form we created from the previous post.

Let’s take a quick at where we left off at on our last form.


    <div id="formContainer">
        <h1>
            My First MVVM Web View!</h1>
        <form id="myForm" action="">
        <label>
            First Name</label>
        <input type="text" data-bind="value: firstName, disabled:  isDisabled" />
        <label>
            Last Name</label>
        <input type="text" data-bind="value: lastName, disabled:  isDisabled" />
        <label>
            Email</label>
        <input type="text" data-bind="value: email, disabled:  isDisabled" />
        <label>
            Twitter</label>
        <input type="text" data-bind="value: twitter, disabled:  isDisabled" />
        <label>
            Site</label>
        <input type="text" data-bind="value: site, disabled:  isDisabled" />
        <label>
            Address</label>
        <input type="text" data-bind="value: address, disabled:  isDisabled" />
        <label>
            City</label>
        <input type="text" data-bind="value: city, disabled:  isDisabled" />
        <label>
            State</label>
        <input type="text" data-bind="value: state, disabled:  isDisabled" />
        <label>
            Zip</label>
        <input type="text" data-bind="value: zip, disabled:  isDisabled" />
        <label>
            Occupation</label>
        <select data-bind="source: occupations, value: occupation, disabled:  isDisabled">
        </select>
        <br />
        <br />
        <input type="button" value="Load" data-bind="click: load" />
        <input type="button" value="Edit" data-bind="click: edit" />
        <input type="button" value="Cancel" data-bind="click: cancel" />
        <input type="button" value="Reset" data-bind="click: reset" />
        </form>
    </div>

Now let’s add a couple of extra fields and declaritively add some validation to our form. While doing this let’s go ahead and change the types to our input controls with the appropriate HTML5 (http://www.w3schools.com/html5/html5_form_attributes.asp) types e.g. date, number, email, etc.. Quick note, notice that we are using the HTML5 [pattern] attribute for the input field named “Phone”.


    <div id="formContainer">
        <h1>
            My First MVVM Web View <br />
            now with Validation!</h1>
        <form id="myForm" action="">
        <label>
            First Name</label>
        <input type="text" name="firstName" data-bind="value: firstName, disabled:  isDisabled" required validationMessage="please enter a first name" />
        <label>
            Last Name</label>
        <input type="text" name="lastName" data-bind="value: lastName, disabled:  isDisabled" required validationMessage="please enter a last name" />
        <label>
            Email</label>
        <input type="email" name="email" data-bind="value: email, disabled:  isDisabled" required />
        <label>
            Twitter</label>
        <input type="url" name="twitter" data-bind="value: twitter, disabled:  isDisabled" required data-required-msg="please enter a {0}" data-url-msg="please enter a valid url" />
        <label>
            Site</label>
        <input type="url" name="site" data-bind="value: site, disabled:  isDisabled" required validationMessage="please enter a {0}" data-url-msg="please enter a valid url"/>
        <label>
            Address</label>
        <input type="text" name="address" data-bind="value: address, disabled:  isDisabled" required validationMessage="please enter a {0}" />
        <label>
            City</label>
        <input type="text" name="city" data-bind="value: city, disabled:  isDisabled" required validationMessage="please enter a {0}"/>
        <label>
            State</label>
        <input type="text" name="state" data-bind="value: state, disabled:  isDisabled" required validationMessage="please enter a {0}"/>
        <label>
            Zip</label>
        <input type="text" name="zip" data-bind="value: zip, disabled:  isDisabled" required validationMessage="please enter a {0}"/>
        <label>
            Phone</label>
        <input type="tel" name="phone" pattern="\d{10}"  data-bind="value: phone, disabled:  isDisabled" required data-required-msg="please enter a {0} number" data-pattern-msg="please enter a 10 digit phone number"/>
        <label>
            Lucky Number</label>
        <input type="number" name="luckynumber" data-bind="value: luckyNumber, disabled:  isDisabled" required data-required-msg="please enter a lucky number" data-max-msg="please enter a number less than 100" min="1" max="100" />
        <label>
            Birth Date</label>
        <input type="date" name="birthdate" data-bind="value: birthDate, disabled:  isDisabled" required validationMessage="please enter a {0}" />
        <label>
            Occupation</label>
        <select name="occupation" data-bind="source: occupations, value: occupation, disabled:  isDisabled" required valdationMessage="please select a {0}">
        </select>
        <br />
        <br />
        <input type="button" value="Load" data-bind="click: load" />
        <input type="button" value="Edit" data-bind="click: edit" />
        <input type="button" value="Cancel" data-bind="click: cancel" />
        <input type="button" value="Reset" data-bind="click: reset" />
        <button type="submit" value="Submit">Save</button>
        </form>
    </div>

Notice that all we are doing here is really providing the Validator Framework with is validation messages for the type of validation. There is a convention that we need to follow which is marking the control with the attribute: data-[rule]-msg, [rule] just needs to be replaced with actual rule name so for example data-required-msg=”please enter a luckly number” will be the message that is displayed when there is no number in the input field and data-max-message=”please enter a number less than 100″ will be the validation message that is shown if a user enters a number that is greater than 100. Now that all of our validation messages are setup let’s add our one line of script to get this all of our validations wired up (line 45).


    var myViewModel;
    $(document).ready(function () {
        myViewModel = kendo.observable({
            firstName: "Long",
            lastName: "Le",
            email: "lelong37@gmail.com",
            twitter: "twitter.com/lelong37",
            site: "blog.longle.net",
            address: "3737 Galatic Avenue",
            city: "Cloud City",
            state: "Texas",
            occupations: ["", "Hacker", "Jedi", "Ninja"],
            occupation: "Jedi",
            phone: "1111111111",
            luckyNumber: 34,
            birthDate: null,
            isSaved: false,
            isDisabled: true,
            edit: function (e) {
                this.set("isDisabled", false);
            },
            cancel: function (e) {
                this.set("isDisabled", true);
            },
            reset: function (e) {
                this.set("firstName", null);
                this.set("lastName", null);
                this.set("email", null);
                this.set("twitter", null);
                this.set("site", null);
                this.set("address", null);
                this.set("city", null);
                this.set("state", null);
                this.set("zip", null);
                this.set("occupation", "");
                this.set("phone", null);
                this.set("luckyNumber", null);
            },
            load: function (e) {
                LoadJohnDoesInfo();
            }
        });

        kendo.bind($("#myForm"), myViewModel);
        var validator = $("#myForm").kendoValidator().data("kendoValidator");
    });

Now with our declaritive validation and one line of Javascript to wire everything up let’s give our form a spin. Let’s clear out our form by and try saving by clicking [Edit], [Reset] and [Save].

Let’s test out our messsages that are custom to a specific validation type by typing in an invalid Url for our Twitter, Site and invalid email address when filling out our form and clicking [Save].

Notice how are validation messages are not the displaying the standard required messages but now they are validation messages specific to the type of validation. For example with the field “Lucky Number” the standard validation message was “please enter a lucky number” now that we actually typed in a value that is greater than the max attribute that was set to 100 we are getting the declartive validation message “please enter a number less than 100″. Also for the field [email] we also have typed in something however because we changed the input type to email (new with HTML5) out of the box the framework is displaying a standard message for us “email is not a valid email” even though we did not explicitly declare a validation message for email.

Now let’s fix all the fields with the correct values email, twitter, site, phone and lucky number and notice as we tab out of each field our validation messages dissapear and we are able to post the form.

Last but not least we can manually invoke the Validator by invoking the validate() method, let’s go ahead and rewire our click button to go ahead and validate our form as well as display a message to the user.


        $("button").click(function (e) {
            e.preventDefault();
            if (validator.validate()) {
                alert("Your form is golden!");
            } else {
                alert("Your form has errors!");
            }
        });

Let’s give this a try.

Happy Coding! :)

Download:

Telerik’s HTML5 Kendo UI Grid with Server Side Paging, Sorting & Filtering with MVC3, EF4 & Dynamic LINQ

Update: 06/18/2013 – It is recommended that you follow this post for Kendo UI Grid, Datasource filtering http://blog.longle.net/2013/06/18/mvc-4-web-api-odata-entity-framework-kendo-ui-grid-datasource-with-mvvm/

Update: 05/11/2012 – Added support for querying objects with child properties

Update: 04/24/2012 – Added recursion to the filters processing to support multiple search criterias on the same field while at the same time supporting searches across multiple fields.

I recently did a post Telerik’s HTML5 Kendo UI (Grid, Detail Template, TabStrip) which illustrated how to wire up their HTML5 Grid and handle server side paging. After doing so I quickly found myself needing to wire up the rest of server side bells and whistles e.g. sorting, filtering, etc.. Did some relentless googling and didn’t find any good resources on how to do this with MVC3 and EF4 so hence this blog post for the those of us that are doing just that. Rather than starting from scratch I’ll go ahead and continue where the my last blog left off.

So this first thing we need to do is configure our Kendo UI Grid for to do server side sorting and filtering so that we decompose what the requests pay loads look like coming from the Grid when performing these types of actions on it.

Configuring the Kendo UI Grid:


    $(document).ready(function () {
        var grid = $(&quot;#grid&quot;).kendoGrid({
            dataSource: {
                type: &quot;json&quot;,
                serverPaging: true,
                serverSorting: true,
                serverFiltering: true,
                allowUnsort: true,
                pageSize: 5,
                transport: {
                    read: {
                        url: &quot;Products/GetAll&quot;,
                        dataType: &quot;json&quot;,
                        type: &quot;POST&quot;,
                        contentType: &quot;application/json; charset=utf-8&quot;,
                        data: {}
                    },
                    parameterMap: function (options) {
                        return JSON.stringify(options);
                    }
                },
                schema: {
                    model: {
                        fields: {
                            ProductId: { type: &quot;number&quot; },
                            Name: { type: &quot;string&quot; },
                            Status: { type: &quot;string&quot; },
                            Created: { type: &quot;date&quot; }
                        }
                    },
                    data: &quot;Products&quot;,
                    total: &quot;TotalCount&quot;
                }
            },
            height: 700,
            sortable: true,
            groupable: true,
            pageable: true,
            filterable: true,
            columns: [
                    { field: &quot;ProductId&quot;, title: &quot;ProductId&quot; },
                    { field: &quot;ProductType&quot;, title: &quot;ProductType&quot; },
                    { field: &quot;Name&quot;, title: &quot;Name&quot; },
                    { field: &quot;Created&quot;, title: &quot;Created&quot;, format: &quot;{0:MM/dd/yyyy}&quot; }
                ],
            detailTemplate: kendo.template($(&quot;#template&quot;).html()),
            toolbar: kendo.template($(&quot;#toolBarTemplate&quot;).html()),
            detailInit: detailInit,
            dataBound: function () {
                this.expandRow(this.tbody.find(&quot;tr.k-master-row&quot;).first());
            }
        });

        var dropDown = grid.find(&quot;#requestType&quot;).kendoDropDownList({
            dataTextField: &quot;text&quot;,
            dataValueField: &quot;value&quot;,
            autoBind: false,
            optionLabel: &quot;All&quot;,
            dataSource: [
                    { text: &quot;Electronics&quot;, value: &quot;2&quot; },
                    { text: &quot;Machinery&quot;, value: &quot;1&quot; }
                ],
            change: function () {
                var value = this.value();
                if (value) {
                    grid.data(&quot;kendoGrid&quot;).dataSource.filter(
                        { field: &quot;ProductType&quot;, operator: &quot;eq&quot;, value: parseInt(value) });
                } else {
                    grid.data(&quot;kendoGrid&quot;).dataSource.filter({});
                }
            }
        });
    });

I’ve highlighted some of the major changes we made to our configuration which include setting up the Grid for server side actions: paging, sorting, filter, unsort and surfacing the filteration capabilities to the UI. Lines 54-72 is for setting up a Grid Toolbar which will contain a Kendo UI DrownDownList so that we can filter the Grid on ProductTypes which we will come back around to later on.

Now that we have the Grid configured for server side processing let’s take a quick look at what’s going down the wire in terms of pay loads for each of these actions so that we can mock up our models for these requests. When loading up IE Developer Tools (hit F12 or Tools > Developer Tools) and clicking on the Network Tab to start capturing network traffic we can see the actual pay load request for each of these actions.

So we can see that the pay load that is coming down the wire when a user performs a filter and sort on the grid is:


{&quot;take&quot;:5,&quot;skip&quot;:0,&quot;page&quot;:1,&quot;pageSize&quot;:5,&quot;group&quot;:[],&quot;filter&quot;:{&quot;filters&quot;:[{&quot;field&quot;:&quot;ProductType&quot;,&quot;operator&quot;:&quot;eq&quot;,&quot;value&quot;:&quot;3&quot;}],&quot;logic&quot;:&quot;and&quot;},&quot;sort&quot;:[{&quot;field&quot;:&quot;Name&quot;,&quot;dir&quot;:&quot;desc&quot;}]}

From this we can start mocking up our models needed for these types of Grid Actions for our Controller.


namespace MvcApplication3.Models
{
    public class GridFilter
    {
        public string Operator { get; set; }
        public string Field { get; set; }
        public string Value { get; set; }
    }

    public class GridFilters
    {
        public List&lt;GridFilter&gt; Filters { get; set; }
        public string Logic { get; set; }
    }

    public class GridSort
    {
        public string Field { get; set; }
        public string Dir { get; set; }
    }
}

Making changes to our Controller Action

We need to make changes to our existing Action on our Controller to support these new Grid objects that is being posted from our Grid when a user does a server side sort, filter, etc..


    public class ProductsController : Controller
    {
        [HttpPost]
        public JsonResult GetAll(int skip, int take, int page, int pageSize, 
            List&lt;GridSort&gt; sort = null, GridFilters filter = null)
        {
            var myDatabaseContext = new MyDatabaseContext();

            var products = myDatabaseContext.Products.AsQueryable();
            var totalCount = myDatabaseContext.Products.AsQueryable();

            if (filter != null &amp;&amp; (filter.Filters != null &amp;&amp; filter.Filters.Count &gt; 0))
            {
                string whereClause = null;
                var parameters = new List&lt;object&gt;();
                var filters = filter.Filters;

                for (var i = 0; i &lt; filters.Count; i++)
                {
                    if (i == 0)
                        whereClause += string.Format(&quot; {0}&quot;, 
                            BuildWhereClause&lt;Product&gt;(i, filter.Logic, filters[i], 
                            parameters));
                    else
                        whereClause += string.Format(&quot; {0} {1}&quot;, 
                            ToLinqOperator(filter.Logic), 
                            BuildWhereClause&lt;Product&gt;(i, filter.Logic, filters[i], 
                            parameters));
                }

                products = products.Where(whereClause, parameters.ToArray());
                totalCount = products.Where(whereClause, parameters.ToArray());
            }

            if (sort != null &amp;&amp; sort.Count &gt; 0)
                foreach (var s in sort)
                {
                    s.Field = (s.Field == &quot;ProductType&quot;) ? &quot;ProductTypeId&quot; : s.Field;
                    products = products.OrderBy(s.Field + &quot; &quot; + s.Dir);
                }

            products = products.Skip(skip).Take(take);

            List&lt;Product&gt; productList = products.ToList();

            var productViewModels = new List&lt;ProductViewModel.Product&gt;();

            foreach (var p in productList)
            {
                productViewModels.Add(new ProductViewModel.Product
                                            {
                                                Completed = p.Completed.Date,
                                                CompletedBy = p.CompletedBy,
                                                Created = p.Created.Date,
                                                CreatedBy = p.CreatedBy,
                                                Name = p.Name,
                                                ProductId = p.ProductId,
                                                ProductType = p.ProductType.Name,
                                                ProductDetails = p.ProductDetails,
                                                Status = p.Status,
                                                Updated = p.Updated.Date,
                                                UpdatedBy = p.UpdatedBy
                                            });
            }

            return Json(
                new ProductViewModel
                    {
                        Products = productViewModels,
                        TotalCount = totalCount.Count()
                    });
        }

        public static string BuildWhereClause&lt;T&gt;(int index, string logic, 
            GridFilter filter, List&lt;object&gt; parameters)
        {
            var entityType = (typeof(T));
            var property = entityType.GetProperty(filter.Field);

            switch (filter.Operator.ToLower())
            {
                case &quot;eq&quot;:
                case &quot;neq&quot;:
                case &quot;gte&quot;:
                case &quot;gt&quot;:
                case &quot;lte&quot;:
                case &quot;lt&quot;:
                    if (typeof(DateTime).IsAssignableFrom(property.PropertyType))
                    {
                        parameters.Add(DateTime.Parse(filter.Value).Date);
                        return string.Format(&quot;EntityFunctions.TruncateTime({0}){1}@{2}&quot;, 
                            filter.Field, 
                            ToLinqOperator(filter.Operator), 
                            index);
                    }
                    if (typeof(int).IsAssignableFrom(property.PropertyType))
                    {
                        parameters.Add(int.Parse(filter.Value));
                        return string.Format(&quot;{0}{1}@{2}&quot;, 
                            filter.Field, 
                            ToLinqOperator(filter.Operator), 
                            index);
                    }
                    parameters.Add(filter.Value);
                    return string.Format(&quot;{0}{1}@{2}&quot;, 
                        filter.Field, 
                        ToLinqOperator(filter.Operator), 
                        index);
                case &quot;startswith&quot;:
                    parameters.Add(filter.Value);
                    return string.Format(&quot;{0}.StartsWith(&quot; + &quot;@{1})&quot;, 
                        filter.Field, 
                        index);
                case &quot;endswith&quot;:
                    parameters.Add(filter.Value);
                    return string.Format(&quot;{0}.EndsWith(&quot; + &quot;@{1})&quot;, 
                        filter.Field, 
                        index);
                case &quot;contains&quot;:
                    parameters.Add(filter.Value);
                    return string.Format(&quot;{0}.Contains(&quot; + &quot;@{1})&quot;, 
                        filter.Field, 
                        index);
                default:
                    throw new ArgumentException(
                        &quot;This operator is not yet supported for this Grid&quot;, 
                        filter.Operator);
            }
        }

        public static string ToLinqOperator(string @operator)
        {
            switch (@operator.ToLower())
            {
                case &quot;eq&quot;: return &quot; == &quot;;
                case &quot;neq&quot;: return &quot; != &quot;;
                case &quot;gte&quot;: return &quot; &gt;= &quot;;
                case &quot;gt&quot;: return &quot; &gt; &quot;;
                case &quot;lte&quot;: return &quot; &lt;= &quot;;
                case &quot;lt&quot;: return &quot; &lt; &quot;;
                case &quot;or&quot;: return &quot; || &quot;;
                case &quot;and&quot;: return &quot; &amp;&amp; &quot;;
                default: return null;
            }
        }

        public JsonResult GetProductDetails(int skip, int take, int page, 
            int pageSize, string group)
        {
            var myDatabaseContext = new MyDatabaseContext();

            var productDetails = myDatabaseContext.ProductDetails
                .OrderBy(p =&gt; p.ProducDetailtId);

            return Json(
                new ProductDetailsViewModel
                    {
                        ProductDetails = productDetails.Skip(skip).Take(take),
                        TotalCount = productDetails.Count()
                    },
                JsonRequestBehavior.AllowGet);
        }
    }

Note: Instead of downloading the LINQ Dynamic Query Library, you may want to actually download the sample application for this post because the DynamicQueryable.cs class from the Linq Dynamic Libray has been slightly modified to handle EntityFunctions to support string search actions from our Grid such as Contains, StartsWidth and EndsWith string searches.

A few quick notes in regards to our changes to our Action on our Controller to now support complete server side processing of paging, sorting and filtering.

  • BuildWhereClause<T>(int index, string logic, GridFilter filter, List parameters)
    This helper method will build our our where clauses and predicates so tha we can chain them up and pass them into Dynamic LINQ.

  • ToLinqOperator(string @operator)

    This helper method will convert operators that are sent from our Grid to C# operators that Dynamic LINQ will understand and convert them for us

  • Lines 48-64, here we are iterating through the results to trim off the timestamp off of any properties that are of type datetime, so that when we do any grouping or filtering from the grid the timestamp of these fields are ignored.

                foreach (var p in productList)
                {
                    productViewModels.Add(new ProductViewModel.Product
                                                {
                                                    Completed = p.Completed.Date,
                                                    CompletedBy = p.CompletedBy,
                                                    Created = p.Created.Date,
                                                    CreatedBy = p.CreatedBy,
                                                    Name = p.Name,
                                                    ProductId = p.ProductId,
                                                    ProductType = p.ProductType.Name,
                                                    ProductDetails = p.ProductDetails,
                                                    Status = p.Status,
                                                    Updated = p.Updated.Date,
                                                    UpdatedBy = p.UpdatedBy
                                                });
                }
    
  • Lines 88-103, here we are checking against the type of the column (property) that we are searching against so that we can convert the search criteria to the appropriate type. Currently we are supporting searches against types of string, datetime and int. If you need to add more types simply enhance this section of the implementation.

                        if (typeof(DateTime).IsAssignableFrom(property.PropertyType))
                        {
                            parameters.Add(DateTime.Parse(filter.Value).Date);
                            return string.Format(&quot;EntityFunctions.TruncateTime({0}){1}@{2}&quot;, 
                                filter.Field, 
                                ToLinqOperator(filter.Operator), 
                                index);
                        }
                        if (typeof(int).IsAssignableFrom(property.PropertyType))
                        {
                            parameters.Add(int.Parse(filter.Value));
                            return string.Format(&quot;{0}{1}@{2}&quot;, 
                                filter.Field, 
                                ToLinqOperator(filter.Operator), 
                                index);
                        }
                        parameters.Add(filter.Value);
    
    
  • Lines 109-123, here we are just framing up the different queries for string searches from the Grid. The Grid supports StartsWith, Contains, and EndsWith.

                    case &quot;startswith&quot;:
                        parameters.Add(filter.Value);
                        return string.Format(&quot;{0}.StartsWith(&quot; + &quot;@{1})&quot;, 
                            filter.Field, 
                            index);
                    case &quot;endswith&quot;:
                        parameters.Add(filter.Value);
                        return string.Format(&quot;{0}.EndsWith(&quot; + &quot;@{1})&quot;, 
                            filter.Field, 
                            index);
                    case &quot;contains&quot;:
                        parameters.Add(filter.Value);
                        return string.Format(&quot;{0}.Contains(&quot; + &quot;@{1})&quot;, 
                            filter.Field, 
                            index);
    

    As you can see in the screenshot right below these are the current string search capabilites that the Grid has.

Great, let’s run a few searches from the Grid now.

  • Search on ProductId: 3

  • Search on Created Date: 04/20/2013

  • Search on Created Date >= 04/15/2012 and Name containing “sample product 3″

    Voila! Our controller only returns 3 records which assert our test case and are all of Created date >= 04/20/2012 and all contain the string “sample product 3″ in the name.

Update: 04/24/2012

Added recursion to the filters processing to support multiple search criterias on the same field while at the same time supporting searches across multiple fields.

 

        [HttpPost]
        public JsonResult GetAll(int skip, int take, int page, int pageSize, 
            List&lt;GridSort&gt; sort = null, GridFilter filter = null)
        {
            var myDatabaseContext = new MyDatabaseContext();

            var products = myDatabaseContext.Products.AsQueryable();
            var totalCount = myDatabaseContext.Products.AsQueryable();

            if (filter != null &amp;&amp; (filter.Filters != null &amp;&amp; filter.Filters.Count &gt; 0))
            {
                ProcessFilters(filter, ref products);
                totalCount = products;
            }

            if (sort != null &amp;&amp; sort.Count &gt; 0)
                foreach (var s in sort)
                {
                    s.Field = (s.Field == &quot;ProductType&quot;) ? &quot;ProductTypeId&quot; : s.Field;
                    products = products.OrderBy(s.Field + &quot; &quot; + s.Dir);
                }

            products = products.Skip(skip).Take(take);

            List&lt;Product&gt; productList = products.ToList();

            var productViewModels = new List&lt;ProductViewModel.Product&gt;();

            foreach (var p in productList)
            {
                productViewModels.Add(new ProductViewModel.Product
                                            {
                                                Completed = p.Completed.Date,
                                                CompletedBy = p.CompletedBy,
                                                Created = p.Created.Date,
                                                CreatedBy = p.CreatedBy,
                                                Name = p.Name,
                                                ProductId = p.ProductId,
                                                ProductType = p.ProductType.Name,
                                                ProductDetails = p.ProductDetails,
                                                Status = p.Status,
                                                Updated = p.Updated.Date,
                                                UpdatedBy = p.UpdatedBy
                                            });
            }

            return Json(
                new ProductViewModel
                    {
                        Products = productViewModels,
                        TotalCount = totalCount.Count()
                    });
        }

        public static void ProcessFilters(GridFilter filter, ref IQueryable&lt;Product&gt; queryable)
        {
            var whereClause = string.Empty;
            var filters = filter.Filters;
            var parameters = new List&lt;object&gt;();
            for (int i = 0; i &lt; filters.Count; i++)
            {
                var f = filters[i];

                if (f.Filters == null)
                {
                    if (i == 0)
                        whereClause += BuildWhereClause&lt;Product&gt;(f, i, parameters) + &quot; &quot;;
                    if (i != 0)
                        whereClause += ToLinqOperator(filter.Logic) + 
                            BuildWhereClause&lt;Product&gt;(f, i, parameters) + &quot; &quot;;
                    if (i == (filters.Count - 1))
                    {
                        CleanUp(ref whereClause);
                        queryable = queryable.Where(whereClause, parameters.ToArray());
                    }
                }
                else
                    ProcessFilters(f, ref queryable);
            }
        }

Looks like our server side paging, sorting and filteration is golden!

Update: 05/11/2012- Added support for querying objects with child properties


    public static class GridHelper
    {
        public static void ProcessFilters&lt;T&gt;(GridFilter filter, ref IQueryable&lt;T&gt; queryable)
        {
            var whereClause = string.Empty;
            var filters = filter.Filters;
            var parameters = new List&lt;object&gt;();
            for (int i = 0; i &lt; filters.Count; i++)
            {
                var f = filters[i];

                if (f.Filters == null)
                {
                    if (i == 0)
                        whereClause += BuildWherePredicate&lt;T&gt;(f, i, parameters) + &quot; &quot;;
                    if (i != 0)
                        whereClause += ToLinqOperator(filter.Logic) + BuildWherePredicate&lt;T&gt;(f, i, parameters) + &quot; &quot;;
                    if (i == (filters.Count - 1))
                    {
                        TrimWherePredicate(ref whereClause);
                        queryable = queryable.Where(whereClause, parameters.ToArray());
                    }
                }
                else
                    ProcessFilters(f, ref queryable);
            }
        }

        public static string TrimWherePredicate(ref string whereClause)
        {
            switch (whereClause.Trim().Substring(0, 2).ToLower())
            {
                case &quot;&amp;&amp;&quot;:
                    whereClause = whereClause.Trim().Remove(0, 2);
                    break;
                case &quot;||&quot;:
                    whereClause = whereClause.Trim().Remove(0, 2);
                    break;
            }

            return whereClause;
        }

        public static string BuildWherePredicate&lt;T&gt;(GridFilter filter, int index, List&lt;object&gt; parameters)
        {
            var entityType = (typeof(T));
            PropertyInfo property;
            
            if(filter.Field.Contains(&quot;.&quot;))
                property = GetNestedProp&lt;T&gt;(filter.Field);
            else 
                property = entityType.GetProperty(filter.Field);
            
            var parameterIndex = parameters.Count;

            switch (filter.Operator.ToLower())
            {
                case &quot;eq&quot;:
                case &quot;neq&quot;:
                case &quot;gte&quot;:
                case &quot;gt&quot;:
                case &quot;lte&quot;:
                case &quot;lt&quot;:
                    if (typeof(DateTime).IsAssignableFrom(property.PropertyType))
                    {
                        parameters.Add(DateTime.Parse(filter.Value).Date);
                        return string.Format(&quot;EntityFunctions.TruncateTime(&quot; + filter.Field + &quot;)&quot; + ToLinqOperator(filter.Operator) + &quot;@&quot; + parameterIndex);
                    }
                    if (typeof(int).IsAssignableFrom(property.PropertyType))
                    {
                        parameters.Add(int.Parse(filter.Value));
                        return string.Format(filter.Field + ToLinqOperator(filter.Operator) + &quot;@&quot; + parameterIndex);
                    }
                    parameters.Add(filter.Value);
                    return string.Format(filter.Field + ToLinqOperator(filter.Operator) + &quot;@&quot; + parameterIndex);
                case &quot;startswith&quot;:
                    parameters.Add(filter.Value);
                    return filter.Field + &quot;.StartsWith(&quot; + &quot;@&quot; + parameterIndex + &quot;)&quot;;
                case &quot;endswith&quot;:
                    parameters.Add(filter.Value);
                    return filter.Field + &quot;.EndsWith(&quot; + &quot;@&quot; + parameterIndex + &quot;)&quot;;
                case &quot;contains&quot;:
                    parameters.Add(filter.Value);
                    return filter.Field + &quot;.Contains(&quot; + &quot;@&quot; + parameterIndex + &quot;)&quot;;
                default:
                    throw new ArgumentException(&quot;This operator is not yet supported for this Grid&quot;, filter.Operator);
            }
        }

        public static string ToLinqOperator(string @operator)
        {
            switch (@operator.ToLower())
            {
                case &quot;eq&quot;:
                    return &quot; == &quot;;
                case &quot;neq&quot;:
                    return &quot; != &quot;;
                case &quot;gte&quot;:
                    return &quot; &gt;= &quot;;
                case &quot;gt&quot;:
                    return &quot; &gt; &quot;;
                case &quot;lte&quot;:
                    return &quot; &lt;= &quot;;
                case &quot;lt&quot;:
                    return &quot; &lt; &quot;;
                case &quot;or&quot;:
                    return &quot; || &quot;;
                case &quot;and&quot;:
                    return &quot; &amp;&amp; &quot;;
                default:
                    return null;
            }
        }

        public static PropertyInfo GetNestedProp&lt;T&gt;(String name)
        {
            PropertyInfo info = null;
            var type = (typeof(T));
            foreach(var prop in name.Split('.'))
            {
                info = type.GetProperty(prop);
                type = info.PropertyType;
            }
            return info;
        }
    }

Happy Coding…! :)

Download sample application: https://skydrive.live.com/redir.aspx?cid=949a1c97c2a17906&resid=949A1C97C2A17906!465&parid=949A1C97C2A17906!361

Setting Telerik’s HTML5 Kendo UI Grid’s Height to 100% when Binding to Remote Data

Update 06/24/2013 – Telerik, Kendo UI Team (Dimo Dimov) has reached out to me (see block quote below), and has taken the time to provide the best practice on how to handle the 100% height for the Kendo UI Web Grid http://jsfiddle.net/dimodi/SDFsz/, please use this approach vs. the approach in the blog post, thanks for visiting…!

I am Dimo Dimov from the Telerik / Kendo UI team. A client recently provided this page as an example of a 100% high Kendo UI Grid. I reviewed the code and the comments and it seems that a lot of people would benefit from some clarifications how percentage heights work.

First of all, please take a look at the following jsFiddle demo, which is similar to the jsFiddle demo above, but is better and simpler in a couple of ways:

http://jsfiddle.net/dimodi/SDFsz/

The example discussed in this blog post is also similar, but has one drawback – it uses hard-coded “magic numbers” in order to workaround the presumably disobeyed web standard requirement regarding percentage heights. Let me explain this requirement in more detail using the new jsFiddle demo as a starting point.

We have the following DOM tree:

First of all, the rule of thumb is that an element with a percentage height should have a parent with explicit height. The rule applies recursively until an element with a pixel height is reached, or until the HTML element is reached, which should have a height as well. This is because percentage heights are always calculated with regard to the set height of the parent element.

Now let’s get back to our example. The Grid should be 100% high and we set such a style. This implies that the parent element () should have an explicit pixel or percentage height. It has none of those, so we apply a 100% height to it. Now we go up and reach the element. It should also have an explicit height, otherwise the and Grid percentage heights will be ignored. So we apply a 100% height to the element as well. Now we have three nested elements that are 100% high and these heights start working. Since 100% high elements should not have vertical borders, margins or paddings, we remove those, as shown in the jsFiddle demo.

With more complex DOM trees the logic is the same – you start with the given element that needs to expand and go up the tree and apply 100% height styles (or any other values) until an element with a pixel height is reached, or until the element is reached. If you have a sequence of nested 100% high elements, removing just one of those heights will cause all the others to stop working. The most common reason for 100% heights to not work is that there is an element with no height, which breaks the sequence of 100% high nested elements.

One thing more to keep in mind is that 100% high elements cannot have siblings, otherwise they will overflow their parent.

This blog post uses the opposite approach compared to the one I describe – the height is calculated from “the outside to the inside”. The only case, in which such an approach makes sense is when the Grid or some of its parent elements have siblings, so 100% heights cannot be applied that easily. Also, this approach can work with parent elements with missing heights, because it relies on the height of the browser window, but I would not recommend this appoach.

Thank you.

I’ve been somewhat wresting with how to set the KendoUI Grid’s height to 100% so that it could fill a user’s screen as much as possible so that it would only show it’s vertical scrollbar only when absolutely needed e.g. when a user’s actual browser real estate is shorter in height than the grid’s content (rows). Those of us that have wired up the Grid binding to remote data e.g. OAuth, JSON, etc.. have been required to explicitly set the Grid’s height.

I’ve visited the Telerik’s forums and googled, unfortunately no working real solution, hence this blog post.

http://www.kendoui.com/forums/ui/grid/dynamic-grid-height.aspx
http://jsfiddle.net/dimodi/4eNu4/33/

If we don’t set the height, the Grid will render as follows:

Note: No height was set for the Grid configuration and it appears as if the Grid has no records when indeed our service is returning records it’s just the Grid’s content div’s height is 0px because we did not explicity set the height, and if we try to set it to a percent 100%, the Grid does not render at all.


    $(document).ready(function () {
        $(&quot;#grid&quot;).kendoGrid({
            dataSource: {
                type: &quot;json&quot;,
                serverPaging: true,
                pageSize: 5,
                transport: { read: { url: &quot;Products/GetAll&quot;, dataType: &quot;json&quot;} },
                schema: { data: &quot;Products&quot;, total: &quot;TotalCount&quot; }
            },
            pageable: true,
            columns: [
                    { field: &quot;ProductId&quot;, title: &quot;ProductId&quot; },
                    { field: &quot;ProductType&quot;, title: &quot;ProductType&quot; },
                    { field: &quot;Name&quot;, title: &quot;Name&quot; },
                    { field: &quot;Created&quot;, title: &quot;Created&quot; }
                ],
            detailTemplate: kendo.template($(&quot;#template&quot;).html()), detailInit: detailInit,
            dataBound: function () {
                this.expandRow(this.tbody.find(&quot;tr.k-master-row&quot;).first());
            }
        });
    });

If explicity set the height, the Grid will render as follows:

Note: Line 10, where we are explicitly setting the height, however notice that we have plenty of screen real estate under grid, and if we could get the Grid could take advantage of that empty space we wouldn’t need the scrollbar.


    $(document).ready(function () {
        $(&quot;#grid&quot;).kendoGrid({
            dataSource: {
                type: &quot;json&quot;,
                serverPaging: true,
                pageSize: 5,
                transport: { read: { url: &quot;Products/GetAll&quot;, dataType: &quot;json&quot;} },
                schema: { data: &quot;Products&quot;, total: &quot;TotalCount&quot; }
            },
            height: 500,
            pageable: true,
            columns: [
                    { field: &quot;ProductId&quot;, title: &quot;ProductId&quot; },
                    { field: &quot;ProductType&quot;, title: &quot;ProductType&quot; },
                    { field: &quot;Name&quot;, title: &quot;Name&quot; },
                    { field: &quot;Created&quot;, title: &quot;Created&quot; }
                ],
            detailTemplate: kendo.template($(&quot;#template&quot;).html()), detailInit: detailInit,
            dataBound: function () {
                this.expandRow(this.tbody.find(&quot;tr.k-master-row&quot;).first());
            }
        });
    });

So a quick way we can remedy this is to calculate how much vertical space we have and set the maximum height of the Grid ourselves. By taking the Html document height and offsetting it factoring in our layout e.g. header and footer.


    $(document).ready(function () {
        $(&quot;#grid&quot;).kendoGrid({
            dataSource: {
                type: &quot;json&quot;,
                serverPaging: true,
                pageSize: 5,
                transport: { read: { url: &quot;Products/GetAll&quot;, dataType: &quot;json&quot;} },
                schema: { data: &quot;Products&quot;, total: &quot;TotalCount&quot; }
            },
            height: $(document).height() - 350,
            pageable: true,
            columns: [
                    { field: &quot;ProductId&quot;, title: &quot;ProductId&quot; },
                    { field: &quot;ProductType&quot;, title: &quot;ProductType&quot; },
                    { field: &quot;Name&quot;, title: &quot;Name&quot; },
                    { field: &quot;Created&quot;, title: &quot;Created&quot; }
                ],
            detailTemplate: kendo.template($(&quot;#template&quot;).html()), detailInit: detailInit,
            dataBound: function () {
                this.expandRow(this.tbody.find(&quot;tr.k-master-row&quot;).first());
            }
        });
    });

Great! Our Grid now renders and takes up the full vertical real estate within the browser on the intial load

Last Problem, now when we resize the browswer, the Grid is NOT automatically resizing itself

No problem, we will add a method for resizing the grid and wire this up to the browser resize event with jQuery.


  $(document).ready(function () {
        $(&quot;#grid&quot;).kendoGrid({
            dataSource: {
                type: &quot;json&quot;,
                serverPaging: true,
                pageSize: 5,
                transport: { read: { url: &quot;Products/GetAll&quot;, dataType: &quot;json&quot;} },
                schema: { data: &quot;Products&quot;, total: &quot;TotalCount&quot; }
            },
            height: 250,
            pageable: true,
            columns: [
                    { field: &quot;ProductId&quot;, title: &quot;ProductId&quot; },
                    { field: &quot;ProductType&quot;, title: &quot;ProductType&quot; },
                    { field: &quot;Name&quot;, title: &quot;Name&quot; },
                    { field: &quot;Created&quot;, title: &quot;Created&quot; }
                ],
            detailTemplate: kendo.template($(&quot;#template&quot;).html()), detailInit: detailInit,
            dataBound: function () {
                this.expandRow(this.tbody.find(&quot;tr.k-master-row&quot;).first());
            }
        });
    });
    
    function resizeGrid() {
        var gridElement = $(&quot;#grid&quot;);
        var dataArea = gridElement.find(&quot;.k-grid-content&quot;);

        var newGridHeight = $(document).height() - 350;
        var newDataAreaHeight = newGridHeight - 65;

        dataArea.height(newDataAreaHeight);
        gridElement.height(newGridHeight);

        $(&quot;#grid&quot;).data(&quot;kendoGrid&quot;).refresh();
    }
    
    $(window).resize(function () {
        resizeGrid();
    });

Now, when grid first loads

Note: Looks great, Grid occupies all the available vertical real estate it can

When user resizes Grid (maximizes browser vertically)

Note: Still Looks great, Grid automatically resizes itself to occupy the new vertical space within the browser after the resize.

Happy Coding…!

Download sample application:

https://skydrive.live.com/redir.aspx?cid=949a1c97c2a17906&resid=949A1C97C2A17906!375&parid=949A1C97C2A17906!361