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!

How to build a grid with knockout and ASP.Net MVC


Fairly recently the team at CBS decided to adopt knockout to help us build our UIs. One of our major undertakings was to replace the JQGrid with a knockout equivalent, we haven't regretted it for a minute. The ability to quickly adapt the functionality to meet the different demands of our various clients plus the ability to style easily and effectively (a big gripe with the JQGrid)  has paid off handsomely.

This tutorial is broken into 3 parts:
Part 1 - building the basic HTML table using knockout and loading data from the server
Part 2 - implementing paging
Part 3 - implementing sorting


In this tutorial I'm going to take you through building a basic html table using knockout and retrieving data from a controller method on the server.

Step 1 - Install the knockout and knockoutMapping js files
You can either download these from the knockout website or use NuGet - if you're using NuGet then you need to install the knockoutjs package and the knockout.mapping package this will add a couple of js files to the scripts folder in your solution.

Once you have the js files added you need to reference them, I normally add them to my _Layout.cshtml file as I use them on virtually every page in my applications. Your _layout.cshtml file should look as follows:

<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/knockout.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/knockout.mapping-latest.js")" type="text/javascript"></script>
</head>
<body>
    <div class="page">
        <div id="header">
            <div id="title">
                <h1>My MVC Application</h1>
            </div>
            <div id="logindisplay">
                @Html.Partial("_LogOnPartial")
            </div>
            <div id="menucontainer">
                <ul id="menu">
                    <li>@Html.ActionLink("Home""Index""Home")</li>
                    <li>@Html.ActionLink("About""About""Home")</li>
                    <li>@Html.ActionLink("People""Index""People")</li>
                </ul>
            </div>
        </div>
        <div id="main">
            @RenderBody()
        </div>
        <div id="footer">
        </div>
    </div>
</body>
</html>
 
Step 2 - Build a ViewThe view I added for this demo is called Index.cshtml and lives in a Views/People folder. The code 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>
 
<script type="text/javascript">
    function peopleViewModel() {
        var _this = {};
 
        _this.people = ko.observableArray();
        ko.applyBindings(_this, $("#people").get(0));
 
        function LoadPeopleFromServer() {
            $.post(
                '/people/data',
                function (data) {
                    var results = ko.observableArray();
                    ko.mapping.fromJS(data, {}, results);
                    for (var i = 0; i < results().length; i++) {
                        _this.people.push(results()[i]);
                    };
                },
                'json'
            )
        }
 
        LoadPeopleFromServer();
 
        return _this;
    }
 
    var viewModel = peopleViewModel();
 
</script>
 
 
I've kept the HTML and Javascript in one file in order to make this easy to follow. If you have no experience of knockout I would strongly suggest you follow the excellent interactive tutorial on the knockout site. The HTML is vanilla knockout, it uses the data-bind attribute to specify how knockout binds values on the javascript model to DOM elements.

The javascript is more interesting: you'll notice that the peopleViewModel() object has a property _this.people which is an observableArray, this means that knockout will take notice when elements are added and removed from it and update the DOM accordingly. The LoadPeopleFromServer() method makes use of the jQuery ($.post) method to retrive data from the server in a JSON format, this JSON is returned to us as an array of javascript objects by JQuery, this is then taken by the knockout mapping utility and turned into an ko.observableArray of javascript objects. The beauty of using the mapping utlity is that we don't have to create any javascript objects to represent people, this is all done for us! This also has the advatage that if we add a new property to a person object on the server we don't need to change any javascript, only the HTML where we want the new value shown.

You may wonder why _this.people has been declared and we then push objects into it once they have returned from the server rather than just passing it into the mapping method. The reason for this is that the mapping method creates a new observableArray and assigs it to the variable you passed in. This ruins your knockout bindings as they were bound to the original array that you declared. You can get around this by only binding once you have got data back from the server but this gets more complicated once you introduce paging, etc. This pattern works and is easy to follow and implement.

Step 3 - The Controller Method
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()
        {
            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));
 
            return Json(people);
        }
 
    }
 
    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;
        }
    }
 
}

The controller code is relatively simple, normally I would be retrieving data from a database but here I've hard coded it. The only things you really need to take heed of are the use of the JsonResult type that is being returned and the Json method that is needed to turn the list of people into valid JSON.

In Part 2 I will show you how to implement paging!

I have put the source code up on bitbucket, you can download it here.