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!

About these ads

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

    • For the most part nothing, other than you don’t have to download it separately and it’s officially supported.

      Like

  1. Hi.Le. thanks for article. i have question

    if set BundleTable.EnableOptimizations = true; in your example will work ?.my not working :(

    Like

  2. Hi Le

    Can you please explain where do you “register” the few JavaScript files created in this article (app.js, homeController.js etc. I am presuming that you placed them in the _Layout.cshtml file in the head section – but am not really sure as you did not write that anywhere.

    Also, I am assuming that the initial skeleton of the Northwind.Web project is created by Visual Studio as an ASP.NET MVC application. If that is the case, then path to the “razor” file

    {{title}}
    ...
    

    is incorrect – it should be Northwind.Web/Views/Home/Index.cshtml instead of Northwind.Web/Views/Home/Home.cshtml

    I suspect that because of that I cannot see the Angular controller getting activated and consequently the angular expression {{title}} (which should be set by $scope.title = “ASP.NET”;) remains undefined.

    Thanks in advance

    NIk

    Like

    • In the meantime I managed to address all my issues (all related to placement of various references to Javascript files in the _Layout.cshtml file). I will be happy to explain this once it is established that someone can benefit from that data :-)

      Like

  3. Pingback: Angular-Kendo.js examples | Sriramjithendra Nidumolu

  4. Pingback: To see 7315-BECBY SKF Single Row Angular Contact Ball Bearing Cheap | Car Bearings

  5. Inspiring. Love the sample on Github, though It is missing Northwind.Web\Scripts\app\main.js and mainController.js.

    Your IService saved me heaps of time with generic Repositories and Services. Swapped out Unity for AutoFac.

    Am intending to extend it with a bunch of extra features from (https://gist.github.com/mombrea/620c2b58737e07fc82bb#file-baseservice-cs) and paging from (https://github.com/tugberkugurlu/GenericRepository/blob/bc05f4d1007596923400997727ae70fc3e87bea4/src/GenericRepository.EntityFramework/IEntityRepository'2.cs). Happy to chat.

    Like

  6. Glad to see somebody finally talk about how AngularJS and MVC can live together. Im wondering if it makes sense to use MVC for certain pages over AngularJS in certain cases? Is there anything the MVC.cshtml framework can help you with that is better than AngluarJS (besides the 3 things you mentioned)

    Like

  7. No Problem.
    Please add the following code snipet to access oData using a .NET client as an option. Not all the clients app have access to the service from Angular. The following sample use Web Api as a façade.
    The urlRemoteBase contains the remote odata location. “URLnotOnlyaccesiblefromServer/odata/Customer”;
    The urlQuery has the query odata parameters. as “?%24inlinecount=allpages&%24top=10″

    
    CustomeDataSource.Js change to consume from Web APi
    northwindApp.factory('customerDataSource',
        function (customerModel) {
            var crudServiceBaseUrl = "/api/Sample";
    
    

    Sample .NET client consuming oData

    public class SampleController : ApiController
        {
            // GET: Sample
            public async Task GetSample()
            {
                var urlRemoteBase = "http://localhost:12106/odata/Customer";<br />
                var urlQuery = HttpContext.Current.Request.Url.Query;
                var url = string.Format("{0}{1}", urlRemoteBase, urlQuery);
    
    var uri = new Uri(url);
            var httpClient = new HttpClient();
            httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            var response = await httpClient.GetAsync(uri);
            var responseContent = await response.Content.ReadAsStringAsync();
            var jsonResult = JsonConvert.DeserializeObject(responseContent);
            return Ok(jsonResult);
        }
    }
    

    Like

  8. Hi Le,
    I just noticed that the public IQueryable GetCustomer() is being called twice.
    Thanks,
    DG

    Like

    • Hi Le,

      What is the fix for this? I downloaded the sample code just a few weeks ago and it still exhibits this behavior.

      Nice series btw. I’ve been gobbling it all up. :)

      Thanks,
      Melani

      Like

Comments are closed.