Using an Abstract AngularJS factory to populate a KendoUI DataSource from an OData Endpoint

Since being introduced to AngularJS, I have enjoyed how robust the framework is, and provide a great layer of abstraction to enable client-side unit testing.   Creating an abstract factory that takes in parameters isolates the client-side data access point and makes it easily testable.

My reason for writing this is that I was unable to find any sources that gave a full end-to-end example (let alone integrating a KendoUI datasource with AngularJS and OData).  KendoUI’s datasource transports make it really simple to connect to an OData endpoint, but I want to Angularize my data calls.

Setup

To manage ContentType data, I used a KendoUI grid to handle all CRUD operations from a simple, intuitive interface.  Using Entity Framework, MVC5, WebAPI2, I exposed an OData endpoint at ~/odata/ContentType.

Setting up an OData endpoint through WebAPI is out of the scope of this article and using VisualStudio 2013 makes it quite simple to create default WebAPI/OData controllers.

The main Get() method of my OData controller is setup like this:

[Queryable]
public virtual IHttpActionResult Get(ODataQueryOptions<ContentType> odataQueryOptions)
 {
 var userId = 102; // mock

try
 {
var results = odataQueryOptions.ApplyTo(_uow.Repository<ContentType>()
    .Query()
    .Get()
    .Where(u => u.UserId == userId)
    .OrderBy(o => o.Description)).Cast<ContentType>()
    .Select(x => new ContentTypeDTO()
    {
       //projection goes here
       ContentTypeId = x.ContentTypeId,
       Description = x.Description,
    });

if (odataQueryOptions.SelectExpand != null)
    {
       Request.SetSelectExpandClause(odataQueryOptions.SelectExpand.SelectExpandClause);
    }

return Ok(results, results.GetType());
 }
 catch (Exception ex)
 {
    throw ex;
 }
 }

Because I want things like paging options to be done at the server level, we apply those to our repository calls through .ApplyTo(…).

Keeping in good form, we also used projection to return a DTO, rather than properties of the entire entity.

Since the endpoint is sitting within my application, I ensured it is stateful, that is, user context details such as userId are available (only mocked in this case).

AngularJS

In my Angular environment, I like to stick to SOLID principles, and end up with several files, but everything is organized.

angulardir

The main page simply contains one tag and the setup is all done through JavaScript in the Angular controller:

<span style="font-family: Georgia, 'Times New Roman', 'Bitstream Charter', Times, serif; font-size: 14px; line-height: 1.5em;">
<body ng=app="app">
</span>   <div ng-controller="contentTypesController">
      <div id="grid"></div>
   </div>
</body>

The main magic really occurs in the configuration of the KendoUI datasource – the grid configuration is quite basic (see the end of the contentType.js controller).

app.js:

</pre>
var app = angular.module('app', ['ngResource', 'ngRoute', 'ngSanitize']);

// ng Routes
//app.config(['$routeProvider', function ($routeProvider) {

// $routeProvider.when('/', {
// controller: 'customersController',
// templateUrl: '/app/views/customers.html'
// })
// .otherwise({ redirectTo: '/' });

//}]);

try { angular.module("ngResource") } catch (err) {
 /* failed to require */
 console.log('not loaded');
}

Injected is ngResource, ngRoute (for use later and routes currently commented-out), and ngSanitize.    I’ve got a bit of debugging code near the bottom to show if there is a problem with loading ngResource.

The ContentTypesController contains all of the UI setup code.

contentTypeController.js

<code>app.controller('contentTypeController', ['$scope', '$log', 'abstractFactory3',
    // the abstract data factory accepts controller type parameters for RESTful CRUD

    function ($scope, $log, abstractFactory3) {

        var dataFactory = new abstractFactory3("/odata/ContentType");

        var crudServiceBaseUrl = "/odata/ContentType";

        var dataSource = new kendo.data.DataSource({
            type: "odata",
            transport: {
                read:

                    function (options) {
                        var odataParams = kendo.data.transports["odata"].parameterMap(options.data, "read");

                        dataFactory.getList(odataParams)
                            .success(function (result) {
                                options.success(result);
                            })
                            .error (function (error) {
                                console.log("data error");
                            });

                    },
                update:
                    function (options) {
                        var data = options.data;
                        dataFactory.update(data.ContentTypeId, data)
                            .success(function (result) {
                                options.success(result);
                            })
                            .error(function (error) {
                                console.log("data error");
                            });
                },
                create:
                    function (options) {
                        var data = options.data;
                        data.ContentTypeId = "0";           // required for valid field data
                        dataFactory.insert(data)
                            .success(function (result) {
                                options.success(result);
                            })
                            .error(function (error) {
                                console.log("data error");
                            });
                },
                destroy:
                    function (options) {
                        var data = options.data;
                        dataFactory.remove(data.ContentTypeId)
                            .success(function (result) {
                                options.success(result);
                            })
                            .error(function (error) {
                                console.log("data error");
                            });

                },
                parameterMap: function (options, type) {
                    // this is optional - if we need to remove any parameters (due to partial OData support in WebAPI
                    if (operation !== "read" && options.models) {
                        return JSON.stringify({ models: options });
                    }
                },

            },
            batch: false,
            pageSize: 10,
            serverPaging: true,
            change: function (e) {
                console.log("change: " + e.action);
                // do something with e
            },
            schema: {
                data: function (data) {
                    //console.log(data)
                    return data.value;
                },
                total: function (data) {
                    console.log("count: " + data["odata.count"]);
                    return data["odata.count"];
                },
                model: {
                    id: "ContentTypeId",
                    fields: {
                        ContentTypeId: { editable: false, nullable: true },
                        //UserId: {editable: false, nullable: false },
                        Description: { type: "string", validation: { required: true } },
                        //msrepl_tran_version: { type: "string", validation: { required: true } }
                    }
                }
            },
            error: function (e) {
                //var response = JSON.parse(e.responseText);
                var response = e.status;
                console.log(response);
            }

        });

        $("#grid").kendoGrid({
            dataSource: dataSource,
            pageable: true,
            height: 400,
            toolbar: ["create"],
            columns: [
                        { field: "ContentTypeId", editable: false, width: 90, title: "ID" },
                        { field: "Description", title: "Content Type" },
                        { command: ["edit", "destroy"] }
            ],
            editable: "inline"
        });

    }]);</code>

The abstract factory is injected into the controller.

abstractFactory3.js:

<code>app.factory('abstractFactory3', function ($http) {

    function abstractFactory3(odataUrlBase) {
        this.odataUrlBase = odataUrlBase;
    }

    abstractFactory3.prototype = {
        getList: function (odataOptions) {
            //var result = $http({
            //    url: this.odataUrlBase,
            //    method: 'GET',
            //    params: odataParams
            //});

            return $http.get(this.odataUrlBase, {
                params: odataOptions
            });
        },
        get: function (id, odataOptions) {
            return $http.get(this.odataUrlBase + '/' + id, {
                params: odataOptions
            });
        },
        insert: function (data) {
            return $http.post(this.odataUrlBase, data);
        },
        update: function (id, data) {
            return $http.put(this.odataUrlBase + '(' + id + ')', data);
        },
        remove: function (id) {
            return $http.delete(this.odataUrlBase + '(' + id + ')');
        }
    };

    return abstractFactory3;
});</code>

Since we are not using KendoUI’s datasource default transport functions which take care the data type serialization/formatting, etc. for OData compatibility, we have to do this manually, and send this off to the abstract angular factory that uses the $http service.  I could have also used ngResource for RESTful service interaction support.

It is important to know what the data looks like coming back from the OData service.  In this case, we have to tell the KendoUI datasource to look in data.value, which contains a collection of objects.  This is done in the schema/data section.

If your datasource is populating a grid you and you want to introduce efficient paging, you also need to inform the datasource where to find the total number of items – schema/total.  This value is returned when the query string parameter $inlinecount=allpages exists.

With abstractFactory3 injected into our controller, I looked at the initialization process to be similar to object instantiation with an overloaded constructor that takes in one parameter – the OData route:

<code>var dataFactory = new abstractFactory3("/odata/ContentType");</code>

With that established we can now call individual “methods” on the abstract factory to perform our CRUD operations.

Since we also need to pass valid OData parameters, we need to extract this from KendoUI with the following code:

<code>var odataParams = kendo.data.transports["odata"].parameterMap(options.data, "read");</code>

Also take note of the parameterMap area, where data is stringified.

Conclusion

In this article, we have walked through the setup and configuration of a KendoUI grid, and datasource that is hydrated by a RESTful OData endpoint through AngularJS and  have briefly discussed the complexity in using AngularJS over the default KendoUI data transports to manage data flow to/from a KendoUI grid control.

I suggest using Fiddler to view the data coming to/from the client and setting breakpoints at key areas, looking at the stack to see what is contained in kendo.data.transports, and when parameterMap is called.

In following SOLID principles, we want to isolate the work that each object (file, class, etc.) does.  We have setup our page controller that manages all the Angular/JS lifting, and the abstract factory to manage the data flow.  This factory can be re-used by any controller by simply passing in the appropriate OData endpoint path upon initialization within a controller, rather than duplicating this code in all of our Angular controllers.

I used SQL Server for my data store, and frequently spin up the SQL Server Profiler to see the queries being executed on the database server.

A word of caution in setting up OData endpoints.  You can specify client-side or server-side data filtering.  Remember – you don’t usually want to return all your data from the server then filter on the client-side.

As there may be more efficient ways of doing things, I welcome any feedback and suggestions.

Advertisements

A list of past articles, tips and tricks…

Below are a list of older articles, projects and tips/tricks on CodeProject and CodePlex ranging from ASP.Net to SSIS and some DAL work:

RSS/XML Data-Binding to Data Control with Row Limitations

  • This code will use an RSS feed as a data source for a DataGrid control. Instead of displaying all rows in the feed, row-limiting code is also used.

Keyword Matching RSS Reader Control

  • Using RSS feeds on web pages is a good way of creating dynamic content, which search engines love. The more your page changes, the more it gets crawled, and can affect your page rank.

GeoLocation by Radius Using Google Maps and .NET

  • A few years back, I helped to integrate a company’s database of national sales stores/locations with a third-party website. We’ve all seen these “find the nearest store” locator-type pages.

Performing Data Merge and Audit with SSIS

  • This article details with how to perform audits on merging data – checking for duplicates, and adding other validation rules, using SSIS. This method is much more efficient than writing validation code in VB or C#.

C# ViewState Management/Storage – Four Locations!

  • From an SEO standpoint, page size matters, as well as the location of text on your page. When creating a site with VS .Net, we end up with a ViewState variable, which in some cases, can take up a whole lot of space near the top of the page. This could, in fact, dilute the contents’ importance on a given web page. In terms of page size, some search engines only cache the first 120kb of data.

Generic Queryable Data Repository

SubSonic ORM v2.3 with SQL Server 2008 TVP Support

  • This project contains the modified base code of the SubSonic v2.3 ORM, originally written by Rob Conery, that now supports SQL Server 2008 TVP’s (table valued parameters). This modification allows you to generate your full DAL, access and manipulate data in an object-oriented fashion, and most-importantly, now gives you the ability to pass large chunks of data to your stored procedures, in the form of DataTables, rather than bulk inserts.

OData Using C#, MVC, WebAPI, Entity Framework, Dependency Injection (DI)/Inversion of Control (IoC) and Kendo UI

OData – Displaying Hierarchical Data with Kendo UI, MVC and a Queryable Entity Framework Repository

It is recommended that you are proficient with using .Net, dependency injection, MVC, Fiddler, and SQL Server Profiler – ie. good debugging skills, and can use the Kendo UI Grid extension for data display purposes. Otherwise, you can access the OData URL paths for viewing the raw data.

Introduction

As a part of this solution I created a generic queryable repository that I accessed Entity Framework Database First Entities through a repository and unit of work pattern: DOWNLOAD.

To modify your *.tt template to output a compatible context class, see the following article: Modifying Entity Framework for use by a Repository and Unit of Work Pattern

The starting point for this article was a requirement for a new UI POC, for which Kendo UI (http://www.kendoui.com/) was chosen. An existing SQL database was used as a data source and a RESTful OData (http://www.odata.org/) API endpoint was created for data access.

Explaining how to setup Kendo UI and what OData is are out of scope for this article.

The purpose of this article is to identify a clean-cut way of using OData and hierarchical data. One of the most commonly asked questions is, “How do I only return the rows needed from my database server, and not 100,000 records then filter?” (or variations there-of). This article will identify how to accomplish this.

We are using OData because it represents an efficient way of retrieving data, as requested directly from the client, without having to write specific associated controller action methods for things like paging and filtering.

For reads, the client sends query parameters that are received by a single Get() method. The query options are then handled on the server, and can be passed directly through to SQL Server (in this case, through Entity Framework).

All CRUD operations are quite simple through OData as well, but are out of scope of this article (other than Reads).

Note:  If you don’t want to use Dependency Injection, and the generic repository included – simply use Entity Framework directly, I include a controller action method at the end of this article that allows you to do this.

Background

The basis for any data-driven application is the DAL, and it’s important to know how the data is accessed and make sure it is done efficiently, as such, I have worked on several projects including rewriting a popular ORM to make use of SQL Server’s TVP’s (http://subsonic23tvp.codeplex.com/).

I am always cautious when controls automatically “do things”, especially when it comes to data access.

I initially setup a Kendo UI Grid with a details grid containing users. Everything looked fine – I expanded a group, and was able to navigate through a list of associated users.

In using the browser console and SQL Server Profiler, I noticed that all users were being returned from SQL Server, then filtering occurring before it reached the client.

In another attempt, I noticed that all records were returned to the client, an the control was paging through the entire data set.

What I wanted to see was that SQL Server was only returning, say 10 records at a time and applying the filtering options.

For this article I am using and making reference to two tables USERS and USERGROUPS. Use this article as a reference for your own data sets.

I typically put a lot of comments in my code to help others understand exactly what’s going on. These comments usually include some alternate examples and helpful URLs for additional background details.

Not everything is described in the text of this article, and most-likely some of your questions will be answered by actually reading through the code itself – highly recommend it.

Solution Setup

The ATMS.DAL project contains the auto-generated classes through Entity Framework Database First.

Here is what the basic solution structure looks like:

odata-solution1

Next, we see the differences between our two sets of control classes – we have two sets:

odata-solution2

We will be using Entity Framework Database First to generate our database entities. However, for testability, we will first use a generic repository pattern and dependency injection / IoC to access our entities through EF.

Explaining how this works and why it is important is out of scope of this article, but I have included the Repository project as a part of the article’s code. The Repository uses generics and as such is not tightly-coupled on either end. Use freely as needed.

The repository project can be added to any .Net solution and referenced in your associated projects.

Simple Injector (http://simpleinjector.codeplex.com/) was used for our DI container, which can be added through a Nuget package to your solution.