Harness the Power of ASP.NET MVC, Web Api, OData, Kendo UI & RequireJS to Build an Easy & Maintainable SPA (for the .NET Developer) – Published

Apologize for the delay, the original article: “Harness the Power of ASP.NET MVC, Web Api, OData, Kendo UI & RequireJS to Build an Easy & Maintainable SPA (for the .NET Developer)” has now been published as “A .NET Developer Primer to Single-Page Applications (SPA)“.

Download: http://lelong37.files.wordpress.com/2014/03/msdn-mag-1403.pdf
Online: http://msdn.microsoft.com/en-us/magazine/dn605877.aspx
Source code: https://easyspa.codeplex.com
Live Demo: http://easyspa.azurewebsites.net/home/spa#/customer/index

3-4-2014 1-36-58 AM

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…! :)

Modern Web Application Layered High Level Architecture with SPA, MVC, Web API, EF, Kendo UI, OData

This will be part one 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: 07/22/2013 – Added blog series on actual implementation steps, for this architecture with patterns towards the end of this blog post.

Search and searched and seems difficult to locate any comprehensive top down, full stack architecture or high level design diagrams for modern (SPA) web apps. It’s probably important you have at least a high level picture what this architecture looks like now that there is quite a bit more design work involved on the client side especially with more and more implementations are around SPA and patterns like MVVM; so hence this post. Obviously there is no such thing as one size fits all especially when it comes to architecture, so feel free to omit or add to the architecture based on your specific needs.

Modern Web Application Logical and Physical Architecture High Level Design with SPA

Client Layer (HTML5 Browser)
Model View ViewModel (MVVM) is a design pattern which helps developers separate the Model (the data) from the View (the UI). The View-Model part of MVVM is responsible for exposing the data objects from the Model in such a way that those objects are easily consumed in the View. Kendo MVVM is an implementation of the MVVM pattern which seamlessly integrates with the rest of the Kendo framework (widgets and DataSource).

Web Layer (Server)
Almost the entire ASP.NET MVC Web Layer can leverage the DI & IoC Pattern, you can read up on what the benefits are and how to do this download both a sample MVC app that uses MEF or Unity 3 from one of my previous post.

  • Presentation Layer
    For modern MVC web applications, the presentation layer (server-side) consists of a Controllers who’s only tasks are to render an HTML page, css, Javascript, HTML templates, images, etc. Very little server-side code, if any, is responsible for any UI rendering responsibilities. Once the page is rendered in the browser client-side components (the browser or user agent that executes scripts and displays the HTML). With client-side techniques such as AJAX and with rich client-side frameworks such as Kendo UI Web, it is possible to execute logic on the client, for nice fluid user experiences. Implementing a SPA, can greatly increase the user experience by, reducing or eliminating post backs and refreshes.

  • Business Layer
    When designing the business layer for your Web application, consider how to implement the business logic and long-running workflows. Using a separate business layer that implements the business logic and workflows can improve the maintainability and testability of your application, and allow you to centralize and reuse common business logic functions.

  • Data Layer
    Consider designing a data layer for your Web application that abstracts the logic necessary to access the database. This can be achieved with implementing the Repository pattern, the Repository pattern is often implemented with the Unit of Work pattern. Entity Framework already implements the Unit of Work Pattern with the DbContext, however you should always work an abstraction of this, you can read up on one of previous post on how to do this. Using a separate data layer makes the application easier to configure and maintain, and hides the details of the database from other layers of the application.

    Your business entities, usually shared between the layers of your application e.g. Business and Data Layer should be POCO entities. Entity Framework enables you to use custom data classes together with your data model without making any modifications to the data classes themselves. This means that you can use “plain-old” CLR objects (POCO), such as existing domain objects, with your data model. These POCO data classes (also known as persistence-ignorant objects), which are mapped to entities that are defined in a data model, support most of the same query, insert, update, and delete behaviors as entity types that are generated by the Entity Data Model tools.

Services Layer
Consider designing a separate service layer if you plan to deploy your business layer on a remote tier, or if you plan to expose your business logic using a Web service. Design the services to achieve maximum reusability by not assuming the specific details of clients that will use them, and avoid changes over time that might break the service interface for existing clients. Instead, implement versions of the interface to allow clients to connect to the appropriate version.

Download PNG Version
Download PDF Version

I’ve posted a three part blog series, that covers the actual implementation of most of this architecture and patterns used:

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

Happy Architecting…!

Learning Kendo UI Web Development with MVC Book Published…!

Follow up on one of my previous blog post in regards to Kendo UI Web with MVC book project I had the opportunity to be involved with as a Technical Reviewer, the book is complete and has been published..! Kudos to John Adams (author) from RBA for all the late nights, weekends and hard work I know he put into the project.

For those of you looking for a good starting point, incorporating Kendo UI Web into your MVC project or solution the book has some good real world practical implementations with ASP.NET MVC.

Personally and professionally Kendo UI Web is my client side framework of choice. It’s one solution that offers it all, so that you don’t have into incorporate and include several different frameworks to gain some of the fundamental features of Kendo UI Web, especially when working with MVC.

My personal top favorite features of Kendo UI Web Framework

  • Widgets (client-side controls)
  • MVVM Framework (a must have for modern web apps)
  • Validation Framework
  • Templates
  • Built on top of jQuery
  • Custom UI/UX Themes
  • MVC 4 Sever Side Wrappers
  • SPA (Single Page App) Support (a must have for modern web apps)
  • Globalization
  • DataSource
  • List goes on…

http://www.amazon.com/dp/1849694346/?tag=packtpubli-20
http://www.packtpub.com/learning-kendo-ui-web-development/book
http://packtlib.packtpub.com/library/9781849694346

An easy-to-follow practical tutorial to add exciting features to your web pages without being a JavaScript expert with this book and ebook

Overview

Learn from clear and specific examples on how to utilize the full range of the Kendo UI tool set for the web
Add powerful tools to your website supported by a familiar and trusted name in innovative technology
Learn how to add amazing features with clear examples and make your website more interactive without being a JavaScript expert

In Detail

Creating useful and attractive web sites for today’s audiences requires more JavaScript programming than ever before. JavaScript, however, isn’t easy and staying up to date with recent trends makes it even harder. You need a way to integrate the latest technology into your web site without having to start from the beginning.

“Learning Kendo UI Web Development” shows you how to add the latest web features to your site without doing all of the work yourself. The experts at Telerik have created a fully supported code library that lets you focus on what makes your sites special while letting them focus on how that translates into cutting edge JavaScript code.

This book will take you on a survey through Kendo UI for the web from the basic widgets that wrap input fields to the full page scaffolding widgets for setting up your site.

Kendo UI for the web is a rich framework of JavaScript widgets and tools that will add immediate value to your web sites. Learn the full spectrum of what Kendo UI has to offer in specialized widgets that gather input from users by displaying calendars, sliders, or date pickers all the way up to widgets that structure your web pages through data-driven models in accordions, tabs, list views, and tree views.

“Learning Kendo UI Web Development” is the perfect companion for navigating the broad features offered by Telerik in JavaScript web development.

What you will learn from this book

  • Leverage data source objects and JavaScript templates with the TabStrip and Grid widgets
  • Guide users in date selection with the Calendar widget
  • Create fast and fluid word wheels with the AutoComplete widget and date selection with the Calendar widget
  • Take advantage of the powerful MVVM JavaScript architectural pattern
  • Take full HTML input from users with a graphical editor
  • Structure your site with the Menu and ListView widgets
  • Build interactive accordions with the PanelBar widget and a fun way to select numbers with the Slider widget
  • Organize your data with the Splitter and TreeView widgets and Customize pop-up windows and file uploads with the Window and Upload widgets

Approach

A practical tutorial with step-by-step example based approach.

Who this book is written for

This book is for web developers who want to take advantage of cutting edge JavaScript and HTML 5 web site features, but who don’t have the time or the knowledge to write all of that code by hand. The reader should be familiar with basic HTML 5 and JavaScript but does not need to be an expert.

Author:

John Adams currently works as an application development consultant in the Dallas/Fort Worth area for a fantastic company called RBA. He has been developing custom business applications with the Microsoft .NET platform for 6 years and has specialized in development with ASP.NET MVC. He loves writing code and creating solutions. Above all, he loves his wife and children and the lord Jesus Christ.

This book is dedicated to Michell, Samuel, and Sophie whose patience with my late nights made this project possible.

I would also like to thank RBA, especially my manager Will, who introduced me to the project and kicked everything off.

Finally, I would like to thank Kartikey Pandey, Anugya Khurana, Mayur Hule, Ricardo Covo, and Long Le for their oversight and editing skills. Their work has been exceptional and valuable throughout.

Reviewers:

Ricardo Covo has more than a decade of international experience in the Software Development field, with experience in Latin America, California, and Canada. He has a wealth of experience in delivering data-driven enterprise solutions across various industries.

With a Bachelor’s degree in Systems Engineering, complemented with a certification in Advanced Project Management, he has the right combination of technical and leadership skills to build development teams and set them up for efficient execution.

In 2007 he founded (and is the principal of) Web Nodes – Software Development (http://webnodes.ca); a custom software development company, with clients big and small in Canada, United States, and South America.

Prior to Web Nodes, Ricardo spent some years in the corporate world both in Canada and in the U.S., being part of companies such as Loblaws Inc., Trader Corporation, UNX (http://www.unix.com) and Auctiva (http://www.auctiva.com).

Ricardo’s passion for technology goes beyond work; he normally works on personal projects in an effort to always remain on top of the changes in technology. These projects include: http://ytnext.com, http://serversok.com, and http://toystrunk.com.

Long Le is a Principle .NET Architect and ALM Practitioner at CBRE. He also serves as consultant for Thinklabs and spends most of his time developing frameworks and application blocks, providing guidance for best practices and patterns, and standardizing the enterprise technology stack. He has been working with Microsoft technologies for over 10 years.

Le has focused on a wide spectrum of server-side and web technologies, such as ASP.NET Web Forms, ASP.NET MVC, Windows Workflow, LINQ and Entity Framework, DevExpress, and Kendo UI. In his spare time, he enjoys blogging (http://blog.longle.net) and playing Call of Duty on his XBOX. He’s recently became a proud father of his new born daughter Khloe Le. You can reach and follow him on Twitter @LeLong37.

Special thanks to my significant other Tina Le for all your love and support throughout this project and to my wonderful newborn daughter Khloe Le. I love you.

https://twitter.com/LeLong37/status/344402714240897024

Telerik’s Kendo UI Web MVVM Framework Rocks!

For those of us that fell in love with the awesome binding power that WPF and Silverlight brought to the masses with MVVM (Model-ViewModel-Model), well I have good news for you folks. Telerik has their Kendo UI Web MVVM Framework so that you can leverage MVVM in your (MVC) web apps!

Quick synopsis on MVVM for those of us that are new to it (especially MVC devs) and in the interest of time I’ll try to do this in a 60 second nutshell. So when working with a view whether it be in WPF, Silverlight, and now MVC, you can declaritively set which controls bind to which properties to your ViewModel (simply a JSON object with properties). So for example I can set a DropDownList (select) control on my view to bind directly to a property of my ViewModel that is a collection.

So let’s get right to it with a few simple examples.

First add a script references for jQuery and Kendo UI, here are some links for Microsoft’s and Teleriks’ CDN’s for these scripts.


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Kendo UI Web MVVM</title>
</head>
<body>
</body>
<script src="http://code.jquery.com/jquery-1.7.1.min.js"></script>
<script src="http://cdn.kendostatic.com/2012.2.710/js/kendo.all.min.js"></script>
</html>

Now let’s create a simple form.


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Kendo UI Web MVVM</title>
    <script src="http://code.jquery.com/jquery-1.7.1.min.js" type="text/javascript"></script>
    <script src="http://cdn.kendostatic.com/2012.2.710/js/kendo.all.min.js" type="text/javascript"></script>
    <style type="text/css">
        body
        {
            font-family: Arial;
        }
        label
        {
            display: block;
            margin-top: 10px;
        }
        #formContainer
        {
            background-color: #F2F7F9;
            width: 400px;
            padding: 20px;
            margin: 50px auto;
            border: 6px solid #8FB5C1;
            border-radius: 15px;
            position: relative;
        }
    </style>
</head>
<body>
    <div id="formContainer">
        <h1>My First MVVM Web View!</h1>
        <form id="myForm" action="">
        <label>
            First Name</label>
        <input type="text" />
        <label>
            Last Name</label>
        <input type="text"  />
        <label>
            Email</label>
        <input type="text"  />
        <label>
            Twitter</label>
        <input type="text"  />
        <label>
            Site</label>
        <input type="text" />
        <label>
            Address</label>
        <input type="text" />
        <label>
            City</label>
        <input type="text" />
        <label>
            State</label>
        <input type="text" />
        <label>
            Zip</label>
        <input type="text" />
        <label>
            Occupation</label>
        <select>
        </select>
        <br />
        <br />
        <input type="button" />
        <input type="button" />
        <input type="button" />
         <input type="button" />
        </form>
    </div>
</body>

</html>

Now let’s create a ViewModel (JavaScript JSON object or class if you will) with some default values for our View (in this case our form) to bind to. We will also add some methods to our ViewModel so that we can bind our buttons that are on our view too.


  <script language="javascript" type="text/javascript">

    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: ["Please Select", "Hacker", "Jedi", "Ninja"],
            occupation: "Jedi",
        });

        kendo.bind($("#myForm"), myViewModel);
    });

</script>

Note: Line 18 is what will bind our form to our ViewModel, if your wondering why nothing is happening yet it’s because we are missing once more step which is to provide the binding (mapping) information, which will map our controls to our ViewModel. The beauty here is we will do this declaritively with without code.

Our View will have two modes: read-only and edit, so we will bind our controls for where each control will get and set it’s values to and another binding for enabling and disabling them.


    <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" />
        <input type="button" value="Edit" />
        <input type="button" value="Cancel" />
         <input type="button" value="Reset" />
        </form>
    </div>

Now if we refresh the page we that our View is not bound to our ViewModel…!

Note: Notice that our form is in read-only mode, all of our controls are disabled for editing because the disabled attribute is bound to the isDisabled property in our ViewModel whose default value is set to true.

For those of us that are ASP.NET MVC developers, the answer is yes you can get and post your ViewModel back as JSON using jQuery from and to an actions on your controller!

Now let’s demonstrate binding our buttons to methods on our ViewModel, first let’s add a couple of methods to our ViewModel (edit, cancel, reset, and load)

  • edit, will enabled our View for editing
  • cancel, will set our View back to read-only mode
  • reset, will clear out our View
  • load, will load John Doe’s information into our View (obviously here you could load this from using a “GET” to an action off of your controller)

    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: ["Please Select", "Hacker", "Jedi", "Ninja"],
            occupation: "Jedi",
            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", "Please Select");
            },
            load: function (e) {
                LoadJohnDoesInfo();
            }
        });

        kendo.bind($("#myForm"), myViewModel);
    });

    function LoadJohnDoesInfo() {
        myViewModel.set("firstName", "John");
        myViewModel.set("lastName", "Doe");
        myViewModel.set("email", "jdoe@skyranch.com");
        myViewModel.set("twitter", "twitter.com/jedi");
        myViewModel.set("site", "starwars.com");
        myViewModel.set("address",  "1212 SkyRanch");
        myViewModel.set("state", "California");
        myViewModel.set("zip", "98000");
        myViewModel.set("occupation", "Jedi");
    }

Add our binding meta-data to our buttons declaritively.


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


So let’s invoke some our methods on our ViewModel and give our button’s a run for their money.

Now let’s click on the [Load] button and here I just wanted to demonstrating that all we need to do here to update the View is interact with the ViewModel, any changes to the ViewModel and the View automatically updates because they are bound together, which is the magic and essense of the MVVM pattern!

Our load method on our ViewModel.


            load: function (e) {
                LoadJohnDoesInfo();
            }

Which turns around and invokes our LoadJohnDoesInfo method and set’s his data on our ViewModel.


    function LoadJohnDoesInfo() {
        myViewModel.set("firstName", "John");
        myViewModel.set("lastName", "Doe");
        myViewModel.set("email", "jdoe@skyranch.com");
        myViewModel.set("twitter", "twitter.com/jedi");
        myViewModel.set("site", "starwars.com");
        myViewModel.set("address", "1212 SkyRanch");
        myViewModel.set("state", "California");
        myViewModel.set("zip", "98000");
        myViewModel.set("occupation", "Jedi");
    }

Notice when the View first loads our entire form is disabled for our read-only mode and when we click [Edit] all of our controls that had the binding for disabled that was bound to the isDisabled property on our ViewModel which be default is set to true. Once we click on the Edit button the isDisabled property is set to true enabling all of our controls for edit mode.

Our declaritive binding on one of our controls.


<input type="text" data-bind="value: firstName, disabled:  isDisabled" />

The Edit method that is invoked when clicking the [Edit] button.


            edit: function (e) {
                this.set("isDisabled", false);
            },

Hitting the [Reset] button will simply invoke the reset method on our ViewModel which clears out all our properties by setting them null values and again our View is automagically updated because it is bound to our ViewModel.


            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", "Please Select");
            },

Well great, I know we aren’t doing anything ground breaking here, however this post is really just to illustrate MVVM on the client-side using Telerik’s Kendo UI Web MVVM Framework and ellaborate on how we can really just work with the ViewModel to update the View vice- versa any updates to the View will update our ViewModel because they are “bound” together using MVVM.

Last but not least, even though the Kendo UI Framework was written with jQuery, notice how we didn’t have to explicitly use any jQuery or Javascript to set or get any values from our controls. I’m not at all saying jQuery is a bad thing, however when developing you probably want to be coding something that is immediately adding business value rather than coding jQuery selectors to get and and set values in your View right..? :p

Happy coding…! :)

Download sample code: https://skydrive.live.com/redir?resid=949A1C97C2A17906!1951