Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Server-side paging+filtering+sorting for ng-grid with WebAPI

I'm trying to create a simple working example of using ng-grid with ASP.NET WebAPI. Thus, I started from the server-side paging example in the ng-grid examples page (http://angular-ui.github.io/ng-grid/); anyway, my grid always shows empty columns, even if when debugging I can confirm that data are received properly. Probably I'm just missing something in the grid setup, but all the samples I found look similar to mine. Could anyone help? Here is what I did:

Update #1: the suggested solution seems to work but only for the 1st page. Whenever I move to a new page or do any other operation requiring a refresh, the displayed data stay the same even if the server returned data change as expected. Also, from all the code samples I found it seems the correct way of setting data is just replacing the array member value rather than emptying and filling it again. I tried with apply as suggested in https://groups.google.com/forum/#!searchin/angular/nggrid/angular/vUIfHWt4s_4/oU_C9w8j-uMJ, but I get the same result.


Server side

Just create a new MVC4 app, update NuGet packages and add angular and ng-grid packages. My fake data model is represented by the Item class:

public sealed class Item
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public bool IsFemale { get; set; }
}

I also add a couple of models for dealing with paging, filtering and sorting various sets of data (I find easier to have a common paging base model -PagedFilter-, and a number of derived models):

public class PagedFilter
{
    private int _nPageSize;
    private int _nPageNumber;

    public int PageSize
    {
        get { return _nPageSize; }
        set
        {
            if (value < 1) throw new ArgumentOutOfRangeException("value");
            _nPageSize = value;
        }
    }

    public int PageNumber
    {
        get { return _nPageNumber; }
        set
        {
            if (value < 1) throw new ArgumentOutOfRangeException("value");
            _nPageNumber = value;
        }
    }

    public int TotalItems { get; set; }

    public int TotalPages
    {
        get { return (int)Math.Ceiling((double)(TotalItems / PageSize)); }
    }

    public PagedFilter()
    {
        _nPageSize = 20;
        _nPageNumber = 1;
    }
}

Here is the ItemFilter:

public class ItemFilter : PagedFilter
{
    public List<string> SortFields { get; set; }
    public List<string> SortDirections { get; set; }
    public string Name { get; set; }
    public int? MinAge { get; set; }
    public int? MaxAge { get; set; }
}

Then I add an API controller for getting items:

public class ItemController : ApiController
{
    // fake data
    private readonly List<Item> _items;

    public ItemController()
    {
        Random rnd = new Random();
        _items = new List<Item>();
        char c = 'a';

        for (int i = 0; i < 1000; i++)
        {
            _items.Add(new Item
                            {
                                Id = i,
                                Age = rnd.Next(1, 100),
                                IsFemale = ((i & 1) == 0),
                                Name = String.Format(CultureInfo.InvariantCulture, "{0:00000}-{1}",
                                    i, new string(c, 5))
                            });
            if (++c > 'z') c = 'a';
        }
    }

    public dynamic Get([FromUri] ItemFilter filter)
    {
        var items = _items.AsQueryable();

        // filtering
        if (!String.IsNullOrEmpty(filter.Name))
            items = items.Where(i => i.Name.Contains(filter.Name));

        if (filter.MinAge.HasValue)
            items = items.Where(i => i.Age >= filter.MinAge.Value);

        if (filter.MaxAge.HasValue)
            items = items.Where(i => i.Age <= filter.MaxAge.Value);

        // ...sorting (using Dynamic Linq) omitted for brevity...

        // paging
        int nTotalItems = items.Count();
        items = items.Skip((filter.PageNumber - 1) * filter.PageSize)
                     .Take(filter.PageSize);
        return new
                   {
                       totalItems = nTotalItems,
                       items = items.ToArray()
                   };
    }
}

Client side

On the client side, my angular app is just a single controller modeled on the ng-grid sample: thus I directly add properties to $scope, even if in a real-world scenario I'd rather use a model (probably generated from a TypeScript class). HTML:

<div ng-app="MyApp" ng-controller="MainController">
    <div ng-grid="gridOptions" style="height: 400px">
    </div>
</div>

JS:

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

app.controller('MainController', ['$scope', '$http', function ($scope, $http, $apply) {
    $scope.items = [];

    // filter
    $scope.filterOptions = {
        filterText: "",
        useExternalFilter: true
    };

    // paging
    $scope.totalServerItems = 0;
    $scope.pagingOptions = {
        pageSizes: [25, 50, 100],
        pageSize: 25,
        currentPage: 1
    };

    // sort
    $scope.sortOptions = {
        fields: ["name"],
        directions: ["ASC"]
    };

    // grid
    $scope.gridOptions = {
        data: "items",
        columnDefs: [
            { field: "name", displayName: "Name", pinnable: true },
            { field: "age", displayName: "Age", width: "60" },
            { field: "isFemale", displayName: "F", width: "40" }
        ],
        enablePaging: true,
        enablePinning: true,
        pagingOptions: $scope.pagingOptions,        
        filterOptions: $scope.filterOptions,
        keepLastSelected: true,
        multiSelect: false,
        showColumnMenu: true,
        showFilter: true,
        showGroupPanel: true,
        showFooter: true,
        sortInfo: $scope.sortOptions,
        totalServerItems: "totalServerItems",
        useExternalSorting: true,
        i18n: "en"
    };

    $scope.refresh = function() {
        setTimeout(function () {
            var p = {
                name: $scope.filterOptions.filterText,
                pageNumber: $scope.pagingOptions.currentPage,
                pageSize: $scope.pagingOptions.pageSize,
                sortFields: $scope.sortOptions.fields,
                sortDirections: $scope.sortOptions.directions
            };

            $http({
                url: "/api/item",
                method: "GET",
                params: p
            }).success(function(data, status, headers, config) {
                $scope.totalServerItems = data.totalItems;
                // SUGGESTION #1 -- empty and fill the array
                /* $scope.items.length = 0;
                angular.forEach(data.items, function (item) {
                   $scope.items.push(item);
                }); 
                */
                // https://groups.google.com/forum/#!searchin/angular/nggrid/angular/vUIfHWt4s_4/oU_C9w8j-uMJ
                $scope.$apply(function () { $scope.items = data.items; });
                if (!$scope.$$phase) {
                    $scope.$apply();
                }
            }).error(function(data, status, headers, config) {
                alert(JSON.stringify(data));
            });
        }, 100);
    };

    // watches
    $scope.$watch('pagingOptions', function (newVal, oldVal) {
        if (newVal !== oldVal && newVal.currentPage !== oldVal.currentPage) {
            $scope.refresh();
        }
    }, true);

    $scope.$watch('filterOptions', function (newVal, oldVal) {
        if (newVal !== oldVal) {
            $scope.refresh();
        }
    }, true);

    $scope.$watch('sortOptions', function (newVal, oldVal) {
        if (newVal !== oldVal) {
            $scope.refresh();
        }
    }, true);

    $scope.refresh();
}]);

In my code, the success callback is called, and I can browse all the returned items in data.items. Yet, nothing is displayed in the grid. No error appears in the console.

like image 537
Naftis Avatar asked Jul 22 '13 11:07

Naftis


3 Answers

After experimenting a bit, I think I found the correct code. This post about $apply helped me a bit: http://jimhoskins.com/2012/12/17/angularjs-and-apply.html. In fact, if I understand well the call to apply should not be needed at all, given that my data are coming from $http which already provides this. So, I ended with just setting the scope items variable in the success callback. Here is the full JS again, hope this can help some newcomer like me. Now I'm going to expand the test with TypeScript models, services and all the real-world stuff: I fear I'll have to make some new post... :)

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

app.controller('MainController', ['$scope', '$http', function ($scope, $http, $apply) {
    $scope.items = [];

    // filter
    $scope.filterOptions = {
        filterText: "",
        useExternalFilter: true
    };

    // paging
    $scope.totalServerItems = 0;
    $scope.pagingOptions = {
        pageSizes: [25, 50, 100],
        pageSize: 25,
        currentPage: 1
    };

    // sort
    $scope.sortOptions = {
        fields: ["name"],
        directions: ["ASC"]
    };

    // grid
    $scope.gridOptions = {
        data: "items",
        columnDefs: [
            { field: "id", displayName: "ID", width: "60" },
            { field: "name", displayName: "Name", pinnable: true },
            { field: "age", displayName: "Age", width: "60" },
            { field: "isFemale", displayName: "F", width: "40" }
        ],
        enablePaging: true,
        enablePinning: true,
        pagingOptions: $scope.pagingOptions,        
        filterOptions: $scope.filterOptions,
        keepLastSelected: true,
        multiSelect: false,
        showColumnMenu: true,
        showFilter: true,
        showGroupPanel: true,
        showFooter: true,
        sortInfo: $scope.sortOptions,
        totalServerItems: "totalServerItems",
        useExternalSorting: true,
        i18n: "en"
    };

    $scope.refresh = function() {
        setTimeout(function () {
            var sb = [];
            for (var i = 0; i < $scope.sortOptions.fields.length; i++) {
                sb.push($scope.sortOptions.directions[i] === "DESC" ? "-" : "+");
                sb.push($scope.sortOptions.fields[i]);
            }

            var p = {
                name: $scope.filterOptions.filterText,
                pageNumber: $scope.pagingOptions.currentPage,
                pageSize: $scope.pagingOptions.pageSize,
                sortInfo: sb.join("")
            };

            $http({
                url: "/api/item",
                method: "GET",
                params: p
            }).success(function(data, status, headers, config) {
                $scope.totalServerItems = data.totalItems;
                $scope.items = data.items;
            }).error(function(data, status, headers, config) {
                alert(JSON.stringify(data));
            });
        }, 100);
    };

    // watches
    $scope.$watch('pagingOptions', function (newVal, oldVal) {
        if (newVal !== oldVal) {
            $scope.refresh();
        }
    }, true);

    $scope.$watch('filterOptions', function (newVal, oldVal) {
        if (newVal !== oldVal) {
            $scope.refresh();
        }
    }, true);

    $scope.$watch('sortOptions', function (newVal, oldVal) {
        if (newVal !== oldVal) {
            $scope.refresh();
        }
    }, true);

    $scope.refresh();
}]);

(As a sidenote, you can see from the code that I'm passing a single string for sort data, rather than two arrays for fields and directions. In fact, I could not find the right way of receiving arrays as members of my input model in the C# controller; so I'm just passing a single string where each field name is prefixed by + or - according to the ascending/descending direction).

like image 153
Naftis Avatar answered Nov 12 '22 18:11

Naftis


You are setting datasource on ng-grid to items but then you are never updating the items array on server success callback.

On succcess callback do something like this

$scope.totalServerItems = data.totalItems;
angular.forEach(data.items, function(item) {
   $scope.items.push(item);
});
like image 30
Chandermani Avatar answered Nov 12 '22 18:11

Chandermani


It might help too

The HTML code-sample

<html ng-app="myApp">  
    <head lang="en">
        <meta charset="utf-8">
        <title>Getting Started With ngGrid code-sample</title>  
        <script type="text/javascript" src="angular.js"></script>
        <script type="text/javascript" src="ng-grid-1.3.2.js"></script>
    </head>
    <body ng-controller="MyCtrl">
        <div class="gridStyle" ng-grid="gridOptions"></div>
    </body>
</html>

The AngulaJs code-sample

var app = angular.module('myApp', ['ngGrid']);
app.controller('MyCtrl', function($scope, $http) {
    $scope.filterOptions = {
        filterText: "",
        useExternalFilter: true
    }; 
    $scope.totalServerItems = 0;
    $scope.pagingOptions = {
        pageSizes: [250, 500, 1000],
        pageSize: 250,
        currentPage: 1
    };  
    $scope.setPagingData = function(data, page, pageSize){  
        var pagedData = data.slice((page - 1) * pageSize, page * pageSize);
        $scope.myData = pagedData;
        $scope.totalServerItems = data.length;
        if (!$scope.$$phase) {
            $scope.$apply();
        }
    };
    $scope.getPagedDataAsync = function (pageSize, page, searchText) {
        setTimeout(function () {
            var data;
            if (searchText) {
                var ft = searchText.toLowerCase();
                $http.get('jsonFiles/largeLoad.json').success(function (largeLoad) {        
                    data = largeLoad.filter(function(item) {
                        return JSON.stringify(item).toLowerCase().indexOf(ft) != -1;
                    });
                    $scope.setPagingData(data,page,pageSize);
                });            
            } else {
                $http.get('jsonFiles/largeLoad.json').success(function (largeLoad) {
                    $scope.setPagingData(largeLoad,page,pageSize);
                });
            }
        }, 100);
    };

    $scope.getPagedDataAsync($scope.pagingOptions.pageSize, $scope.pagingOptions.currentPage);

    $scope.$watch('pagingOptions', function (newVal, oldVal) {
        if (newVal !== oldVal && newVal.currentPage !== oldVal.currentPage) {
          $scope.getPagedDataAsync($scope.pagingOptions.pageSize, $scope.pagingOptions.currentPage, $scope.filterOptions.filterText);
        }
    }, true);
    $scope.$watch('filterOptions', function (newVal, oldVal) {
        if (newVal !== oldVal) {
          $scope.getPagedDataAsync($scope.pagingOptions.pageSize, $scope.pagingOptions.currentPage, $scope.filterOptions.filterText);
        }
    }, true);

    $scope.gridOptions = {
        data: 'myData',
        enablePaging: true,
        showFooter: true,
        totalServerItems: 'totalServerItems',
        pagingOptions: $scope.pagingOptions,
        filterOptions: $scope.filterOptions
    };
});
like image 1
Anil Singh Avatar answered Nov 12 '22 16:11

Anil Singh