Click here to Skip to main content
11,646,594 members (54,189 online)
Click here to Skip to main content

Data-grid using Knockout JS with paging and sorting

, 1 Oct 2013 CPOL 83.5K 2.5K 43
Rate this:
Please Sign up or sign in to vote.
Creating AJAX and non-AJAX based data-grids with sorting/paging.

Grid as it appears in web-page

Introduction

We will build two types of data-grids (tables) in this article:

  1. One with AJAX based paging and sorting for displaying large number of records
  2. One with JavaScript based (no-postbacks/AJAX calls) paging and sorting for lesser number of records

In both of the above we want the following:

  • Reusable, extensible, and maintainable JavaScript code
  • Use Knockout JS (KO)

The Application

We would display a list of students, their age, school name, and school address in a data-grid (as shown in the image above).

In this article, I want to demonstrate the use of KO and some basic object oriented JavaScript. There are other good articles on CodeProject demonstrating, for example, the use of jqGrid with MVC 4. However here we will develop the entire data-grid (with paging/sorting functions) to have better control over the generated output.

Background

Though I have used ASP.NET MVC 4 as backend to this project, other technologies (PHP etc.) can be used as well, as the client side code to generate data-grid is agnostic of backend technology. I will keep explaining any particular JavaScript constructs that are a bit tricky for users having only basic knowledge of JavaScript.

The back-end code

The C# models are created as below:

public class Student
{
    public string Name { get; set; }
    public School School { get; set; }
    public int Age { get; set; }
}
public class School
{
    public string Name{get;set;}
    public string Address{get;set;}
}

Now, we will create two methods in our controller: one called GetStudentList which will be used by our data-grid that uses AJAX for paging/sorting, and the second called GetAllStudentList that will give back all the data rows in one shot. This will be used by our second kind of data-grid that maintains the data internally and does the paging/sorting in the client side script. Note that we are using a library called Json.NET (fetch it through NuGet in your project) for easily converting our result list to JSON data.

//NOTE: if you are using GET (instead of POST), then form a URL
// (with querystring having all the parameters and their values)
[HttpPost]
public string GetStudentList(int pageIndex = 1, 
       int pageSize = 10, string sortField = "", string sortOrder = "ASC")
{
    var data = GetStudentsSorted(pageIndex, pageSize, sortField, sortOrder == "ASC" ? 
        SortOrder.ASC : SortOrder.DESC);
        //this function does paging sorting in C# code
        //or calls related ORM functions or SQL stored procs
    var jsonData = JsonConvert.SerializeObject(data);
    var total = ((List<Student>)Session["Students"]).Count();
    return JsonConvert.SerializeObject(new { totalRows = total, result = jsonData });
}

[HttpPost]
public string GetAllStudentList()
{
    var data = GetStudentsSorted(); //get the full list of students
    var jsonData = JsonConvert.SerializeObject(data);
    var total = ((List<Student>)Session["Students"]).Count();
    return JsonConvert.SerializeObject(new { totalRows = total, result = jsonData });
}

The client side scripts

JavaScript Namespaces

Let us first create few JavaScript namespaces. I would highly recommend doing so, though won't go into depths of why it's considered a good practice. So let's say I give an acronym to my company "RIT" and an acronym to my project "eW". I will create these namespaces in my main.js:

var RIT = window.RIT || {};
RIT.eW = RIT.eW || {}; //var is not required here as eW namespace is inside RIT namespace
RIT.eW.Services = RIT.eW.Services || {}; //this to hold my AJAX related functions
RIT.eW.Utils = RIT.eW.Utils || {};//this to hold other utilitiy functions

I want to demonstrate my data-grid in a section (in my web-app) called Dashboard, hence I will also create a namespace called Dashboard.

AJAX helper functions

Now, we will create a function to do the AJAX POST requests. We want this function to call a callback function when data is received from the other end (server). We want this 'generic' function to take care of the JSON conversion. As a helper method, I have created a Utils functions that does the JSON conversion.

RIT.eW.Services.AjaxJsonPostCall = function (fullUrl, dataObj, callbackFunction) {
    $.ajax({
        type: 'post',
        url: fullUrl,
        data: JSON.stringify(dataObj),
        dataType: 'json',
        cache: false,
        success: function (data) { callbackFunction(RIT.eW.Utils.GetJson(data)); },
        error: function (XMLHttpRequest, textStatus, errorThrown) {
            console.log("error :" + XMLHttpRequest.responseText);
            alert('There was an error in performing this operation.');
        }
    });
};

RIT.eW.Utils.GetJson = function (data) {
    if (data == '' || data == 'undefined') return null;
    return (JSON && JSON.parse(data) || $.parseJSON(data));
};

I have added other functions to send/receive data in other formats. What we have done is create wrapper functions for all our AJAX JSON stuff.

Basics of object oriented JavaScript

Of course it's not possible to explain much of OOP (object oriented programming) with JavaScript in this article. What I will do is, show you a small snippet for you to understand the very basics of OOP used in my code. The following should be very easy to understand. I have used something called "Revealing Module Pattern" and classical style (feel free to use "class" syntax and/or prototypal styles if you are comfortable with these). Go ahead and Google a bit if you are new:

var myClass = (function() {
    //private variables and functions
    var x = 1;
    var y= 'hello';
    function doSomething(){
    };
    //publically accessible variables and functions
    myClass.prototype.SomeVar = 'blah';
    myClass.prototype.GetData = function(){
    };
    //the constructor
    function myClass(){
    };
    return myClass;
})();
//to use the above "myClass" 
var x = new myClass();
x.GetData();
//static function
myClass.MyStaticFunction = function(){
};

What is Knockout JS?

Simply saying, it allows you to bind your JavaAcript variables to your HTML (DOM) elements. So when the value of the variables are changed in JavaScript, KO automatically updates(refreshes) the associated bound elements in HTML. Isn't this amazing? Such binding can be applied to text values, attributes, css styles and also to events. The major benefit: you won't have to manipulate your HTML in JavaScript (we will demo it here).
All this is used in implementing a software pattern called MVVM (Model-View-ViewModel). In this pattern, your JavaScript variables that hold the data (and are used in binding) are the "Models", your HTML is your "View" and the JavaScript that presents Models in usable form to Views is called "ViewModel". This facilitates JavaScript unit-testing and helps segregate your HTML (UI) from scripting (programming). However, we won't demonstrate MVVM here in this project, but concentrate on KO binding.
Few important things to know:

  1. For your JavaScript variables to be usable in "dynamic" binding, you have to declare them as ko.observable (or ko.observablearray or ko.computed)
  2. In HTML, KO binding is done by adding "data-bind" attribute in proper syntax
For more details, please refer to KO website.

The AJAX style data-grid

Go ahead and read the following snippet. It's self explanatory. The DataGridAjax class has a constructor that accepts the URL to the function that returns paginated/sorted data (in our case GetStudentList). GridParams is a KO observable object that saves current pageIndex, pageSize, etc. DataRows is a KO observablearray that we will bind to our view. Note that though I haven't named it 'viewmodel', MVVM naming conventions would prefer that.

RIT.eW.DataGridAjax = (function () {
    var getDataUrl = '';
    function DataGridAjax(url, pageSize) {
        var self = this;
        getDataUrl = url;
        self.GridParams = {
            pageIndex: ko.observable(1),
            pageSize: ko.observable(pageSize),
            sortField: ko.observable(''),
            sortOrder: ko.observable('ASC'),
            totalRows: ko.observable(0),
            totalPages: ko.observable(0),
            requestedPage: ko.observable(0),
            pageSizeOptions: [5, 10, 20, 30, 50, 100]
        };
        self.DataRows = ko.observableArray();
        self.SelectedPageSizeOption = ko.observable(pageSize);
        self.GridParams.requestedPage.subscribe(self.FlipPageDirect, self);
        self.SelectedPageSizeOption.subscribe(self.ChangePageSize, self);
    }
    DataGridAjax.prototype.GetData = function () {
        var self = this;
        RIT.eW.Services.AjaxPostCall(getDataUrl, self.GridParams, 
          $.proxy(self.OnGetDataDone, this));
          // this is required to retain the context of "this" keyword
    };
    DataGridAjax.prototype.OnGetDataDone = function (data) {
        var self = this;
        self.DataRows(RIT.eW.Utils.GetJson(data.result));
        self.GridParams.totalRows(RIT.eW.Utils.GetJson(data.totalRows));
        var totalPages = Math.ceil(self.GridParams.totalRows() / self.GridParams.pageSize());
        self.GridParams.totalPages(totalPages);
        self.GridParams.requestedPage(self.GridParams.pageIndex());
    };
    DataGridAjax.prototype.FlipPage = function (newPageNo) {
        var self = this;
        if (parseInt(newPageNo) > 0 && parseInt(newPageNo) <= self.GridParams.totalPages()) {
            self.GridParams.pageIndex(newPageNo);
            self.GetData();
        }
    };
    DataGridAjax.prototype.FlipPageDirect = function (newValue) {
        var self = this;
        var ri = parseInt(self.GridParams.requestedPage());
        if ( ri == NaN) {
            self.GridParams.requestedPage(self.GridParams.pageIndex());
            return;
        }
        if (ri > 0 && ri <= self.GridParams.totalPages()) {
            self.GridParams.pageIndex(ri);
            self.GetData();
            return;
        }
        self.GridParams.requestedPage(self.GridParams.pageIndex());
        return;
    };
    DataGridAjax.prototype.ChangePageSize = function () {
        var self = this;
        if (self.GridParams.pageSize() != self.SelectedPageSizeOption()) {
            self.GridParams.pageSize(self.SelectedPageSizeOption());
            self.GridParams.pageIndex(1);
            self.GridParams.requestedPage(1);
            self.GetData();
        }
    };
    DataGridAjax.prototype.Sort = function (col) {
        var self = this;
        if (self.GridParams.sortField() === col) {
            if (self.GridParams.sortOrder() === 'ASC') {
                self.GridParams.sortOrder('DESC');
            } else {
                self.GridParams.sortOrder('ASC');
            }
        } else {
            self.GridParams.sortOrder('ASC');
            self.GridParams.sortField(col);
        }
        self.GetData();
    };
    return DataGridAjax;
})();

The JavaScript (non-AJAX) style data-grid

Similarly, the code for the JavaScript data-grid:

RIT.eW.DataGridBasic = (function () {
    var getDataUrl = '';
    var allDataRows = new Array();
    function DataGridBasic(url, pageSize) {
        var self = this;
        getDataUrl = url;
        self.GridParams = {
            pageIndex: ko.observable(1),
            pageSize: ko.observable(pageSize),
            sortField: ko.observable(''),
            sortOrder: ko.observable('ASC'),
            totalRows: ko.observable(0),
            totalPages: ko.observable(0),
            requestedPage: ko.observable(0),
            pageSizeOptions: [5, 10, 20, 30, 50, 100]
        };
        self.DataRows = ko.observableArray();
        self.SelectedPageSizeOption = ko.observable(pageSize);
        self.GridParams.requestedPage.subscribe(self.FlipPageDirect, self);
        self.SelectedPageSizeOption.subscribe(self.ChangePageSize, self);
    }
    DataGridBasic.prototype.GetData = function () {
        var self = this;
        RIT.eW.Services.AjaxPostCall(getDataUrl, '', $.proxy(self.OnGetDataDone, this));
    };
    DataGridBasic.prototype.OnGetDataDone = function (data) {
        var self = this;
        allDataRows = RIT.eW.Utils.GetJson(data.result);
        self.GridParams.totalRows(RIT.eW.Utils.GetJson(data.totalRows));
        self.UpdateData();
    };
    DataGridBasic.prototype.UpdateData = function () {
        var self = this;
        self.DataRows(self.GetPagedData());
        var totalPages = Math.ceil(self.GridParams.totalRows() / self.GridParams.pageSize());
        self.GridParams.totalPages(totalPages);
        self.GridParams.requestedPage(self.GridParams.pageIndex());
    };
    DataGridBasic.prototype.FlipPage = function (newPageNo) {
        var self = this;
        if (parseInt(newPageNo) > 0 && parseInt(newPageNo) <= self.GridParams.totalPages()) {
            self.GridParams.pageIndex(newPageNo);
            self.UpdateData();
        }
    };
    DataGridBasic.prototype.FlipPageDirect = function (newValue) {
        var self = this;
        var ri = parseInt(self.GridParams.requestedPage());
        if (ri == NaN) {
            self.GridParams.requestedPage(self.GridParams.pageIndex());
            return;
        }
        if (ri > 0 && ri <= self.GridParams.totalPages()) {
            self.GridParams.pageIndex(ri);
            self.UpdateData();
            return;
        }
        self.GridParams.requestedPage(self.GridParams.pageIndex());
        return;
    };
    DataGridBasic.prototype.ChangePageSize = function () {
        var self = this;
        if (self.GridParams.pageSize() != self.SelectedPageSizeOption()) {
            self.GridParams.pageSize(self.SelectedPageSizeOption());
            self.GridParams.pageIndex(1);
            self.GridParams.requestedPage(1);
            self.UpdateData();
        }
    };
    DataGridBasic.prototype.Sort = function (col) {
        var self = this;
        if (self.GridParams.sortField() === col) {
            if (self.GridParams.sortOrder() === 'ASC') {
                self.GridParams.sortOrder('DESC');
            } else {
                self.GridParams.sortOrder('ASC');
            }
        } else {
            self.GridParams.sortOrder('ASC');
            self.GridParams.sortField(col);
        }
        allDataRows.sort(self.dynamicSort(self.GridParams.sortField(), 
                         self.GridParams.sortOrder()));
        self.UpdateData();
    };
    DataGridBasic.prototype.GetPagedData = function() {
        var self = this;
        var size = self.GridParams.pageSize();
        var start = (self.GridParams.pageIndex()-1)*size;
        return allDataRows.slice(start, start + size);
    };
    DataGridBasic.prototype.dynamicSort = function (sortProperty, direction) {
        debugger;
        var thisMethod = function(a, b) {
            var valueA = a[sortProperty];
            var valueB = b[sortProperty];
            if (typeof valueA != "number" && typeof valueA != "object") {
                valueA = a[sortProperty].toLowerCase();
                valueB = b[sortProperty].toLowerCase();
            }
            if (direction.toLowerCase() == "asc") {
                if (valueA < valueB) { return -1; }
                if (valueA > valueB) { return 1; }
            } else { if (valueA > valueB) { return -1; }
                if (valueA < valueB) { return 1; }
            } return 0;
        }; return thisMethod;
    };
    return DataGridBasic;
})();

The HTML

Now here you will see the real power of KnockoutJS (KO). First, please look at the code:

<div id="studentListGrid">
    <table class="grdTbl">
        <thead class="grdTblHead">
            <tr>
                <th><a href="#" onclick="RIT.eW.Dashboard.StudentDataGrid.Sort('Name')" class="sortCol">Name</a></th>
                <th><a href="#" onclick="RIT.eW.Dashboard.StudentDataGrid.Sort('Age')" class="sortCol">Age</a></th>
                <th>School Name</th>
                <th>School Address</th>
            </tr>
        </thead>
        <tbody data-bind="foreach: RIT.eW.Dashboard.StudentDataGrid.DataRows">
            <tr>
                <td data-bind="text: Name"></td>
                <td data-bind="text: Age"></td>
                <td data-bind="text: School.Name"></td>
                <td data-bind="text: School.Address"></td>
            </tr>
        </tbody>
    </table>
    <div class="pagerWrap">
        <ul class="grdLinePager">
            <li class="liBgFirst"><a href="#" onclick="RIT.eW.Dashboard.StudentDataGrid.FlipPage(1)"></a></li>
            <li class="liBgPrev"><a href="#" data-bind="click: function () { RIT.eW.Dashboard.StudentDataGrid.FlipPage(RIT.eW.Dashboard.StudentDataGrid.GridParams.pageIndex() - 1)  }"></a></li>
            <li class="liBgCur">
                <input data-bind="value: RIT.eW.Dashboard.StudentDataGrid.GridParams.requestedPage" type="text" />
                <span>of total</span>
                <span data-bind="text: RIT.eW.Dashboard.StudentDataGrid.GridParams.totalPages()"></span>
                <span> pages</span>
            </li>
            <li class="liBGNext"><a href="#" data-bind="click: function () { RIT.eW.Dashboard.StudentDataGrid.FlipPage(RIT.eW.Dashboard.StudentDataGrid.GridParams.pageIndex() + 1) }"></a></li>
            <li class="liBGLast"><a href="#" data-bind="click: function () { RIT.eW.Dashboard.StudentDataGrid.FlipPage(RIT.eW.Dashboard.StudentDataGrid.GridParams.totalPages()) }"></a></li>
        </ul>
        <div class="pagerNumWrap">
            <span># of rows in page </span>
            <select data-bind="options: RIT.eW.Dashboard.StudentDataGrid.GridParams.pageSizeOptions, value: RIT.eW.Dashboard.StudentDataGrid.SelectedPageSizeOption "></select>
        </div>      
    </div>
</div>

Now we will create instance of our JavaScript DataGrid class and bind it to our HTML table

RIT.eW.Dashboard = RIT.eW.Dashboard || {};
RIT.eW.Dashboard.StudentDataGrid = new RIT.eW.DataGridBasic('/Home/GetAllStudentList', 5);
//uncomment below to create AJAX based datagrid instead
//RIT.eW.Dashboard.StudentDataGrid = new RIT.eW.DataGridAjax('/Home/GetStudentList',10);                   
RIT.eW.Dashboard.Init = function () {
    RIT.eW.Dashboard.StudentDataGrid.GetData();
    ko.applyBindings(RIT.eW.Dashboard.StudentDataGrid.DataRows, $("#studentListGrid")[0]);
};
RIT.eW.Dashboard.Init();

Note: Yes, you are right, a variable with a smaller name (instead of RIT.eW.Dashboard.StudentDataGrid) would have made the above code & HTML much concise.

The above JavaScript classes are all reusable in your application. You can create as many grids in your page as you want. Besides you can customize the HTML for more functionality.

Summary

This was a brief introduction to the real potential of Knockout JS when used with well structured JavaScript. Happy coding.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Dharmesh Kemkar
Software Developer (Senior)
Australia Australia
I am a .NET developer with extensive experience in ASP.NET, SharePoint and XAML development. I love implementing s/w patterns (MVC/MVVM) and develop testable code. My first degree in Electrical Engineering and some experience in Telecomm switching makes me feel nostalgic looking at electronic circuits and networks.

You may also be interested in...

Comments and Discussions

 
GeneralMy vote of 5 Pin
bhalaniabhishek15-Jul-15 23:58
memberbhalaniabhishek15-Jul-15 23:58 
QuestionGood Article Pin
Avadhesh Kumar Maurya7-May-15 2:04
memberAvadhesh Kumar Maurya7-May-15 2:04 
AnswerRe: Good Article Pin
Dharmesh Kemkar11-May-15 11:22
memberDharmesh Kemkar11-May-15 11:22 
QuestionMy Vote of 5+ Pin
Amarjeet Singh24-Mar-14 9:13
memberAmarjeet Singh24-Mar-14 9:13 
AnswerRe: My Vote of 5+ Pin
Dharmesh Kemkar24-Mar-14 13:01
memberDharmesh Kemkar24-Mar-14 13:01 
GeneralRe: My Vote of 5+ Pin
Amarjeet Singh24-Mar-14 14:09
memberAmarjeet Singh24-Mar-14 14:09 
GeneralRe: My Vote of 5+ Pin
Amarjeet Singh24-Mar-14 14:25
memberAmarjeet Singh24-Mar-14 14:25 
QuestionGreat Topic Pin
AymanHussam25-Feb-14 22:34
memberAymanHussam25-Feb-14 22:34 
AnswerRe: Great Topic Pin
Dharmesh Kemkar27-Feb-14 18:31
memberDharmesh Kemkar27-Feb-14 18:31 
GeneralExcellent Article Pin
chauhan.munish17-Dec-13 0:02
memberchauhan.munish17-Dec-13 0:02 
GeneralRe: Excellent Article Pin
Dharmesh Kemkar17-Dec-13 14:04
memberDharmesh Kemkar17-Dec-13 14:04 
GeneralMy vote of 5 Pin
dgDavidGreene16-Oct-13 19:40
memberdgDavidGreene16-Oct-13 19:40 
GeneralRe: My vote of 5 Pin
Dharmesh Kemkar17-Oct-13 13:17
memberDharmesh Kemkar17-Oct-13 13:17 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.150731.1 | Last Updated 1 Oct 2013
Article Copyright 2013 by Dharmesh Kemkar
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid