December 2014

Volume 29 Number 12

Node.js : Building Web Apps on the MEAN Stack with OData in Microsoft Azure

Long Le

Microsoft .NET developers typically build great apps using JavaScript on the client side and ASP.NET (C# or Visual Basic .NET) on the server side. But what if you could use one common language to build apps on all layers of the stack, everything from the browser and the services layer to server-side business processing, and even to querying and programming in the database? Now you can, with Node.js. Node.js has been around for a number of years, but its adoption has picked up significantly in recent years. Node.js stacks, such as the MongoDB, Express, AngularJS, Node.js (MEAN) stack, bring many benefits to building apps, including the fact that there’s very little disconnect (if any), between front-end, middle-layer and back-end developers. In many cases the same programmer can develop all the layers of an app, because it’s all done in JavaScript. Moreover, you can now build Node.js apps directly within Visual Studio 2013 with Node.js Tools for Visual Studio (NTVS), including full debugging capability.

Getting Started

In this article I’m going to show you that by using the MEAN stack, building create, read, update and delete (CRUD)-heavy applications can be fast and easy. I’m going to assume you have a basic conceptual understanding of AngularJS (angularjs.org), Node.js (nodejs.org), MongoDB (mongodb.org) and Express (expressjs.com). If you’re planning to follow along, please be sure you have the following installed:

The first step is to open the New Project dialog in Visual Studio and choose the Blank Microsoft Azure Node.js Web Application template, as shown in Figure 1. You could shortcut a few items by choosing the Basic Microsoft Azure Express Application template, but a blank template provides more granular control over what to install as middleware for the Node.js application.

Create a Blank Microsoft Azure Node.js Web Application
Figure 1 Create a Blank Microsoft Azure Node.js Web Application

What is Node.js middleware? To oversimplify, it’s simply modules you can plug in to your Node.js application’s Express HTTP request pipeline. Typically, the middleware gets executed for every HTTP request.

Next, install Express using the Node Package Manager (NPM). If you’re familiar with NuGet packages, NPM packages are basically the same thing, but for Node.js applications.

As you can see in Figure 2, I added @3 in the Other npm arguments text field, in order to install the latest version of Express 3. Although Express 4 has been released, you need to stick with Express 3 because the other modules that will be installed haven’t been updated to some of the breaking changes of Express 4.

Finding and Installing NPM Packages Such as Express
Figure 2 Finding and Installing NPM Packages Such as Express

You’ll need to download and install the rest of the required NPM packages: express, odata-server, stringify-object and body-parser, but there’s no need to have any “Other npm arguments,” as I’ll be using the latest version of each of these npm packages.

Setting up the Server.js File

The server.js (sometimes named app.js) file, shown in Figure 3, is basically the starting point of the Node.js app. This is where you configure your application and inject any needed middleware modules.

Figure 3 The Server.js File

1    var http = require('http');
2    var express = require( 'express' );
3    var odata = require( './server/data/odata' );
4    var stringify = require( 'stringify-object' );
5    var config = require("./server/config/config");
6    var bodyParser = require("body-parser");
7    var app = express( );
8    odata.config( app );
9    app.use(bodyParser.json());
10   app.use( express.static( __dirname + "/public" ) );
11   var port = process.env.port || 1337;
12   app.get("/", function(req, res) {
13   res.sendfile("/public/app/views/index.html", { root: __dirname });
14   });
15   http.createServer(app).listen(port);
16   console.log(stringify( process.env ));

In order to consume the required NPM packages/libraries that you download, you need to use the keyword require(“package name”) to bring these libraries in scope for a given Node.js class, as shown in lines 1 to 6 in Figure 3. I’ll quickly review the contents of server.js:

  • Line 1-6: Bring all the required packages into the server.js scope so they can be initialized and plugged into the HTTP request pipeline.
  • Line 7: Initialize a new Express Web application.
  • Line 8: Define the OData configuration for the REST endpoints; more on this in a bit.
  • Line 10: Plug in express.static and pass the directory path to expose the directory path that’s passed in publicly. This lets anyone reach any content placed in the NodejsWebApp/Public directory. For example, https://localhost:1337/image/myImage.gif would render the image in NodejsWebApp/Public/image/myimage.gif to the browser.
  • Line 12: Set up a default landing page using the app.get method. The first parameter takes in the path (the root path of the app). Here, I’m simply rendering a static HTML file by providing the path to it.
  • Line 15: Instruct the application to listen and serve HTTP requests on the specified port; for development purposes I’m using port 1337, so my application will listen for requests at https://localhost:1337.
  • Line 16: Print the environment variables to the Node.js console window to bring some visibility to the Node.js environment.

Configuring OData

With server.js set up, I’m going to focus now on Line 8, where I configure the OData REST endpoints. First, you’ll need to create two modules: NodejsWebApp/server/data/northwind.js (Figure 4) and NodejsWebApp/server/data/odata.js (Figure 5).

Figure 4 NodejsWebApp/server/data/northwind.js

$data.Entity.extend( 'Northwind.Category', {
  CategoryID: { key: true, type: 'id', nullable: false, computed: true },
  CategoryName: { type: 'string', nullable: false, required: true, maxLength: 15 },
  Description: { type: 'string', maxLength: Number.POSITIVE_INFINITY },
  Picture: { type: 'blob', maxLength: Number.POSITIVE_INFINITY },
  Products: { type: 'Array', elementType: 'Northwind.Product', inverseProperty: 'Category' }
} );
$data.Entity.extend( 'Northwind.Product', {
  ProductID: { key: true, type: 'id', nullable: false, computed: true },
  ProductName: { type: 'string', nullable: false, required: true, maxLength: 40 },
  EnglishName: { type: 'string', maxLength: 40 },
  QuantityPerUnit: { type: 'string', maxLength: 20 },
  UnitPrice: { type: 'decimal' },
  UnitsInStock: { type: 'int' },
  UnitsOnOrder: { type: 'int' },
  ReorderLevel: { type: 'int' },
  Discontinued: { type: 'bool', nullable: false, required: true },
  Category: { type: 'Northwind.Category', inverseProperty: 'Products' },
  Order_Details: { type: 'Array', elementType: 'Northwind.Order_Detail', 
  inverseProperty: 'Product' },
  Supplier: { type: 'Northwind.Supplier', inverseProperty: 'Products' }
} );
$data.Class.define( "NorthwindContext", $data.EntityContext, null, {
  Categories: { type: $data.EntitySet, elementType: Northwind.Category },
  Products: { type: $data.EntitySet, elementType: Northwind.Product },
  // Other entity registrations removed for brevity, please see actual source code.
} );
// Other entity definitions removed for brevity, please see actual source code.
NorthwindContext.generateTestData = function( context, callBack ) {
  var category1 = new Northwind.Category( { CategoryName: 'Beverages',
    Description: 'Soft drinks, coffees, teas, beer, and ale' } );
  // Other category instances removed for brevity, please see actual source code.
  context.Categories.add( category1 );
  // Other category inserts removed for brevity, please see actual source code.
  context.Products.add( new Northwind.Product( 
   { ProductName: 'Ipoh Coffee', EnglishName: 'Malaysian Coffee',
    UnitPrice: 46, UnitsInStock: 670, Discontinued: false, Category: category1 } ) );
  // Other product inserts removed for brevity, please see actual source code.
  context.saveChanges( function ( count ) {
    if ( callBack ) {
      callBack( count );
    }
  } );
};
module.exports = exports = NorthwindContext;

Figure 5 NodejsWebApp/server/data/odata.js Module

( function (odata) {
  var stringify = require( 'stringify-object' );
  var config = require( "../config/config" );
  console.log( stringify( config ) );
  odata.config = function ( app ) {
    var express = require( 'express' );
    require( 'odata-server' );
    var northwindContextType = require( './northwind.js' );
    var northwindContext = new NorthwindContext( {
      address: config.mongoDb.address,
      port: config.mongoDb.port,
      username: config.mongoDb.username,
      password: config.mongoDb.password,
      name: config.mongoDb.name,
      databaseName: config.mongoDb.databaseName,
      dbCreation: $data.storageProviders.DbCreationType.DropAllExistingTables
    } );
    console.log( "northwindContext :" );
    stringify( northwindContext );
    northwindContext.onReady( function ( db ) {
      northwindContextType.generateTestData( db, function ( count ) {
        console.log( 'Test data upload successful. ', count, 'items inserted.' );
        console.log( 'Starting Northwind OData server.' );
        app.use( express.basicAuth( function ( username, password ) {
          if ( username == 'admin' ) {
            return password == 'admin';
          } else return true;
        } ) );

Note that MongoDB is a NoSQL database—that is, a non-relational document database. When migrating a traditional Northwind database to MongoDB to take advantage of the NoSQL model, there can be many ways to structure it. For this article, I’ll leave the Northwind schema, for the most part, intact. (I’ve removed other entity model definitions, registrations, and inserts from Figure 4 for brevity.)

In Figure 4 the models and entities are simply being defined, which can be reused later on the client side when performing CRUD operations, such as creating new Products, for example. Also, the NorthwindContext.generateTestData method will seed the database every time the application is restarted, which will come in handy when you deploy the application to a live demo site. This makes it easy to refresh the data whenever needed simply by recycling the application. A more elegant approach would be to wrap this code into an Azure WebJob and schedule it to refresh at a set frequency, but leave it as is for now. The last line in this module, module.exports = exports = NorthwindContext, wraps everything so that later on you can “require” this module and use the “new” operator to create a new instance of the Northwind object type, which is done in the NodejsWebApp/server/data/odata.js module, shown in Figure 5.

You can query MongoDB via the command line or by using one of the many MongoDB GUI tools out there (such as RoboMongo) to confirm that the seed data was indeed inserted. Because the focus of this article is on OData, use LINQPad because it includes a built-in provider to query with LINQ against OData version 3.

To test the endpoints, download and install LINQPad (linqpad.net), and then run your application (F5 in Visual Studio 2013). Then fire up LINQPad and set up a new connection to the OData endpoint. To do so, click Add connection and select OData as your LINQPad data provider. Then configure the OData LINQ connection with the URI https://localhost:1337/northwind.svc; username, Admin; and password, Admin. LINQPad will render the hierarchy based on the OData CSDL endpoint, as you can see in the upper-left corner of Figure 6.

A LINQ Query and Its Results Using the Discovered Data Model
Figure 6 A LINQ Query and Its Results Using the Discovered Data Model

There should be data for Products based on the seed data used on the server-side (NodejsWebApp/server/northwind.js), so you’ll want to do a quick LINQ query on Products using LINQPad:

Products.Take(100)

Figure 6 also shows the query and its results.

As you can see, the OData server is properly set up and you’re able to issue LINQ queries over HTTP and get a list of products back. If you click on the Request Log tab, you can actually see the HTTP GET OData URL LINQPad generates from the LINQ statement: https://localhost:1337/northwind.svc/Products()?$top=100.

Once you’ve confirmed your OData server is indeed running on the Node.js Express Web app, you’ll want to make use of this and start building out some common use cases that can consume that OData goodness. Place everything on the client side in the “public” folder and all code that runs on the server side in a folder named Server. Create all the files needed for your app in advance as stubs or placeholders, and then come back around and fill in the blanks. Figure 7 shows the structure of the NodejsWebApp project.

The NodejsWebApp Project
Figure 7 The NodejsWebApp Project

The app.js file (NodejsWebApp/public/app/app.js) shown in Figure 8 is basically the starting point of the (client-side) AngularJS application. I won’t go into all the details; the takeaway here is that you want to register your client-side routes for your single-page application (SPA) with the $routeProvider. For each of the routes (defined with the .when method), provide a path to the view (HTML) to render by setting the templateUrl property, and specify a view’s controller by setting the controller property for a given route. The AngularJS controller is where all of the code lives to facilitate whatever the view requires—in short, all the JavaScript code for the view. The .otherwise method is used to configure a default route (the home view) for any incoming requests that don’t match any of the routes.

Figure 8 The App.js File

'use strict';
var myApp = angular.module('myApp',
  [
    'ngRoute',
    'ngAnimate',
    'kendo.directives',
    'jaydata'
  ])
  .factory("northwindFactory",
  [
    '$data',
    '$q',
    function($data, $q) {
      // Here you wrap a jquery promise into an angular promise.
      // Simply returning jquery promise causes bogus things
      var defer = $q.defer();
      $data.initService("/northwind.svc").then(function(ctx) {
        defer.resolve(ctx);
      });
      return defer.promise;
    }
  ])
  .config(function($routeProvider) {
    $routeProvider
      .when('/home',
      {
        templateUrl: 'app/views/home.html'
      })
      .when('/product',
      {
        templateUrl: 'app/views/product.html',
        controller: 'productController',
        resolve: {
          northwind: 'northwindFactory'
        }
      })
      .when('/edit/:id',
      {
        templateUrl: 'app/views/edit.html',
        controller: 'editController',
        resolve: {
          northwind: 'northwindFactory'
        }
      })
      .when('/chart',
      {
        templateUrl: 'app/views/chart.html',
        controller: 'chartController',
        resolve: {
          northwind: 'northwindFactory'
        }
      })
      .otherwise(
      {
        redirectTo: '/home'
      });
  });

Here’s a quick recap of how the concerns of the Model-View-ViewModel (MVVM) pattern are represented in the app:

  • View = *.html
  • ViewModel = *controller.js
  • Model = entities that are returned from REST endpoints, usually are domain models and/or entities

Figure 9 shows which files in the application address which concern in the MVVM pattern.

The Model-View-ViewModel Pattern
Figure 9 The Model-View-ViewModel Pattern

Defining the JayData Client-Side DataContext as an AngularJS Service

Because most of the controllers will use the Northwind context, you’ll want to create a service/factory named northwindFactory. And because the initialization of the Northwind context is async, you’ll want to set up a JavaScript promise to ensure the Northwind context initialization has completed and is ready to be used by the time any of the controllers are loaded. So, in short, the Northwind context will finish loading before any controller with a dependency on the northwindFactory loads. Notice that all of the configured routes have a “resolve” property, which is how you define what promises need to resolve before the controller is loaded. In this case, the property, “northwind,” is set to the northwindFactory. The property name “northwind” will also be the name of the instance that will be injected into the controller. You’ll see the constructor function for productController.js in a bit (in Figure 11), where the northwindFactory is injected as northwind, the property name that’s set for northwindFactory in the resolve property in the routes.

Index.html, shown in Figure 10, will basically be the layout page and AngularJS will know which views to swap into the div with the attribute ng-view. Note that you have to specify the AngularJS app by configuring any HTML element that’s a parent element of the div attributed with “ng-view.” In this case you want to set “ng-app” to “myApp,” which is what the application is named in the app.js.

Figure 10 The Index.html File

<!DOCTYPE html>
<html xmlns="https://www.w3.org/1999/xhtml">
  <head>
    <meta charset="utf-8" />
    <title>NodejsWebApp</title>
    <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"
      rel="stylesheet">
    <link href="//cdn.kendostatic.com/2014.2.716/styles/kendo.common.min.css"
      rel="stylesheet" />
    <link href="//cdn.kendostatic.com/2014.2.716/styles/kendo.bootstrap.min.css"
      rel="stylesheet" />
    <link href="//cdn.kendostatic.com/2014.2.716/styles/kendo.dataviz.min.css"
      rel="stylesheet" />
    <link href="//cdn.kendostatic.com/2014.2.716/styles/
      kendo.dataviz.bootstrap.min.css" rel="stylesheet" />
    <link href="../../css/site.css" rel="stylesheet" />
  </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 class="navbar-brand" href="/">NodejsWebApp</a>
        </div>
        <div class="navbar-collapse collapse">
          <ul class="nav navbar-nav">
            <li>
              <a href="#/home">Home</a>
            </li>
            <li>
              <a href="#/about">About</a>
            </li>
            <li>
              <a href="#/contact">Contact</a>
            </li>
            <li>
              <a href="#/product">Product</a>
            </li>
            <li>
              <a href="#/chart">Chart</a>
            </li>
          </ul>
        </div>
       </div>
    </div>
    <!-- Binding the application to our AngularJS app: "myApp" -->
    <div class="container body-content" ng-app="myApp">
      <br />
      <br/>
      <!-- AngularJS will swap our Views inside this div -->
      <div ng-view></div>
      <hr />
      <footer>
        <p>&copy; 2014 - My Node.js Application</p>
      </footer>
    </div>
    <script src="//code.jquery.com/jquery-2.1.1.min.js"></script>
    <script src=
      "//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js">
    </script>
    <script src="//code.angularjs.org/1.3.0-beta.16/angular.min.js"></script>
    <script src="//code.angularjs.org/1.3.0-beta.16/angular-route.min.js"></script>
    <script src="//code.angularjs.org/1.3.0-beta.16/angular-animate.min.js"></script>
    <script src="//cdn.kendostatic.com/2014.2.716/js/kendo.all.min.js"></script>
    <script src="//include.jaydata.org/datajs-1.0.3-patched.js"></script>
    <script src="//include.jaydata.org/jaydata.js"></script>
    <script src="//include.jaydata.org/jaydatamodules/angular.js"></script>
    <script src="/lib/jaydata-kendo.js"></script>
    <!--<script src="//include.jaydata.org/jaydatamodules/kendo.js"></script>-->
    <script src="/app/app.js"></script>
    <script src="/app/controllers/productController.js"></script>
    <script src="/app/controllers/chartController.js"></script>
    <script src="/app/controllers/editController.js"></script>
  </body>
</html>

Note that I’m using a content delivery network (CDN) for all of my client-side JavaScript library includes. You can download the client-side libraries locally using Bower at the command line (as you’d typically do for .NET projects with NuGet using the Package Manager console). In the Microsoft .NET Framework you use NuGet for both client-side and server-side packages. In the Node.js realm, however, Bower is used to download client-side libraries/packages while NPM is used to download and install server-side libraries/packages.

For the layout UI, I use a vanilla bootstrap theme, the one that the Visual Studio ASP.NET MVC 5 project template generates.

Product View

Just a few lines of HTML are needed for the product view (NodejsWebApp/public/app/views/products.html). The first block is the Kendo directive for AngularJS to render the grid:

<!-- Kendo UI's AngularJS directive for the grid -->
<div kendo-grid="grid" k-options="options"></div>
<!-- AngularJS template for our View Detail Button in the Grid Toolbar -->
<script type="text/x-kendo-template" id="viewDetail">
  <a
    class="k-button "
    ng-click="viewDetail(this)">View Detail</a>
</script>

The second block is just an AngularJS template for the custom View Detail button you add to the grid’s toolbar.

Figure 11 shows the Product Controller, NodejsWebApp/app/controllers/productController.js.

Figure 11 The Product Controller

myApp.controller("productController",
  function($scope, northwind, $location) {
    var dataSource =
      northwind
        .Products
        .asKendoDataSource({ pageSize: 10 });
    $scope.options = {
      dataSource: dataSource,
      filterable: true,
      sortable: true,
      pageable: true,
      selectable: true,
      columns: [
        { field: "ProductID" },
        { field: 'ProductName' },
        { field: "EnglishName" },
        { field: "QuantityPerUnit" },
        { field: "UnitPrice" },
        { field: 'UnitsInStock' },
        { command: ["edit", "destroy"] }
      ],
      toolbar: [
        "create",
        "save",
        "cancel",
        {
          text: "View Detail",
          name: "detail",
          template: $("#viewDetail").html()
        }
      ],
      editable: "inline"
    };
    $scope.viewDetail = function(e) {
      var selectedRow = $scope.grid.select();
      if (selectedRow.length == 0)
        alert("Please select a row");
      var dataItem = $scope.grid.dataItem(selectedRow);;
      $location.url("/edit/" + dataItem.ProductID);
    };
  });

To hydrate the Products Grid, you need to instantiate a Kendo UI DataSource ($scope.options.dataSource). JayData provides a helper method to initialize a Kendo UI DataSource bound to its OData REST endpoints. The JayData asKendoDataSourcehelper method knows how to create the DataSource based on the metadata information published by the OData server (https://localhost:1337/northwindsvc), which is then used to configure the $data instance in northwindFactory in app.js. You’ll see more of the Kendo DataSource when I demonstrate visual impressions with the Kendo DataViz charting framework.

Along with the out-of-the-box buttons (create, save and cancel) configured in the grid’s toolbar, you add a custom button to navigate to another view that will render the complete details of a selected product row ($scope.viewDetail). When the View Detail button click event occurs, get the selected product DataItem and then, using the AngularJS $location service, navigate to the edit view (MyNodejsWebApp/scripts/app/views/edit.html) for the product.

Figure 12 shows the Edit.html file, NodejsWebApp/public/app/views/edit.html.

Figure 12 The Edit.html File

<div class="demo-section">
  <div class="k-block" style="padding: 20px">
    <div class="k-block k-info-colored">
      <strong>Note: </strong>Please fill out all of the fields in this form.
    </div>
    <div>
      <dl>
        <dt>
          <label for="productName">Name:</label>
        </dt>
        <dd>
          <input id="productName" type="text"
            ng-model="product.ProductName" class="k-textbox" />
        </dd>
        <dt>
          <label for="englishName">English Name:</label>
        </dt>
        <dd>
          <input id="englishName" type="text"
            ng-model="product.Englishname" class="k-textbox" />
        </dd>
        <dt>
          <label for="quantityPerUnit">Quantity Per Unit:</label>
        </dt>
        <dd>
          <input id="quantityPerUnit" type="text"
            ng-model="product.QuantityPerUnit" class="k-textbox" />
        </dd>
        <dt>
          <label for="unitPrice">Unit Price:</label>
        </dt>
        <dd>
          <input id="unitPrice" type="text"
            ng-model="product.UnitPrice" class="k-textbox" />
        </dd>
        <dt>
          <label for="unitsInStock">Units in Stock:</label>
        </dt>
        <dd>
          <input id="unitsInStock" type="text"
            ng-model="product.UnitsInStock" class="k-textbox" />
        </dd>
        <dt>
          <label for="reorderLevel">Reorder Level</label>
        </dt>
        <dd>
          <input id="reorderLevel" type="text"
            ng-model="product.ReorderLevel" class="k-textbox" />
        </dd>
        <dt>
          <label for="discontinued">Discontinued:</label>
        </dt>
        <dd>
          <input id="discontinued" type="text"
            ng-model="product.Discontinued" class="k-textbox" />
        </dd>
        <dt>
          <label for="category">Category:</label>
        </dt>
        <dd>
           <select
             kendo-drop-down-list="dropDown"
             k-data-text-field="'CategoryName'"
             k-data-value-field="'CategoryID'"
             k-data-source="categoryDataSource"
             style="width: 200px"></select>
        </dd>
      </dl>
      <button kendo-button ng-click="save()"
        data-sprite-css-class="k-icon k-i-tick">Save</button>
      <button kendo-button ng-click="cancel()">Cancel</button>
      <style scoped>
        dd {
          margin: 0px 0px 20px 0px;
          width: 100%;
        }
        label {
          font-size: small;
          font-weight: normal;
        }
        .k-textbox { width: 100%; }
        .k-info-colored {
          margin: 10px;
          padding: 10px;
        }
      </style>
    </div>
  </div>
</div>

Notice how the inputs are decorated with the ng-model attribute, which is the AngularJS way of declaratively indicating that the value for that input will be stored in a property the ng-model value is set to on the controller $scope. For example, in the first input field in this view, whose HTML element id is set to productName (id=“productName”), ng-model is set to product.ProductName. This means that whatever the user enters in the input field (textbox), the value for $scope.productName will be set accordingly. Moreover, whatever $scope.product.productName is set to programmatically in editController will be automatically reflected in the value of the input field for productName.

As an example, when the view first loads, you load the product by the ID passed in through URL, then set $scope.product to that product (see Figure 13).  Once this happens, everything in the view with ng-model set to $scope.property.* will reflect all the property values in $scope.product. In the past, developers typically set values in input fields using jQuery or straight JavaScript for any type of manipulation of the DOM. When building an application with the MVVM pattern (regardless of the framework), best practice is to manipulate the DOM only through changes to the ViewModel, never directly (for example, with JavaScript or jQuery). I’m definitely not implying there’s anything wrong with JavaScript or jQuery, but if you decide to use a pattern to solve a specific problem (in my case, MVVM to maintain the separation of concerns between the View, ViewModel and Model), it should be consistent throughout your application.

Figure 13 The editController.js File

myApp.controller("editController",
  function($scope, northwind, $routeParams, $location) {
    var productId = $routeParams.id;
    $scope.categoryDataSource = northwind.Categories.asKendoDataSource();
    northwind
      .Products
      .include("Category")
      .single(
        function(product) {
          return product.ProductID == productId;
        },
        { productId: productId },
        function(product) {
          $scope.product = product;
          northwind.Products.attach($scope.product);
          $scope.dropDown.value($scope.product.Category.CategoryID);
          $scope.$apply();
        });
    $scope.save = function() {
      var selectedCategory = $scope
                            .categoryDataSource
                            .get($scope.product.Category.CategoryID);
      console.log("selecctedCategory: ", selectedCategory.innerInstance());
      $scope.product.Category = selectedCategory.innerInstance();
      // Unwrap kendo dataItem to pure JayData object
      northwind.saveChanges();
    };
    $scope.cancel = function() {
      $location.url("/product");
    };
  });

Note that you could implement a POST server-side action on Node.js, which is what is typically done with the ASP.NET Web API. However, the purpose here is to demonstrate how to do this with Node.js and OData:

app.post('/api/updateProduct', function(req, res) {
  var product = req.body;
  // Process update here, typically what is done with the ASP.NET Web API
});

For the chart view (NodejsWebApp/public/app/views/chart.html), you need just one line of markup:

<kendo-chart k-options="options"></kendo-chart>

All that’s happening here is declaring the Kendo UI Bar Chart directive, setting these options to bind to a property in the controller named options. Figure 14 shows the product chart view and Figure 15 shows the product chart controller.

Product Chart View
Figure 14 Product Chart View

Figure 15 The Product Chart Controller

myApp.controller("chartController",
  function($scope, northwind) {
    var dataSource = northwind.Products.asKendoDataSource();
    $scope.options = {
      theme: "metro",
      dataSource: dataSource,
      chartArea: {
        width: 1000,
        height: 550
      },
      title: {
        text: "Northwind Products in Stock"
      },
      legend: {
        position: "top"
      },
      series: [
        {
          labels: {
            font: "bold italic 12px Arial,Helvetica,sans-serif;",
            template: '#= value #'
          },
          field: "UnitsInStock",
          name: "Units In Stock"
        }
      ],
      valueAxis: {
        labels: {
          format: "N0"
        },
        majorUnit: 100,
        plotBands: [
          {
            from: 0,
            to: 50,
            color: "#c00",
            opacity: 0.8
          }, {
            from: 50,
            to: 200,
            color: "#c00",
            opacity: 0.3
          }
        ],
        max: 1000
      },
      categoryAxis: {
        field: "ProductName",
        labels: {
          rotation: -90
        },
        majorGridLines: {
          visible: false
        }
      },
      tooltip: {
        visible: true
      }
    };
  });

As with productController.js, here you also inject the northwindFactory as northwind in the controller construction function, again creating a Kendo dataSource with the JayData helper asKendoDataSource method. Here are more details about what happens in the chart controller:

$scope.options.series

  • type: This configures the type of chart.
  • field: The field from the model/entity that will be used for the series (x-axis) value.

$scope.options.valueAxis

  • majorUnit: The interval between major divisions. If the valueAxis.type is set to log, the majorUnit value will be used for the base of the logarithm.
  • plotBands: The plot bands for the graph, which are used to visually indicate the product quantity. If the quantity falls below a specified level, the user should invoke the product restocking process.
  • max: The maximum value for the y-axis.

$scope.options.categoryAxis

  • field: The field labels for the x-axis.
  • labels.rotation: The degrees to rotate the labels. Here you configure the labels on the x-axis to be perpendicular to the running x-axis by setting the value to -90 (degrees), that is, rotate the labels counterclockwise by 90 degrees.
  • majorGridLines.visible: Turns the major gridlines on or off. You may want to turn them off for cosmetic reasons, to give the chart a cleaner and more polished look.
  • tooltip.visible: This enables tooltips when a user hovers over a vertical bar.

Please see the Kendo UI Chart API for details at bit.ly/1owgWrS.

Azure Web Site Deployment

Because the source code is conveniently hosted in a CodePlex Git repository, using Azure Web Sites to set up continuous deployment (continuous delivery) is as simple as it gets:

  1. Navigate to your Azure Web site dashboard and select Set up deployment from source control.
  2. Select your repository; for this example, select CodePlex.
  3. Click Next.
  4. Select your CodePlex project.
  5. Select the branch.
  6. Click Check.
  7. For every sync to your Git repository, a build and deployment will occur.

That’s it. With a few simple clicks, your application is deployed with continuous integration and delivery. For more information on deploying with Git, visit bit.ly/1ycBo9S.

As a .NET developer, I’ve come to really enjoy how quick and easy it is to build CRUD-heavy applications with ASP.NET MVC, ASP.NET Web API, OData, Entity Framework, AngularJS and Kendo UI. Now, by developing on the MEAN stack, I can still leverage much of this domain knowledge and experience, with the help of the JayData libraries. The only difference between the two stacks is the server-side layer. If you’ve been developing with ASP.NET MVC and the ASP.NET Web API, Node.js shouldn’t present many problems because you already have some basic JavaScript experience. You’ll find complete source code for the example in this article at msdnmeanstack.codeplex.com, and a live demo at meanjaydatakendo.azurewebsites.net.


Long Le is the principal app/dev architect at CBRE Inc. and a Telerik/Kendo UI MVP. He spends most of his time developing frameworks and application blocks, providing guidance for best practices and patterns, and standardizing the enterprise technology stack. In his spare time, he enjoys blogging (blog.longle.net), playing Call of Duty, or mentoring (codementor.io/lelong37). You can reach and follow him on Twitter at twitter.com/LeLong37.

Thanks to the following technical experts for reviewing of this article: Robert Bany (JayData), Burk Holland (Telerik) and Peter Zentai (JayData)