AngularJS & Kendo UI using Angular Kendo with ASP.NET MVC 5, Web API & OData

Update: 9/12/2014 – Codeplex source code download URL update.

Update: 5/1/2014 – Customer View & customerController.js has been updated to toggle styles by manipulating the DOM through the ViewModel with ng-show to adhere and stay consistent with AngularJS best practices.

Update: 5/14/2014 – Added section “Animating our View Swapping”, to elaborate on adding animation to the “ng-view” swapping process.

Source code: http://goo.gl/f1esAf
Live demo: http://longle.azurewebsites.net

With AngularJS and ASP.NET MVC, we now have an MVC pattern and architecture for both on the client and server. What do we do and/or how do we approach this? Well one can argue we don’t use any of the MVC architecture on the server and build out a full-fledged AngularJS front-end application and only use make async calls to Web API for all things that absolutely need to be on the server e.g. CRUD, workflows, business logic, etc.

Now, there’s absolutely nothing wrong with this approach, and for the most part a lot of heavy front end SPA’s are built this way in ASP.NET MVC. However, with all the .NET, ASP.NET and AngularJS goodness, why not leverage the best of both worlds? Again, there’s absolutely nothing wrong with building a pure AngularJS application and only using Web API, but for this blog post we’ll go over patterns of using the best of both worlds along with integrating AngularJS with Kendo UI.

Note: for those that prefer to build a pure AngularJS front-end and only leveraging Web API on the backend (which many devs prefer), simply ignore the MVC Razor sections. If this is the case you can also opt to render raw *.html vs. *.cshtml views as well.

Let’s take a look at our Project structure for all things client-side.

2014-05-01_1-21-35

Staying true to the AngularJS seed (sample) application from the AngularJS team, we’ll also create an “app” folder under “scripts”, staying true to ASP.NET MVC, where JS scripts are supposed to reside.

Northwind.Web/Scripts/app/app.js


var northwindApp = angular.module('northwindApp', ['kendo.directives', 'ngRoute', 'ngAnimate'])
    .config(function ($routeProvider)
    {
        $routeProvider
            .when('/home',
            {
                templateUrl: '/home/home',
                controller: 'homeController'
            })
            .when('/customer',
            {
                templateUrl: '/customer',
                controller: 'customerController'
            })
            .when('/customer/edit/:id',
            {
                templateUrl: '/customer/edit',
                controller: 'customerEditController'
            })
            .when('/help',
            {
                templateUrl: '/help'
            })
            .otherwise(
            {
                redirectTo: '/home'
            });
    });

This is where our AngularJS application bootstrapping process takes place, here we’ve declared our new app “northwindApp” and we register all our routes (URL’s) for our SPA application. Note, if you’re not building a SPA, you can still leverage all the AngularJS goodness and architecture it brings (which btw, is huge), you can just omit the registering any of the routes in this step. Notice, the parameters we are passing into the module method: “northwindApp” is the name of our app, after that it’s an array of all the AngularJS modules we want injected into our application:

  • “kendo.directives” are needed so that we have full integration with AngularJS and Kendo UI, this will make more sense when we cover how AngularJS will compile Kendo UI widgets using directives.
  • “ngRoute” is needed so that we can configure our SPA routes, if your familiar with ASP.NET MVC routes, pretty much the same principles here, only difference here is routing of your application will take place on the client vs. the server. Here we are configuring each out with the “when” method by simply passing in the path in the URL and within the “when” we configure what the server URL path is to remotely load the View and which Controller to use. The “otherwise” method is simply a default route that will be used if none of the other routes are matched from what’s in the URL. You could route the default “otherwise” route to go to a custom 404 page, for our purposes we’ll just route the user to the default home page.
  • “ngAnimate” is injected here so that we can add some animation when AngularJS swaps out our Views in our SPA.

Northwind.Web/Scripts/app/controllers/homeController.js


'use strict';

northwindApp.controller('homeController',
    function ($scope) {
        $scope.title = "ASP.NET";
    });

AngularJS has the notion of scopes with in application, there is one $rootScope and multiple children $scopes (so to speak), in your application. The best .NET analogy of a scope is something like a context, so $rootScope would map to an ApplicationContext (global with Singleton like behaivor) and $scope would map to something like a (if we had one) ControlllerContext. $scope is more of a context that you use to as an adhesive to bind you View to your Controller. So from homeController.js you see that we’ve defined a property named “title” with the value “ASP.NET”, let’s take a look at our partial Home.cshtml mark-up.

Northwind.Web/Views/Home/Home.cshtml


<div class="jumbotron">
   <h1>{{title}</h1>
   <p class="lead">ASP.NET is a free web framework for building great Web sites and Web applications using HTML, CSS, and JavaScript.</p>
   <p><a href="http://asp.net" class="btn btn-primary btn-lg">Learn more &raquo;</a></p>
</div>
<div class="row">
   <div class="col-md-4">
       <h2>Getting started</h2>
       <p>
           ASP.NET Web API is a framework that makes it easy to build HTTP services that reach a broad range of clients, including browsers and mobile devices. ASP.NET Web API is an ideal platform for building RESTful applications on the .NET Framework.
       </p>
       <p><a class="btn btn-default" href="http://go.microsoft.com/fwlink/?LinkId=301870">Learn more &raquo;</a></p>
   </div>
   <div class="col-md-4">
       <h2>Get more libraries</h2>
       <p>NuGet is a free Visual Studio extension that makes it easy to add, remove, and update libraries and tools in Visual Studio projects.</p>
       <p><a class="btn btn-default" href="http://go.microsoft.com/fwlink/?LinkId=301871">Learn more &raquo;</a></p>
   </div>
   <div class="col-md-4">
       <h2>Web Hosting</h2>
       <p>You can easily find a web hosting company that offers the right mix of features and price for your applications.</p>
       <p><a class="btn btn-default" href="http://go.microsoft.com/fwlink/?LinkId=301872">Learn more &raquo;</a></p>
   </div>
</div>

Notice the {{title}} on line 2, between the h1 tags, when the View (Home.cshtml) loads, AngularJS will replace the h1 html with the $scope.title value: “ASP.NET”.

http://longle.azurewebsites.net

2014-05-01_0-04-13

Now that we have a primer to AngularJS’s MVVM pattern, let’s take a look at some bit more complex, the customerController.js which will display a Kendo UI Grid with the customizations which include a toolbar with buttons all wired up with Angular Kendo directives and AngularJS MVVM.

Northwind.Web/Scripts/app/controllers/customerController.js


'use strict';

northwindApp.controller('customerController',
    function ($scope, $rootScope, $location, customerDataSource)
    {
        customerDataSource.filter({}); // reset filter on dataSource everytime view is loaded

        var onClick = function (event, delegate)
        {
            var grid = event.grid;
            var selectedRow = grid.select();
            var dataItem = grid.dataItem(selectedRow);

            if (selectedRow.length &amp;gt; 0)
                delegate(grid, selectedRow, dataItem);
            else
                alert(&amp;quot;Please select a row.&amp;quot;);
        };

        $scope.isToolbarVisible = false;

        var toggleToolbar = function() {
            $scope.isToolbarVisible = !$scope.isToolbarVisible;
        }

        $scope.toolbarTemplate = kendo.template($(&amp;quot;#toolbar&amp;quot;).html());

        $scope.save = function (e)
        {
            onClick(e, function (grid)
            {
                grid.saveRow();
                toggleToolbar();
            });
        };

        $scope.cancel = function (e)
        {
            onClick(e, function (grid)
            {
                grid.cancelRow();
                toggleToolbar();
            });
        },

        $scope.details = function (e)
        {
            onClick(e, function (grid, row, dataItem)
            {
                $location.url('/customer/edit/' + dataItem.CustomerID);
            });
        },

        $scope.edit = function (e)
        {
            onClick(e, function (grid, row)
            {
                grid.editRow(row);
                toggleToolbar();
            });
        },

        $scope.destroy = function (e)
        {
            onClick(e, function (grid, row, dataItem)
            {
                grid.dataSource.remove(dataItem);
                grid.dataSource.sync();
            });
        },

        $scope.onChange = function (e)
        {
            var grid = e.sender;

            $rootScope.lastSelectedDataItem = grid.dataItem(grid.select());
        },

        $scope.dataSource = customerDataSource;

        $scope.onDataBound = function (e)
        {
            // check if there was a row that was selected
            if ($rootScope.lastSelectedDataItem == null)
                return;

            var view = this.dataSource.view(); // get all the rows

            for (var i = 0; i &amp;lt; view.length; i++)
            {
                // iterate through rows
                if (view[i].CustomerID == $rootScope.lastSelectedDataItem.CustomerID)
                {
                    // find row with the lastSelectedProductd
                    var grid = e.sender; // get the grid

                    grid.select(grid.table.find(&amp;quot;tr[data-uid='&amp;quot; + view[i].uid + &amp;quot;']&amp;quot;)); // set the selected row
                    break;
                }
            }
        };
    });

We declare a Controller by using the “controller” method, the first parameter is the name of our Controller, the second parameter takes a function who’s parameters are all the modules we want our Controller to be injected with. Our Controller is being injected with the following.

  • $scope, which we already know about
  • $rootScope, so that we can store some things to help us manage our View state, in this case we will store what was the last selected row on our Grid so that when the user navigates back to this view we can re-select the same row and highlight it.
  • $location, so that we can navigate from this View to other Views e.g. customer edit view which will have a form so we can make changes to a Customer.
  • customerDataSource, which is a reusable AngularJS service which returns a Kendo UI DataSource.

Notice how we are leveraging MVVM to manipulate the DOM to toggle the tool bar above the grid with the “ng-show” directive vs. manipulating the DOM with jQuery, this is key with AngularJS design principles.

As for the rest of the code in CustomerController.js it’s fairly straight forward, for me details on what each of these methods responsibilities are please have a quick read up here: http://blog.longle.net/2013/06/18/mvc-4-web-api-odata-entity-framework-kendo-ui-grid-datasource-with-mvvm.

Northwind.Web/Scripts/app/services/customerDataSource.js


'use strict';

northwindApp.factory('customerDataSource',
    function (customerModel)
    {
        var crudServiceBaseUrl = "/odata/Customer";

        return new kendo.data.DataSource({
            type: "odata",
            transport: {
                read: {
                    async: false,
                    url: crudServiceBaseUrl,
                    dataType: "json"
                },
                update: {
                    url: function (data)
                    {
                        return crudServiceBaseUrl + "(" + data.CustomerID + ")";
                    },
                    type: "put",
                    dataType: "json"
                },
                destroy: {
                    url: function (data)
                    {
                        return crudServiceBaseUrl + "(" + data.CustomerID + ")";
                    },
                    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"]; },
                model: customerModel
            },
            error: function (e)
            {
                alert(e.xhr.responseText);
            }
        });
    });

We create the customerDataSource as a reusable AngularJS service using the “factory” method. The first parameter is the name of our service and the second parameter is method who’s parameters indicates that we are injected with customModel.

Northwind.Web/Scripts/app/services/customerModel.js


'use strict';

northwindApp.factory('customerModel', function ()
{
    return kendo.data.Model.define({
        id: "CustomerID",
        fields: {
            CustomerID: { type: "string", editable: false, nullable: false },
            CompanyName: { title: "Company", type: "string" },
            ContactName: { title: "Contact", type: "string" },
            ContactTitle: { title: "Title", type: "string" },
            Address: { type: "string" },
            City: { type: "string" },
            PostalCode: { type: "string" },
            Country: { type: "string" },
            Phone: { type: "string" },
            Fax: { type: "string" },
            State: { type: "string" }
        }
    });
});

Again, we create a reusable AngularJS service which returns a Kendo UI Customer model. This reusable Cusomer model will be used for our Grid and Controllers.

Northwind.Web/Customer/Index.cshtml


@{
   ViewBag.Title = "Index";
}

<div>
   <h2 ng-cloak>{{title}}</h2>
   <div>
       <div class="demo-section">
           <div class="k-content" style="width: 100%">
               <div kendo-grid="grid"
                    k-sortable="true"
                    k-pageable="true"
                    k-filterable="true"
                    k-editable="'inline'"
                    k-selectable="true"
                    k-toolbar='[ { template: toolbarTemplate } ]'
                    k-columns='[
                       { field: "CustomerID", title: "ID", width: "75px" },
                       { field: "CompanyName", title: "Company"},
                       { field: "ContactName", title: "Contact" },
                       { field: "ContactTitle", title: "Title" },
                       { field: "Address" },
                       { field: "City" },
                       { field: "PostalCode" },
                       { field: "Country" },
                       { field: "Phone" },
                       { field: "Fax" }]'
                    k-data-source="dataSource"
                    k-on-data-bound="onDataBound(kendoEvent)"
                    k-on-change="onChange(kendoEvent)">
               </div>
               <style scoped>
                   .toolbar { padding: 15px; float: right; }
               </style>
           </div>
       </div>

       <script type="text/x-kendo-template" id="toolbar">
           <div>
               <div class="toolbar">
                   <button kendo-button ng-click="edit(this)"><span class="k-icon k-i-tick"></span>Edit</button>
                   <button kendo-button ng-click="destroy(this)"><span class="k-icon k-i-tick"></span>Delete</button>
                   <button kendo-button ng-click="details(this)"><span class="k-icon k-i-tick"></span>Edit Details</button>
               </div>
               <div class="toolbar" style="display:none">
                   <button kendo-button ng-click="save(this)"><span class="k-icon k-i-tick"></span>Save</button>
                   <button kendo-button ng-click="cancel(this)"><span class="k-icon k-i-tick"></span>Cancel</button>
               </div>
           </div>
       </script>
   </div>
</div>

Here we can see that we that the declarative syntax for Kendo UI widgets have changed a bit, and at a first glance it seems that it’s dramatic change, but as they say, there’s a method to the madness. If you understand the convention here, it’s pretty simple, take look at the cheat sheet we have below for our view.

Customer.cshtml Cheat Sheet

Before (with Kendo UI) After (with Angular Kendo)
data-role=”grid” kendo-grid
data-sortable k-sortable
data-bind=”source: dataSource” k-data-source=”dataSource”
data-bind=”events: { change: onChange }” k-on-change=”onChange(kendoEvent)”

For declarative widgets, we simply prefix with “kendo-“, quick note, where the name has camel casing, you would simply separate it with dashes e.g. DropDownList -> k-drop-down-list.

  • Options for a widget are simply prefixed with a “k-“.
  • For all things that were bound with “data-bind”, simply become first class citizen attributes again following the dash-separted convention.
  • Same for events, however the only other item, is that you have to explicitly pass in a “kendoEvent” vs. before you didn’t.

If we run our application, we can see that everything renders and it’s business as usual.

http://longle.azurewebsites.net/#/customer

2014-05-01_0-44-42

Northwind.Web/Scripts/app/controllers/customerEditController.js


'use strict';

northwindApp.controller('customerEditController',
    function ($scope, $routeParams, $location, customerDataSource)
    {
        var customerId = $routeParams.id;

        customerDataSource.filter({ field: "CustomerID", operator: "eq", value: customerId });
        $scope.customer = customerDataSource.at(0);

        $scope.save = function ()
        {
            customerDataSource.view()[0].dirty = true;
            customerDataSource.sync();
            $location.url('/customer');
        };

        $scope.cancel = function ()
        {
            $location.url('/customer');
        }
    });

For the customerEditController.js, we are being injected with $scope, $routeParams, $location and customerDataSource, notice the reusability of our customerDataSource. We use the customerDataSource to hydrate our grid with a list of customers on our Northwind.Web.Views/Conrollers/Index.cshml view as well as our Northwind.Web/Views/Customers/Edit.cshtml view with a single customer.

On the Sever-Side of Things

In the beginning of the post, we mentioned leveraging the best of both worlds, we’ve seen how we done this on the client side of things with all the goodness of AngularJS and Kendo UI. AngularJS brings a nice fully fledged framework which forces us to using nice well defined patterns (MVC’ish & MVVM), which has a natural side effect of having an elegant architecture on the client side. So how do we make the most of ASP.NET MVC on the server side? Simple, we follow all the best practices as we’ve always been doing in the past.

Taking full advantage of MVC’s bundling and Minification

Northwind.Web/App_Start/BundleConfig.cs


using System.Web.Optimization;

namespace Northwind.Web
{
    public class BundleConfig
    {
        // For more information on bundling, visit http://go.microsoft.com/fwlink/?LinkId=301862
        public static void RegisterBundles(BundleCollection bundles)
        {
            //Scripts
            bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                        "~/Scripts/jquery-{version}.js",
                        "~/Scripts/jquery-migrate-{version}.js"));

            // Use the development version of Modernizr to develop with and learn from. Then, when you're ready for production, use the build tool at http://modernizr.com to pick only the tests you need.
            bundles.Add(new ScriptBundle("~/bundles/modernizr").Include("~/Scripts/modernizr-*"));

            bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include(
                      "~/Scripts/bootstrap.js",
                      "~/Scripts/respond.js"));

            bundles.Add(new ScriptBundle("~/bundles/angular").Include(
                      "~/Scripts/angular.js",
                      "~/Scripts/angular-route.js",
                      "~/Scripts/angular-animate.js"));

            bundles.Add(new ScriptBundle("~/bundles/kendo").Include(
                "~/Scripts/kendo/2014.1.318/kendo.web.min.js",
                "~/Scripts/angular-kendo.js"));

            bundles.Add(new ScriptBundle("~/bundles/app").Include(
                      "~/Scripts/app/app.js",
                      "~/Scripts/app/services/customerModel.js",
                      "~/Scripts/app/services/customerDataSource.js",
                      "~/Scripts/app/controllers/homeController.js",
                      "~/Scripts/app/controllers/customerController.js",
                      "~/Scripts/app/controllers/customerEditController.js"));

            //Styles
            bundles.Add(new StyleBundle("~/Content/css").Include(
                      "~/Content/bootstrap.css",
                      "~/Content/site.css"));

            bundles.Add(new StyleBundle("~/Content/kendo").Include(
                      "~/Content/kendo/2014.1.318/kendo.common.min.css",
                      "~/Content/kendo/2014.1.318/kendo.bootstrap.min.css"));

            // Set EnableOptimizations to false for debugging. For more information, visit http://go.microsoft.com/fwlink/?LinkId=301862
            BundleTable.EnableOptimizations = false;
        }
    }
}


ASP.NET’s Layout.cshtml have always been great to work with, so we stick with it.


<!DOCTYPE html>
<html lang="en" ng-app="northwindApp">
<head>
   <meta charset="utf-8" />
   <meta name="viewport" content="width=device-width" />
   <meta name="description" content="SPA Application" />

   <title>@ViewBag.Title</title>

   @Styles.Render("~/Content/css")
   @Styles.Render("~/Content/kendo")
   @RenderSection("Styles", required: false)
   @Scripts.Render("~/bundles/modernizr")
</head>
<body>
   <div class="navbar navbar-inverse navbar-fixed-top">
       <div class="container">
           <div class="navbar-header">
               <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                   <span class="icon-bar"></span>
                   <span class="icon-bar"></span>
                   <span class="icon-bar"></span>
               </button>
               <a href="#/home" class="navbar-brand">http://blog.longle.net</a>
           </div>
           <div class="navbar-collapse collapse">
               <ul class="nav navbar-nav">
                   <li><a href="#/home">Home</a></li>
                   <li><a href="#/customer">Customer</a></li>
                   <li><a href="#/help">API</a></li>
               </ul>
           </div>
       </div>
   </div>
   <div class="container body-content">
       @RenderBody()
       <footer>
           <p>Copyright &copy; @DateTime.Now.Year - SPA Application</p>
       </footer>
   </div>

   @Scripts.Render("~/bundles/jquery")
   @Scripts.Render("~/bundles/angular")
   @Scripts.Render("~/bundles/app")
   @Scripts.Render("~/bundles/kendo")
   @Scripts.Render("~/bundles/bootstrap")

   @RenderSection("Scripts", required: false)
</body>
</html>

Notice we are taking advantage of all of our bundling and minification.

By default, ASP.NET MVC loads Index.cshtml e.g. http://localhost:2569/Home, again, same here, we use Index.cshtml as our landing page as usual.

Northwind.Web/Vviews/Home/Index.cshml


@{
   ViewBag.Title = "SPA";
   ViewBag.Description = "SPA App";
}
<div ng-view></div>

Notice how we have very little mark-up here and this View is simply for us to server up all the HTML in our Northwind.Web/Views/Shared/_Layout.cshml, then the @RenderBody() will render Northwind.Web/Views/Home/Index.cshml which will have the “div” element where AngularJS will load our AngularJS Views into on the client side.

Northwind.Web/Controllers/CustomerController.cs


using System.Web.Mvc;

namespace Northwind.Web.Controllers
{
    public class CustomerController : Controller
    {
        // GET: Customer
        public ActionResult Index()
        {
            return PartialView();
        }

        public ActionResult Edit()
        {
            return PartialView();
        }
    }
}

In CustomerController.cs, we are returning PartialView’s vs. View’s, well because that’s what they really are now “PartialViews”, there simply chunks of HTML that AngularJS will remotely load into our div> element when swapping views.

Northwind.Web.Api.CustomerController.cs


public class CustomerController : ODataController
{
private readonly ICustomerService _customerService;
private readonly IUnitOfWorkAsync _unitOfWorkAsync;

public CustomerController(
    IUnitOfWorkAsync unitOfWorkAsync,
    ICustomerService customerService)
{
    _unitOfWorkAsync = unitOfWorkAsync;
    _customerService = customerService;
}

// GET: odata/Customers
[HttpGet]
[Queryable]
public IQueryable<Customer> GetCustomer()
{
    return _customerService.ODataQueryable();
}

// GET: odata/Customers(5)
[Queryable]
public SingleResult<Customer> GetCustomer([FromODataUri] string key)
{
    return SingleResult.Create(_customerService.ODataQueryable().Where(t => t.CustomerID == key));
}

// PUT: odata/Customers(5)
public async Task<IHttpActionResult> Put(string key, Customer customer)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    if (key != customer.CustomerID)
    {
        return BadRequest();
    }

    customer.ObjectState = ObjectState.Modified;
    _customerService.Update(customer);

    try
    {
        await _unitOfWorkAsync.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!CustomerExists(key))
        {
            return NotFound();
        }
        throw;
    }

    return Updated(customer);
}

// POST: odata/Customers
public async Task<IHttpActionResult> Post(Customer customer)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    customer.ObjectState = ObjectState.Added;
    _customerService.Insert(customer);

    try
    {
        await _unitOfWorkAsync.SaveChangesAsync();
    }
    catch (DbUpdateException)
    {
        if (CustomerExists(customer.CustomerID))
        {
            return Conflict();
        }
        throw;
    }

    return Created(customer);
}

//// PATCH: odata/Customers(5)
[AcceptVerbs("PATCH", "MERGE")]
public async Task<IHttpActionResult> Patch([FromODataUri] string key, Delta<Customer> patch)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    Customer customer = await _customerService.FindAsync(key);

    if (customer == null)
    {
        return NotFound();
    }

    patch.Patch(customer);
    customer.ObjectState = ObjectState.Modified;

    try
    {
        await _unitOfWorkAsync.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!CustomerExists(key))
        {
            return NotFound();
        }
        throw;
    }

    return Updated(customer);
}

// DELETE: odata/Customers(5)
public async Task<IHttpActionResult> Delete(string key)
{
    Customer customer = await _customerService.FindAsync(key);

    if (customer == null)
    {
        return NotFound();
    }

    customer.ObjectState = ObjectState.Deleted;

    _customerService.Delete(customer);
    await _unitOfWorkAsync.SaveChangesAsync();

    return StatusCode(HttpStatusCode.NoContent);
}

// GET: odata/Customers(5)/CustomerDemographics
[Queryable]
public IQueryable<CustomerDemographic> GetCustomerDemographics([FromODataUri] string key)
{
    return
        _customerService.ODataQueryable()
            .Where(m => m.CustomerID == key)
            .SelectMany(m => m.CustomerDemographics);
}

// GET: odata/Customers(5)/Orders
[Queryable]
public IQueryable<Order> GetOrders([FromODataUri] string key)
{
    return _customerService.ODataQueryable().Where(m => m.CustomerID == key).SelectMany(m => m.Orders);
}

protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        _unitOfWorkAsync.Dispose();
    }
    base.Dispose(disposing);
}

private bool CustomerExists(string key)
{
    return _customerService.Query(e => e.CustomerID == key).Select().Any();
}
}

This is fairly straight forward, and we used the Visual Studio 2013 Update 2 RC out of the box Controller scaffolding to generate most of this code. We then replaced the code that was directly using Entity Framework to use a Repository and Service pattern, for more information on the pattern used please have a quick read here: https://genericunitofworkandrepositories.codeplex.com.

Visual Studio 2013 with Update 2 RC Web API OData Scaffolding Template

2014-05-01_1-10-42

Animating our View Swapping

To polish our UX (user experience) a bit, were going to add some animation to how AngularJS swaps view in our “ng-view” div.

Northwind.Web/Content/Site.css


body {
    padding-top: 50px;
    padding-bottom: 20px;
}

/* Set padding to keep content from hitting the edges */
.body-content {
    padding-left: 15px;
    padding-right: 15px;
    position: relative;
}

/* Set width on the form input elements since they're 100% wide by default */
input,
select,
textarea {
    max-width: 280px;
}

[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
    display: none !important;
}

.ng-enter, .ng-leave {
    -webkit-transition: all cubic-bezier(0.250, 0.460, 0.450, 0.940) .5s;
    transition: all cubic-bezier(0.250, 0.460, 0.450, 0.940) .5s;
    display: block;
    width: 1170px;
    padding-left: 15px;
    padding-right: 15px;
    position: absolute;
    left: 0;
    right: 0;
}

.ng-enter {
    left: 100%;
}

    .ng-enter.ng-enter-active {
        left: 0;
    }

.ng-leave.ng-leave-active {
    left: -200%;
}

We’ve added some rudimentary animation with .ng-enter and .ng-leave, both which the cubic-brezier, to slide in our Views. This implementation was originally based off AngularJS original animation documentation for “ng-view”, please have a quick read up here: https://docs.angularjs.org/api/ngRoute/directive/ngView for more details.

There you have it AngularJS, Kendo UI with Angular Kendo and ASP.NET MVC 5, all harmoniously with the best of both worlds.

Happy Coding…! :)

Source code: http://goo.gl/f1esAf
Live demo: http://longle.azurewebsites.net

Stay tuned for integrating this with RequireJS for large scale apps!

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

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

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

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

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

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

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

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

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

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

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

You’ll need KendoUIWeb NuGet package.

6-18-2013 1-40-51 AM

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

Index.html


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

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

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

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

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

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

TemplateLoader Helper


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

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

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

Kendo UI Web Router


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

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

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

            router.start();
        });

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

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

addRoute Helper


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

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

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

6-17-2013 10-41-19 PM

Spa/Content/views/layout.html


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

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

<div d="body">

Spa/Content/views/home.html


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

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

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

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

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

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

Spa/Content/views/contact.html


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

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

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

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

Spa/Content/views/about.html


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

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

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

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

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

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

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

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

Stay tune for Part 2 and 3…

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