Saturday, March 24, 2012

How to build a grid with knockout - Part 2: Paging

In Part 1 of this tutorial we built a simple HTML table using knockout, in this post we're going to add paging functionality to it.

The code for the entire index.cshtml view now looks as follows:
@{
    ViewBag.Title = "Index";
}
 
<h2>Index</h2>
 
<table id="people">
    <thead>
    <tr>
        <th>First Name</th>
        <th>Last Name</th>
        <th>Age</th>
    </tr>
    </thead>
    <tbody data-bind="foreach: people">
        <tr>
            <td><span data-bind="text: FirstName"></span></td>
            <td><span data-bind="text: LastName"></span></td>
            <td><span data-bind="text: Age"></span></td>
        </tr>
    </tbody>
</table>
<div class='grid_footer ui-helper-clearfix'>
    <div class='grid_info'>
        <span data-bind='text: pagesText'></span>
    </div>
    <div class='paging'>
        <a data-bind='visible: page() > 1, click: PageFirst' class='ui-button'>First</a>
        <a data-bind='visible: page() > 1, click: PageBack' class='ui-button'>Previous</a>
        <a data-bind='visible: page() < totalPages(), click: PageNext' class='ui-button'>Next</a> 
        <a data-bind='visible: page() < totalPages(), click: PageLast' class='ui-button'>Last</a>
    </div>
</div>
 
 
<script type="text/javascript">
    function peopleViewModel() {
        var _this = {};
 
        //pager data
        _this.page = ko.observable(1);
        _this.records = ko.observable(1);
        _this.totalPages = ko.observable(1);
        _this.rowsPerPage = ko.observable(1);
        _this.pagesText = ko.computed(function () { return _this.page() + 
        " of " + _this.totalPages() + " pages"; });

        _this.people = ko.observableArray();
 
        /************************
        Public Functions
        ************************/
 
        function PageFirst(item) {
            _this.page(1);
            LoadDataFromServer();
        }
 
        _this.PageFirst = PageFirst;
 
        function PageLast(item) {
            _this.page(_this.totalPages());
            LoadDataFromServer();
        }
 
        _this.PageLast = PageLast;
 
 
        function PageBack(item) {
            _this.page(_this.page() - 1);
            LoadDataFromServer();
        }
 
        _this.PageBack = PageBack;
 
        function PageNext(item) {
            _this.page(_this.page() + 1);
            LoadDataFromServer();
        }
 
        _this.PageNext = PageNext;
 
        function LoadDataFromServer() {
            var url = '/people/data';
            //add paging params
            url += '?rows=' + _this.rowsPerPage() +
            '&page=' + _this.page();
 
            $.post(
                url,
                function (data) {
                    _this.records(data.TotalRowsCount);
                    _this.totalPages(data.TotalPageCount);
 
                    var results = ko.observableArray();
                    _this.people.removeAll();
 
                    ko.mapping.fromJS(data.GridData, {}, results);
                    for (var i = 0; i < results().length; i++) {
                        _this.people.push(results()[i]);
                    };
                },
                'json'
            )
        }
 
        /************************
        Initialization
        ************************/
 
        ko.applyBindings(_this, $("body").get(0));
        LoadDataFromServer();
 
        return _this;
    }
 
    var viewModel = peopleViewModel();
 
</script>


Step 1 - The HTML
If you look at the listing above you'll see I've added a <div> below the <table>, this is where the elements of the pager are going. Within the <div> I've added four <a>s for each of the paging buttons that will be required and a <span> to show some text. I've also introduced some new knockout binding methods: visible which determines if a span will be shown or not, and click which will call a function on our javascript model when the <a> is clicked.

Step 2 - The Javascript
Within our javascript view model I've added observables to track the information that we need to make the pager work. I've also added a pagesText variable which is a ko.computed() function. This means that when any of the observables that the function references are changed knockout will do a rebind, this is paricularly usefull in this instance.

The other major change is in the LoadDataFromServer() function where we now get an object back from the server that not only contains the rows for the grid but paging information including the total number of rows and the total number of pages.

I've set the number of rows per page in this instance to 1 so that the paging functionality is obvious with only 3 rows!

Step 3 - The Controller 
The code for the controller method now looks as follows:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
namespace knockoutGrid.Controllers
{
    public class PeopleController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }
 
 
        public JsonResult Data(int rows, int page)
        {
            List<Person> people = new List<Person>();
            people.Add(new Person("Richie""McCaw", 33));
            people.Add(new Person("Dan""Carter", 32));
            people.Add(new Person("Owen""Franks", 23));
 
            GridDataResult gridDataResult = new GridDataResult();
            gridDataResult.TotalRowCount = people.Count;
 
            int totalPages = 1;
            if (rows > 0)
            {
                totalPages = gridDataResult.TotalRowCount / rows;
                if (gridDataResult.TotalRowCount % rows != 0)
                    totalPages += 1;
            }
 
            gridDataResult.TotalPageCount = totalPages;
            gridDataResult.GridData = people.Skip((page - 1) * rows).Take(rows);
 
            return Json(gridDataResult);
        }
 
    }
 
 
    public class GridDataResult
    {
        public int TotalRowCount { getset; }
        public int TotalPageCount { getset; }
        public object GridData { getset; }
    }
 
    public class Person
    {
        public string FirstName { getset; }
        public string LastName { getset; }
        public int Age { getset; }
 
        public Person(string firstName, string lastName, int age)
        {
            this.FirstName = firstName;
            this.LastName = lastName;
            this.Age = age;
        }
    }
 
} 





I've added a class called GridDataResult - this will enable us to send some more information about the paging back to the browser, because I want to use this class for all my controller methods that return data to a grid I have made the GridData property of type object (you could use generics if you wanted to). The MVC JSON serializer has no problems with serializing this into JSON properly.

The GridData() controller method is still straight forward with some added logic to work out the total rows and pages for the grid. The LINQ methods skip() and next() are used to get the correct subset of the data to be sent back . If you are using a database framework that supports LINQ (such as the Entity Framework) to retrieve your records from a database this is particularly usefull.

And that's it! A grid with paging - done!

2 comments:

  1. Hi David,

    Nice tutorial. Knockout looks powerful with features like conditions in bindings and function bindings and the ko.computed() function. I think I will spend some time learning it soon.
    Thank you for the great presentation!

    ReplyDelete
  2. Please host a demo page also. and provide the download link for the code.

    ReplyDelete