Knockout.js: Pagination with ASP.NET Web API (or WCF service)

Introduction
In this article, I am trying to implement HTML table as a grid with pagination using knockout.js. This is the easiest way to get page wise data from server using ASP.NET Web API (or WCF service) and display them as a grid.

Before we jump in to this article, let’s have a look at the standard definition of Knockout.js and ASP.NET Web API:

Knockout.js is a JavaScript library that helps you to create rich, responsive displays and editor user interfaces with a clean underlying data model. Any time you have sections of UI that update dynamically (e.g., changing depending on user’s actions or when an external data source changes), KO can help you implement it more simply and maintainable.

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.

Define a PagedObservable
To get page wise data , create a JavaScript file pagedobservable.js and write the following codes:

(function (window, ko) {
window.Utils = window.Utils || {};
window.Utils.pagedObservable = function (options) {
options = options || {};
var _allData = ko.observableArray(), //the data collection to dispaly in grid

_columns = ko.observableArray(options.columns || []), //the columns of grid

_pageSize = ko.observable(options.pageSize || 10), //the size of the pages to display
_pageSizes = ko.observable(options.pageSizes || []), //the size of the pages to display
_pageIndex = ko.observable(1), //the index of the current page
_pageCount = ko.observable(0), //the number of pages

_totalRecords = ko.observable(0), //the number of records

_sortName = ko.observable(options.sortName || ”), //the sort column name

_sortOrder = ko.observable(options.sortOrder || ‘asc’), //the sort order
//move to the next page
_nextPage = function () {
if (_pageIndex() < _pageCount()) {
_pageIndex(parseInt(_pageIndex()) + 1);
}
},
//move to the previous page
_previousPage = function () {
if (_pageIndex() > 1) {
_pageIndex(_pageIndex() – 1);
}
},
//move to first page
_firstPage = function () {
if (_pageIndex() > 1) {
_pageIndex(1);
}
},
//move to last page
_lastPage = function () {
if (_pageIndex() < _pageCount()) {
_pageIndex(_pageCount());
}
},
//sort a column
_sort = function (column) {
_sortName(column.index);
_sortOrder(_sortOrder() === ‘asc’ ? ‘desc’ : ‘asc’);
_pageIndex(1);
_loadFromServer();
},
//the message for record info
_recordMessage = ko.computed(function () {
if (_allData().length > 0) {
return ‘Records ‘ + ((_pageIndex() – 1) * _pageSize() + 1) + ‘ – ‘ + (_pageIndex() < _pageCount() ? _pageIndex() * _pageSize() : _totalRecords()) + ‘ of ‘ + _totalRecords();
}
else {
return ‘No records’;
}
}),
//the message for page info
_pageMessage = ko.computed(function () {
if (_allData().length > 0) {
return ‘Page ‘ + _pageIndex() + ‘ of ‘ + _pageCount();
}
else {
return ‘No pages’;
}
}),
//service url
_serviceURL = ko.computed(function () {
return options.serviceURL + (options.serviceURL.indexOf(‘?’) != -1 ? “&” : “?”) + “sidx=” + _sortName() + “&sord=” + _sortOrder() + “&page=” + _pageIndex() + “&rows=” + _pageSize();
}, this),
//load data from server
_loadFromServer = function () {
$.getJSON(_serviceURL(), function (data) {
if (data != null) {
_totalRecords(data.records);
_pageCount(data.total);
_allData(data.rows || []);
}
else {
_totalRecords(0);
_pageCount(0);
_allData([]);
}
});
};
_pageIndex.subscribe(function () {
_pageIndex() < 1 ? _pageIndex(1) : _loadFromServer();
});
_pageSize.subscribe(function () {
_pageIndex() != 1 ? _pageIndex(1) : _loadFromServer();
});
_loadFromServer();
//public members
this.columns = _columns;
this.rows = _allData;
this.totalRecords = _totalRecords;
this.pageSize = _pageSize;
this.pageSizes = _pageSizes;
this.pageIndex = _pageIndex;
this.pageCount = _pageCount;
this.nextPage = _nextPage;
this.previousPage = _previousPage;
this.firstPage = _firstPage,
this.lastPage = _lastPage,
this.sortOrder = _sortOrder;
this.sortName = _sortName;
this.sort = _sort;
this.recordMessage = _recordMessage;
this.pageMessage = _pageMessage;
this.load = _loadFromServer;
};
}(window, ko));

The new instance exposes a number of properties to support paging:

  • columns– An observableArray instance which helps to display column information.
  • rows– An observableArray instance exposing the page wise data.
  • totalRecords– An observable instance exposing the total no of records.
  • pageSize – An observable instance containing the number of items per page.
  • pageSizes – An observableArray instance exposing the page sizes user want to display on dropdown list in grid.
  • pageIndex – An observable instance containing the current page index.
  • pageCount – A computed observable that returns the number of pages.
  • rows – An observableArray instance that contains the current page data.
  • previousPage – A function that moves to the previous page.
  • nextPage – A function that moves to the next page.
  • firstPage – A function that moves to the first page.
  • lastPage – A function that moves to the last page.
  • recordMessage – It shows current record range information(ex: Records 1 – 10 of 14).
  • pageMessage – It shows current page index information(ex: Page 1 of 2).
  • load – It loads page wise data from server.

Define a ViewModel
To make use of above PagedObservable, create an instance of Utils.pagedObservable as below:

var API_URL = “../api/contacts/”;

var ViewModel = function () {
this.pagedList = new Utils.pagedObservable({
pageSize: 10,
pageSizes: [5, 10, 15],
sortName: ‘Id’,
sortOrder: ‘asc’,
columns: [{ name: ‘ID’, index: ‘Id’, sortable: true, width: ‘10%’ },
{ name: ‘First Name’, index: ‘FirstName’, sortable: true, width: ‘25%’ },
{ name: ‘Last Name’, index: ‘LastName’, sortable: true, width: ‘25%’ },
{ name: ‘Phone’, index: ‘Phone’, sortable: true, width: ‘25%’ },
{ name: ”, index: ”, sortable: false, width: ‘15%’ }
],
serviceURL: API_URL
});

Let’s explain the properties used in Utils.pagedObservable so that we can costomize according to our requirement:

  • pageSize: It is used to set default page size.
  • paeSizes: It is used to set page sizes which is displayed as dropdownlist in grid.
  • sortName: It is used to set default sort column name.
  • sortOrder: It is used to set sort order.
  • columns: It is used to set the columns name(which will be displayed in the header of grid), index(which is used to sort column), sortable(which is used to enable/disable sort functionality for a particular column) and width(which is used to set width of each column).
  • serviceURL: It is used to set the API controller url which is used to retrieve server data.

Define a View
To bind the exposed properties to view, write some simple HTML:

<table class=”table table-bordered table-condensed table-hover” data-bind=”with: pagedList”>
<thead class=”btn-primary”>
<tr>
<!– ko foreach: columns –>
<th data-bind=”click: sortable ? $parent.sort : ”, style: { width: width, cursor: sortable ? ‘pointer’ : ” }”>
<span data-bind=”text: name”></span>

<span class=”icon-white icon-circle-arrow-down” data-bind=”visible: $parent.sortOrder() === ‘desc’ && sortable && $parent.sortName() === index”></span>
<span class=”icon-white icon-circle-arrow-up” data-bind=”visible: $parent.sortOrder() === ‘asc’ && sortable && $parent.sortName() === index”></span>
</th>
<!– /ko –>
</tr>
</thead>
<tbody data-bind=”foreach: rows”>
<tr>
<td>
<span data-bind=”text: Id”></span>
</td>
<td>
<span data-bind=”text: FirstName”></span>
</td>
<td>
<span data-bind=”text: LastName”></span>
</td>
<td>
<span data-bind=”text: Phone”></span>
</td>
<td>
<a href=”#” data-bind=”click: $root.editContact”>Edit</a>

<a href=”#” data-bind=”click: $root.removeContact”>Delete</a>
</td>
</tr>
</tbody>
<tfoot class=”btn-primary”>
<tr>
<td colspan=”5″>
<div class=”row”>
<div class=”span9″></div>
<div class=”span9″ align=”center”>
<span class=”icon-white icon-fast-backward” data-bind=”click: firstPage, style: { cursor: pageIndex() > 1 ? ‘pointer’ : ” }”></span>
<span class=”icon-white icon-backward” data-bind=”click: previousPage, style: { cursor: pageIndex() > 1 ? ‘pointer’ : ” }”></span>
<input type=”text” style=”width: 30px;” class=”search-query” data-bind=”value: pageIndex” />
<span data-bind=”text: pageMessage”></span>
<select style=”width: 60px;” class=”search-query” data-bind=”options: pageSizes, value: pageSize”></select>
<span class=”icon-white icon-forward” data-bind=”click: nextPage, style: { cursor: pageIndex() < pageCount() ? ‘pointer’ : ” }”></span>
<span class=”icon-white icon-fast-forward” data-bind=”click: lastPage, style: { cursor: pageIndex() < pageCount() ? ‘pointer’ : ” }”></span>
</div>
<div class=”span9″ align=”right”>
<span data-bind=”text: recordMessage”></span>
</div>
</div>
</td>
</tr>
</tfoot>
</table>

Let’s explain three section of HTML Table so that it will be easy to customize:

1. Header:

<thead class=”btn-primary”>
<tr>
<!– ko foreach: columns –>
<th data-bind=”click: sortable ? $parent.sort : ”, style: { width: width, cursor: sortable ? ‘pointer’ : ” }”>
<span data-bind=”text: name”></span>

<span class=”icon-white icon-circle-arrow-down” data-bind=”visible: $parent.sortOrder() === ‘desc’ && sortable && $parent.sortName() === index”></span>
<span class=”icon-white icon-circle-arrow-up” data-bind=”visible: $parent.sortOrder() === ‘asc’ && sortable && $parent.sortName() === index”></span>
</th>
<!– /ko –>
</tr>
</thead>

In this section I am using some knockout bindings to

  • enable/disable column sorting
  • show header text

according to the columns specified in paged List of ViewModel.

2. Body:

<tbody data-bind=”foreach: rows”>
<tr>
<td>
<span data-bind=”text: Id”></span>
</td>
<td>
<span data-bind=”text: FirstName”></span>
</td>
<td>
<span data-bind=”text: LastName”></span>
</td>
<td>
<span data-bind=”text: Phone”></span>
</td>
<td>
<a href=”#” data-bind=”click: $root.editContact”>Edit</a>

<a href=”#” data-bind=”click: $root.removeContact”>Delete</a>
</td>
</tr>
</tbody>

In this section, rows are populating using knockout “foreach” binding.

3. Footer:

<tfoot class=”btn-primary”>
<tr>
<td colspan=”5″>
<div class=”row”>
<div class=”span9″></div>
<div class=”span9″ align=”center”>
<span class=”icon-white icon-fast-backward” data-bind=”click: firstPage, style: { cursor: pageIndex() > 1 ? ‘pointer’ : ” }”></span>
<span class=”icon-white icon-backward” data-bind=”click: previousPage, style: { cursor: pageIndex() > 1 ? ‘pointer’ : ” }”></span>
<input type=”text” style=”width: 30px;” class=”search-query” data-bind=”value: pageIndex” />
<span data-bind=”text: pageMessage”></span>
<select style=”width: 60px;” class=”search-query” data-bind=”options: pageSizes, value: pageSize”></select>
<span class=”icon-white icon-forward” data-bind=”click: nextPage, style: { cursor: pageIndex() < pageCount() ? ‘pointer’ : ” }”></span>
<span class=”icon-white icon-fast-forward” data-bind=”click: lastPage, style: { cursor: pageIndex() < pageCount() ? ‘pointer’ : ” }”></span>
</div>
<div class=”span9″ align=”right”>
<span data-bind=”text: recordMessage”></span>
</div>
</div>
</td>
</tr>
</tfoot>

In this section I am using knockout bindings to

  • enable/disable forward and backward option
  • show page size dropdownlist
  • show page index textbox
  • show page index message(ex: Page 1 of 2)
  • show record range message(ex: Records 1 – 10 of 14)

Web API Controller

public class ContactsController : ApiController
{
IContactComponent contactComponent = new ContactComponent();

// GET api/
public dynamic GetContactList(string sidx, string sord, int page, int rows)
{
IQueryable contactQuery = contactComponent.GetAll();

IList contactList;

var totalRecords = contactQuery.Count();
var pageIndex = page – 1;

switch (sidx.ToLower())
{
case “id”:
if (sord == “asc”)
{
contactList = contactQuery.OrderBy(o => o.Id).Skip(pageIndex * rows).Take(rows).ToList();
}
else
{
contactList = contactQuery.OrderByDescending(o => o.Id).Skip(pageIndex * rows).Take(rows).ToList();
}
break;
case “firstname”:
if (sord == “asc”)
{
contactList = contactQuery.OrderBy(o => o.FirstName).Skip(pageIndex * rows).Take(rows).ToList();
}
else
{
contactList = contactQuery.OrderByDescending(o => o.FirstName).Skip(pageIndex * rows).Take(rows).ToList();
}
break;
case “lastname”:
if (sord == “asc”)
{
contactList = contactQuery.OrderBy(o => o.LastName).Skip(pageIndex * rows).Take(rows).ToList();
}
else
{
contactList = contactQuery.OrderByDescending(o => o.LastName).Skip(pageIndex * rows).Take(rows).ToList();
}
break;
case “phone”:
if (sord == “asc”)
{
contactList = contactQuery.OrderBy(o => o.Phone).Skip(pageIndex * rows).Take(rows).ToList();
}
else
{
contactList = contactQuery.OrderByDescending(o => o.Phone).Skip(pageIndex * rows).Take(rows).ToList();
}
break;
default:
contactList = contactQuery.OrderBy(o => o.Id).Skip(pageIndex * rows).Take(rows).ToList();
break;
}
var totalPages = (int)Math.Ceiling((float)totalRecords / (float)rows);
return new
{
total = totalPages,
records = totalRecords,
rows = contactList
};
}
}

I have created API controller named ContactsController.The method “GetContactList” is used to retrieve page wise data.It takes minimum 4 parameters(sidx, sord, page, rows).It returns data in a particular format as below:

return new
{
total = totalPages,
records = totalRecords,
rows = contactList
};

For sample project, download from link: KnockoutWithPagination.zip

Summary:
In this blog, I explained how to implement pagination(to get page wise data from server) using knockout.js.

Written By: Rakesh Nayak, ASP.Net Developer, Mindfire Solutions

Advertisements

Posted on August 28, 2013, in ASP.Net, Javascript, Knockout.js and tagged , , , , , , , , , , , , , , . Bookmark the permalink. 5 Comments.

  1. I am not sure where you’re getting your info, but good topic.
    I needs to spend some time learning more or understanding more.
    Thanks for wonderful information I was looking for this info for my mission.

  2. Normally I do not read article on blogs, however I wish to say
    that this write-up very forced me to try and do it! Your writing style has been amazed
    me. Thanks, very great post.

  3. Hi there, I desire to subscribe for this blog to obtain most up-to-date updates, therefore where
    can i do it please help out.

  4. I am extremely impressed together with your writing abilities and
    also with the format in your weblog. Is that this a paid subject
    matter or did you modify it yourself? Either way keep up the
    excellent high quality writing, it’s uncommon to peer
    a nice blog like this one these days..

    • Thanks. I am glad to know that you liked it.

      This is a plugin I developed. Feel free to use and modify it anywhere you want. And also if you have any suggestion, you can post it here.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: