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, Web API, OData, Entity Framework, Kendo UI, Grid, Datasource (CRUD) with MVVM

This will be part four 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 sourcecode 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/18/2013 – Added CRUD actions to Kendo UI Grid & Datasource (read, update, delete) updated sample download and live demo.

Update: 06/20/2013 – Bug fix(es): Fixed View being loaded duplicate times. Enhancement(s): 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

Let’s start off where we left off from my previous blog MVC 4, Kendo UI, SPA with Layout, View, Router & MVVM – Part 1. In this post, we’ll cover how to wire up a Kendo UI Grid and Datasource in our SPA with MVVM using OData.

This will be part two 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.

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

Let’s get Web API setup and configured with OData, you will need the Nuget package Microsoft ASP.NET Web API OData.

6-18-2013 1-32-31 AM

We will use a SQL Server Compact (4.0) Northwind database for this example, you can easily use a full SQL Server Database if you’d like with libraries in this project. I’ve tested them both and they work fine. Both the Northwind SQL Compact and SQL Server database are included in the sample download application that will be available for download in Part 3 of this series. For all of those who are wondering, why did I use (embedded) SQL Server Compact for as web app..?! Well, because I can host the SQL Server Compact database in my Windows Azure Website for free..!

You will need the NuGet package EntityFramework.SqlServerCompact and Microsoft.SqlServerCompact packages. You can read up on some more in-depth details on how to setup ASP.NET MVC with SQL Server Compact Databases here.

6-18-2013 1-38-54 AM

We need to create a ProductController so that we can provide data to our View, this controller will inherit the EntitySetController which inherits the ApiController that we all know so well, so we can serve up our data using OData.

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);
        }
    }

We need to setup OData with the MVC runtime as well as setup the OData endpoint. Not relevant to this post, however, notice that our OData Products Web Api Controller has a dependency for IUnitOfWork and that it is being injected (DI) with an instance of it’s concrete implementation UnitOfWork, courtesy of Unity 3.0. Please read up on post: Generically Implementing the Unit of Work & Repository Pattern with Entity Framework in MVC & Simplifying Entity Graphs, if any clarity is needed for the generic Unit Of Work and Repository pattern used in the ProductController seen here.

Spa.WebApiConfig



    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
            var entitySetConfiguration = modelBuilder.EntitySet<Product>("Product");
            entitySetConfiguration.EntityType.Ignore(t => t.Order_Details);
            entitySetConfiguration.EntityType.Ignore(t => t.Category);
            entitySetConfiguration.EntityType.Ignore(t => t.Supplier);

            var model = modelBuilder.GetEdmModel();
            config.Routes.MapODataRoute("ODataRoute", "odata", model);

            config.EnableQuerySupport();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new {id = RouteParameter.Optional}
                );
        }
    }

For interest of time we’ll go ahead an ignore all the relational mappings that our Product entity has.

Now let’s test our ProductsController with Fiddler and makes sure everything we’re able to query our Get method on our ProductController using OData queries.

6-18-2013 12-11-03 AM

The raw HTTP response message should look similar to the following:

http://localhost:29622/odata


HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/atomsvc+xml; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?RDpcVXNlcnNcbGxlXERvd25sb2Fkc1xUZW1wMlxTb2x1dGlvblxTcGFcb2RhdGE=?=
X-Powered-By: ASP.NET
Date: Tue, 18 Jun 2013 05:09:41 GMT
Content-Length: 363

<?xml version="1.0" encoding="utf-8"?>
<service xml:base="http://localhost:29622/odata/" xmlns="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom">
  <workspace>
    <atom:title type="text">Default</atom:title>
    <collection href="Product">
      <atom:title type="text">Product</atom:title>
    </collection>
  </workspace>
</service>

The response body contains the OData service document in JSON format. The service document contains an array of JSON objects that represent the entity sets. In this case, there is a single entity set, “Product”. To query this set, send a GET request to http://localhost:port/odata/Products. The
response should be similar to the following:

http://localhost:29622/odata/Product


HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; odata=minimalmetadata; streaming=true; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?RDpcVXNlcnNcbGxlXERvd25sb2Fkc1xUZW1wMlxTb2x1dGlvblxTcGFcb2RhdGFcUHJvZHVjdA==?=
X-Powered-By: ASP.NET
Date: Tue, 18 Jun 2013 05:18:27 GMT
Content-Length: 21696

{
  "odata.metadata":"http://localhost:29622/odata/$metadata#Product","value":[
    {
      "ProductID":1,"ProductName":"Chai","EnglishName":"Dharamsala Tea","SupplierID":1,"CategoryID":1,"QuantityPerUnit":"10 boxes x 20 bags","UnitPrice":"20","UnitsInStock":39,"UnitsOnOrder":10,"ReorderLevel":10,"Discontinued":false,"State":"Unchanged"
    },{
      "ProductID":2,"ProductName":"Chang","EnglishName":"Tibetan Barley Beer","SupplierID":1,"CategoryID":1,"QuantityPerUnit":"24 - 12 oz bottles","UnitPrice":"19","UnitsInStock":17,"UnitsOnOrder":40,"ReorderLevel":25,"Discontinued":false,"State":"Unchanged"
    },{
      "ProductID":3,"ProductName":"Aniseed Syrup","EnglishName":"Licorice Syrup","SupplierID":1,"CategoryID":2,"QuantityPerUnit":"12 - 550 ml bottles","UnitPrice":"10","UnitsInStock":13,"UnitsOnOrder":71,"ReorderLevel":25,"Discontinued":false,"State":"Unchanged"
    },{
      "ProductID":4,"ProductName":"Chef Anton's Cajun Seasoning","EnglishName":"Chef Anton's Cajun Seasoning","SupplierID":2,"CategoryID":2,"QuantityPerUnit":"48 - 6 oz jars","UnitPrice":"22","UnitsInStock":53,"UnitsOnOrder":0,"ReorderLevel":0,"Discontinued":false,"State":"Unchanged"
    },{
      "ProductID":5,"ProductName":"Chef Anton's Gumbo Mix","EnglishName":"Chef Anton's Gumbo Mix","SupplierID":2,"CategoryID":2,"QuantityPerUnit":"36 boxes","UnitPrice":"21.35","UnitsInStock":0,"UnitsOnOrder":0,"ReorderLevel":0,"Discontinued":true,"State":"Unchanged"
    },{
      "ProductID":6,"ProductName":"Grandma's Boysenberry Spread","EnglishName":"Grandma's Boysenberry Spread","SupplierID":3,"CategoryID":2,"QuantityPerUnit":"12 - 8 oz jars","UnitPrice":"25","UnitsInStock":120,"UnitsOnOrder":0,"ReorderLevel":25,"Discontinued":false,"State":"Unchanged"
    },{
      "ProductID":7,"ProductName":"Uncle Bob's Organic Dried Pears","EnglishName":"Uncle Bob's Organic Dried Pears","SupplierID":3,"CategoryID":7,"QuantityPerUnit":"12 - 1 lb pkgs.","UnitPrice":"30","UnitsInStock":15,"UnitsOnOrder":0,"ReorderLevel":10,"Discontinued":false,"State":"Unchanged"
    },{
      "ProductID":8,"ProductName":"Northwoods Cranberry Sauce","EnglishName":"Northwoods Cranberry Sauce","SupplierID":3,"CategoryID":2,"QuantityPerUnit":"12 - 12 oz jars","UnitPrice":"40","UnitsInStock":6,"UnitsOnOrder":0,"ReorderLevel":0,"Discontinued":false,"State":"Unchanged"
    }
  ]
}

Great, we have data with OData :P

One of the important take aways here, is by implementing OData with a data provider (e.g. Entity Framework) that supports IQueryable all of our REST HTTP GET queries options (e.g. skip, take, sort, filter, equals, etc.) are automatically translated for to us to Entity Framework, so that we don’t have to wire up any of this.

Let’s launch Fiddler again and see this in action and do a OData HTTP GET request querying for product that has the name equal to “Chai”.

http://localhost:29622/odata/Product?%24inlinecount=allpages&%24top=10&%24filter=ProductName+eq+’chai&#8217;

6-18-2013 1-26-12 PM

Raw Request of OData Query


GET http://localhost:29622/odata/Product?%24inlinecount=allpages&%24top=10&%24filter=ProductName+eq+'chai' HTTP/1.1
X-Requested-With: XMLHttpRequest
Accept: application/json, text/javascript, */*; q=0.01
Referer: http://localhost:29622/index.html#/products
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0)
Host: localhost:29622
DNT: 1
Connection: Keep-Alive

Raw Request of OData Response


HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
DataServiceVersion: 3.0
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?RDpcVXNlcnNcbGxlXERvd25sb2Fkc1xUZW1wMlxTb2x1dGlvblxTcGFcb2RhdGFcUHJvZHVjdA==?=
X-Powered-By: ASP.NET
Date: Tue, 18 Jun 2013 18:22:02 GMT
Content-Length: 374

{
  "odata.metadata":"http://localhost:29622/odata/$metadata#Product","odata.count":"1","value":[
    {
      "ProductID":1,"ProductName":"Chai","EnglishName":"Dharamsala Tea","SupplierID":1,"CategoryID":1,"QuantityPerUnit":"10 boxes x 20 bags","UnitPrice":"20","UnitsInStock":39,"UnitsOnOrder":10,"ReorderLevel":10,"Discontinued":false,"State":"Unchanged"
    }
  ]
}

Again, querying is navtively supported out of the box with OData and a data provider that supports IQueryable, however if we re-visit our ProductController, we didn’t have to write up any code for this..!

Spa.Controllers.ProductController


    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);
        }
    }

Now we create a View for the Products listing using Kendo UI Grid, Datasource and use MVVM as the adhesive to bind it all together.

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>

We wrap our html content in the script tags with type=”text/x-kendo-template”, this is so we can leverage all the goodness that Kendo UI Web Templates bring to the table.

productGrid (Spa/Content/Views/products.html)


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

Model & Datasource (Spa/Content/Views/products.html)


    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;
        }
    });

Notice how there is very little, if any, of our own code here. All we’ve done, is simply configured (filling in the blanks) the Kendo Datasource.

The important items to note here is that by default our OData enabled ProductController returns the count as data.odata.count and our data set in data.value, with this being the case we will need to help the Datasource by unpacking this and returning it to the Datasource. You can see how this is done in the schema data and total functions defined above.

We also define the model in the Datasource, the model inherits Kendo’s Observable object (class), meaning their is two-way binding with the Grid and Datasource, so anytime something happens on the Grid updates are automatically sent to the Datasource, then sent to our ProductsController.

The productGrid is declaratively attributed using the data- attributes, this is what Kendo UI MVVM uses for binding the View to the Model.

Subscribing to the viewSwitchedEvent Event


    $(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
            }
        }
    });

Here we are simply subscribing to viewSwitchedEvent that is published from the host page (Spa\index.html) of our SPA. This event is published (raised) everytime is View switching is complete. Here we check that the View that was switched in place was indeed the products View and that it was the first time is was loaded remotely from the server, if so, we bind the View to the Model. We do this only on the first time it loads from the server because there is really no need to do this more than once.

Now let’s load up the application and see our Products listing.

6-18-2013 3-01-38 PM

Testing Filtering and Sorting

6-18-2013 3-01-27 PM

Testing Inline Grid Editing

6-18-2013 6-31-01 PM

Testing Inline Grid Deleting

6-18-2013 6-57-55 PM

6-18-2013 6-55-22 PM

We see this error:

An error has occurred.

The primary key value cannot be deleted because references to this key still exist. [ Foreign key constraint name = Order Details_FK00 ]

Which is expected since the Order Details still has foreign keys to the Product table.

There you have it MVC 4, Web API, OData, Kendo UI, Grid, Datasource with MVVM. To spice things up, I changed the Kendo UI CSS to the Metro UI theme.

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

Stay tuned for Part 3…

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

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

Happy Coding…! :)