My Technical Notes

Tuesday, 18 August 2015

ng-grid with ASP.NET MVC

*** This example assumes you have Northwind database installed. ***

In this example, we display a table with paging, sorting and searching using ng-grid.

Setting up Northwind Entity Context

After installing the Northwind database, add an Entity Framework Context for it. We are only interested in the `Customers` table.

Ignoring Customer collection properties

Because we will be serializing the `Customer` object to JSON, we need to ensure that the `Orders` and `CustomerDemographics` collections are ignored. Add the following partial class for `Customer` to tell the "Json-izer" to ignore (not attempt to serialize) those properties:


[MetadataType(typeof(CustomerMetadata))]
public partial class Customer
{

}

public class CustomerMetadata
{
    [Newtonsoft.Json.JsonIgnore]
    public ICollection<Order> Orders { get; set; }

    [Newtonsoft.Json.JsonIgnore]
    public ICollection<CustomerDemographic> CustomerDemographics { get; set; }
}

Installing Packages

First install the following NuGet packages:

  • AngularJS Core
  • System.Linq.Dynamic

Thereafter add ng-grid.js and ng-grid.css to the project.

Changing ng-grid.js

This step may be unneccessary in the future, but I found it necessary for my code to work. Within `ng-grid.js`, replace the lines:


"            <select style=\"float: left;height: 27px; width: 100px\" ng-model=\"pagingOptions.pageSize\" >\r" +
"\n" +
"                <option ng-repeat=\"size in pagingOptions.pageSizes\" value=\"{{size}}\">{{size}}</option>\r" +
"\n" +
"            </select>\r" +
with

"            <select style=\"float: left;height: 27px; width: 100px\" ng-model=\"pagingOptions.pageSize\" ng-options=\"x as x for x in pagingOptions.pageSizes\" />\r" +

Adding Script and Style References

This step is to add the Angular and Angular Grid references to the layout page. Do the following on `BundleConfig.cs` in `App_Start`:

1. For `~/Content/css`, add `~/Content/ng-grid.css`.

2. Add another bundle `angularjs`:


bundles.Add(new ScriptBundle("~/bundles/angularjs").Include(
    "~/Scripts/angular.js",
    "~/Scripts/ng-grid.js"
    ));

Add the above `~/bundles/angularjs` bundle to the layout page, under the jquery reference:


@Scripts.Render("~/bundles/angularjs")

CustomerService controller

We introduce a class `pageParams`. Our angular code will pass parameters to the action in this form:


public class pageParams
{
    public int page { get; set; }
    public int pageSize { get; set; }
    public string filterText { get; set; }
    public string sortColumn { get; set; }
    public string sortDirection { get; set; }
}

`page` is the page number, initially will be `1`. `pageSize` is the initial size of the page, initially `10`. `filterText` will be the name filter value.

We introduce a class `pageResult`. This is the page data that will be returned from our action method:


public class pageResult<T>
{
    public IList<T> data { get; set; }
    public int page { get; set; }
    public int pageSize { get; set; }
    public int total { get; set; }
    public int totalPages { get; set; }
}

`data` will be the `10` or so records that will be returned as part of the page. `page` is the page number. `pageSize` is the size of the page. `total` is the total number of records. `totalPages` is the total number of pages (so roughly, the `total` divided by the `pageSize`).

Add the following `CustomerServiceController` class. It has a method `GetPage` which will be called by our Javascript when the grid needs to fetch the page data. Note the use of `pageParams` class defined above.

Remember to import the necessary namespaces such as your context's namespace and `System.Linq.Dynamic` and `Newtonsoft.Json`.


public class CustomerServiceController : Controller
{
    NorthwindEntities db = new NorthwindEntities();

    public ActionResult GetPage(pageParams p)
    {
        if (p.page == 0)
            p.page = 1;

        var customers = new Func<IQueryable<Customer>>(() =>
        {
            if (string.IsNullOrWhiteSpace(p.filterText))
                return db.Customers;
            else
                return db.Customers.Where(x => x.ContactName.Contains(p.filterText));
        })();

        if (string.IsNullOrWhiteSpace(p.sortColumn) || string.IsNullOrWhiteSpace(p.sortDirection)) {
            customers = customers.OrderBy(x => x.CustomerID);
        } 
        else 
        {
            customers = customers.OrderBy(p.sortColumn + " " + p.sortDirection);
        }

        var count = customers.Count();
        var pagedCustomers = customers.Skip((p.page - 1) * p.pageSize).Take(p.pageSize).ToList();

        var pageResult = new pageResult<Customer>
        {
            data = pagedCustomers,
            page = p.page,
            pageSize = p.pageSize,
            total = count,
            totalPages = (int)Math.Ceiling((decimal)count / p.pageSize)
        };

        return Content(JsonConvert.SerializeObject(pageResult), "application/json");
    }
}

HTML and CSS

In the HTML, we have a text input bound to `filterOptions.filterText`. This will be used later.


<div ng-app="app" ng-controller="ctrl">
    <input type="text" ng-model="gridVals.filterOptions.filterText" placeholder="Customer Name" />
    <br />
    <div ng-grid="gridOptions" class="gridStyle">
    </div>
</div>

The CSS is required to set the width and height of the table and to give it a border:


.gridStyle {
    border: 1px solid rgb(212,212,212);
    width: 600px; 
    height: 300px;
}

Angular Logic (Javascript)

I will break this down for easy consumption:

We instantiate our Angular module, note that we are passing `ngGrid` (Angular Grid module) as a dependency:


var app = angular.module('app', ['ngGrid']);

We define a factory `CustomerService` that has a single function that calls our `CustomerServiceController`/`GetPage` action method:


app.factory('CustomerService', function ($http) {
    return {
        getPage: function (pageParams) {
            return $http.get('/CustomerService/GetPage', pageParams);
        }
    };
});

Lastly we have the definition of our Angular controller `ctrl` that is explained in the comments:


app.controller('ctrl', function ($scope, $log, CustomerService) {

    $scope.gridVals = {
        data: [], // data to be displayed in the grid
        total: 0, // total number of rows in the table matching the filters
        filterOptions: {
            filterText: "", // initial value of the filterText
            useExternalFilter: true // // I am using my own server-side filtering mechanism, not ng-grid's client side one
        },
        pagingOptions: {
            pageSizes: [5, 10, 20], // drop down with page sizes
            pageSize: 5, // default page size
            currentPage: 1 // default page number on load,
        },
        // sortInfo of the grid. Initially sort the grid by CustomerID in ascending order
        sortInfo: {
            fields: ['CustomerID'],
            directions: ['asc']
        }
    };

    // bindGrid function retrieves paging, sorting and searching data from
    // $scope variables and passes it to CustomerService.getPage function
    // and then sets our grid data.
    function bindGrid() {
        binding = true;

        var gridVals = $scope.gridVals;

        var options = {
            page: gridVals.pagingOptions.currentPage,
            pageSize: gridVals.pagingOptions.pageSize,
            filterText: gridVals.filterOptions.filterText,
            sortColumn: gridVals.sortInfo.fields[0],
            sortDirection: gridVals.sortInfo.directions[0]
        };

        CustomerService.getPage({ params: options }).then(function (data) {
            var pageResult = data.data;
            gridVals.pagingOptions.currentPage = pageResult.page;
            gridVals.pagingOptions.pageSize = pageResult.pageSize;
            gridVals.total = pageResult.total;
            gridVals.data = pageResult.data;
        }).finally(function () {
            binding = false;
        });
    }

    $scope.$watch('gridVals.pagingOptions', function (newVal, oldVal) {
        if (!binding) {
            if (newVal != oldVal && (newVal.pageSize != oldVal.pageSize)) {
                binding = true;
                $scope.gridVals.pagingOptions.currentPage = 1;
            }

            bindGrid();
        }
    }, true);

    $scope.$watch('gridVals.filterOptions.filterText', function (newVal, oldVal) {
        if (!binding)
            bindGrid();
    }, true);

    $scope.$watch('gridVals.sortInfo', function (newVal, oldVal) {
        // reset the selected page to 1 if/when they click on the sort column
        if (!binding) {
            if (newVal != oldVal && (newVal.fields[0] != oldVal.fields[0] || newVal.directions[0] != oldVal.directions[0])) {
                binding = true;
                $scope.gridVals.pagingOptions.currentPage = 1;
            }

            bindGrid();
        }
    }, true);

    // here we set the gridOptions property.  Note that we are using all the
    // properties defined above
    $scope.gridOptions = {
        data: 'gridVals.data',
        totalServerItems: 'gridVals.total',
        pagingOptions: $scope.gridVals.pagingOptions,
        filterOptions: $scope.gridVals.filterOptions,
        useExternalSorting: true,
        sortInfo: $scope.gridVals.sortInfo,
        enablePaging: true,
        showFooter: true,
        columnDefs: [
            { field: 'CustomerID', displayName: 'Cust ID' },
            { field: 'CompanyName', displayName: 'Company' },
            { field: 'ContactName', displayName: 'Name' }
        ]
    };

    // call bindGrid
    bindGrid();
});

References

Downloads

No comments: