A book's pages blowing in the wind

When I suggest that developers consider using web services and a more client-centric approach to solve their UpdatePanel performance problems, the lack of paging is often their first objection.

Conventional ASP.NET wisdom seems to hold that the GridView/UpdatePanel combo is king when asynchronously paging through a data source. If you’ll give me a few minutes of your time, I’d like to challenge that notion!

In this post, I’m going to show you how to implement great client-side paging, using jQuery and ASP.NET AJAX. I’ll be building on foundation laid in two previous posts: Use jQuery and ASP.NET AJAX to build a client side Repeater and How to easily enhance your existing tables with simple CSS. If you haven’t yet, I recommend that you read those posts first, so that we’re all on the same page.

This post will be a bit longer than those were, but I’ve tried to divide the ordeal into granular steps that break the monotony as much as possible:

  • Setting up a page method to retrieve individual pages of data.
  • Using jQuery to consume that page method and render its result.
  • Adding progress indication during the initial load.
  • Adding the basic HTML paging controls.
  • Wiring up event handlers for the paging controls.
  • Implementing the paging functions that those handlers call.
  • Adding progress indication during the paging operations.
  • Using a page method to dynamically determine the size of the data source.

Retrieving a particular “page” of data

Our existing GetFeedBurnerItems page method accepts a PageSize parameter, which specifies how many items it should return. To implement paging, we need to add a new parameter: Page.

This second argument will specify which page items will be returned from.

[WebMethod]
public static IEnumerable GetFeedburnerItems(int PageSize, int Page)
{
XDocument feedXML =
XDocument.Load("http://feeds.encosia.com/Encosia");
 
var feeds =
from feed in feedXML.Descendants("item")
select new
{
Date = DateTime.Parse(feed.Element("pubDate").Value)
.ToShortDateString(),
Title = feed.Element("title").Value,
Link = feed.Element("link").Value,
Description = feed.Element("description").Value,
};
 
return feeds.Skip((Page - 1) * PageSize).Take(PageSize);
}

Using the supplied Page argument, our service uses IQueryable’s Skip() extension method to return the correct page of results.

The purpose of the subtraction is to make the Page argument 1-based. I don’t think the notion of a “page 0″ makes much sense in this scenario, so I prefer they start at 1.

Consuming the service and rendering the result

Now that our service is modified to provide paged results, we can consume the first page of results similar to how we did in the previous examples.

// Set this to any integer you like. 5-7 works well
//  with the FeedBurner data source.
var pageSize = 5;
 
$(document).ready(function() {
// Display the first page of results initially.
DisplayRSSTable(1);
});
 
function DisplayRSSTable(page) {
$.ajax({
type: "POST",
url: "Default.aspx/GetFeedBurnerItems",
data: "{'PageSize':'" + pageSize + "', 'Page':'" + page + "'}",
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(msg) {
// Render the resulting data, via template.
ApplyTemplate(msg);
}
});
}
 
function ApplyTemplate(msg) {
// Changed the template extension from .tpl to .htm, 
//  to avoid the request being blocked by some IIS installs.
$('#Container').setTemplateURL('RSSTable.htm',
null, { filter_data: false });
$('#Container').processTemplate(msg);
}

Since we’ll be calling the page method from other parts of the code, it made sense to refactor that functionality into a separate function: DisplayRSSTable.

Other than that, this is basically the same code that you’ve seen in the previous jQuery templating examples.

Adding initial progress indication on page load

FeedBurner is a great service, but tends to be a bit slow. Due to that latency on the response, the initial page often takes a few seconds to render. This has prompted several of you to ask about adding progress indication, which is a great idea.

There are many perfectly legitimate ways of accomplishing this, but I want to focus on minimizing the harshness of the transition between progress indication and content display. Jumping from a simple loading indicator to a full table is jarring.

One way to smooth that transition is to use a structure very similar to the final content’s template:

<div id="Container">
<table id="RSSTable" cellspacing="0">
<thead>
<tr>
<th></th>
</tr>
</thead>
<tbody>
<td class="loading"></td>
</tbody>
</table>
</div>

The .loading CSS class is defined as follows:

table tbody .loading {
/* Since the table may be empty, set a decent default height. */
height: 350px;
 
/* Center an animated progress indicator over the table body. */
background-image: url(images/progress-indicator.gif);
background-position: center center;
background-repeat: no-repeat;
}

The end result is an empty table, with a centered progress indicator inside its single 350px tall body cell.

Adding paging controls to the HTML template

Now that the first page of results is coming through smoothly, we need to add a paging interface to get at the rest of the data. To keep this from running longer than a Steve Yegge post, let’s stick with a simple previous/next system.

Since it’s logically a part of the rendered template, I decided to add the paging controls to the template itself:

<table id="RSSTable" cellspacing="0">
<thead>
<tr>
<th width="80">Date</th>
<th>Title / Excerpt</th>
</tr>
</thead>
<tbody>
{#foreach $T.d as post}
<tr>
<td rowspan="2">{$T.post.Date}</td>
<td><a href="{$T.post.Link}">{$T.post.Title}</a></td>
</tr>
<tr>
<td>{$T.post.Description}</td>
</tr>
{#/for}
</tbody>
</table>
 
<a id="PrevPage" class="paging">Previous Page</a>
<a id="NextPage" class="paging">Next Page</a>

This template is very similar to the template that was used in the past two posts. The important changes are:

  • The post title and excerpt are rearranged to be vertically oriented.
  • The date column is fixed width, to maintain consistent column widths.
  • Of course, the paging controls are added to the bottom of the table.

After implementing these changes, our rendered table is shaping up nicely:

An example of the rendered template

Initializing the paging interface elements

Now that we’ve got the template rendering those anchor tags, the next step is to add functionality to make them do something:

// Initialize this and store globally for tracking state.
var currentPage = 1;
 
// The feed has 15 items, and we're displaying 5 per page.
var lastPage = 3;
 
function UpdatePaging() {
// If we're not on the first page, enable the "Previous" link.
if (currentPage != 1) {
$('#PrevPage').attr('href', '#');
$('#PrevPage').click(PrevPage);
}
 
// If we're not on the last page, enable the "Next" link.
if (currentPage != lastPage) {
$('#NextPage').attr('href', '#');
$('#NextPage').click(NextPage);
}
}

UpdatePaging adds an href attribute of # to one or both of the paging anchor tags, thereby making them active links. This is primarily to give your users a good UI hint that they can click the links. The anchor’s actual navigation functionality won’t be needed.

.click() is jQuery’s way of attaching click handlers to elements. In this case, we’re wiring up a couple of functions that will handle the actual task of changing pages.

Putting it together, DisplayRSSTable needs to be updated to call UpdatePaging immediately after every template rendering:

function DisplayRSSTable(page) {
$.ajax({
type: "POST",
url: "Default.aspx/GetFeedBurnerItems",
data: "{'PageSize':'" + pageSize + "', 'Page':'" + page + "'}",
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(msg) {
// Render the resulting data, via template.
ApplyTemplate(msg);
 
// Wireup appropriate paging functionality.
UpdatePaging();
}
});
}

By doing this, we can be sure that the paging functionality always reflects the currently rendered page.

Making it all work: implementing the paging functions

Finally, it’s time to implement the PrevPage and NextPage functions that are responsible for actually changing pages.

This is where you really appreciate that the navigation links are generated as part of the template. That allows us to fire off a DisplayRSSTable and not worry about updating the navigation click handlers. Those updates to the paging controls will be automatically handled by the UpdatePaging after the requested page’s template renders.

function NextPage(evt) {
// Prevent the browser from navigating needlessly to #.
evt.preventDefault();
 
// Entertain the user while the previous page is loaded.
DisplayProgressIndication();
 
// Load and render the next page of results, and
//  increment the current page number.
DisplayRSSTable(++currentPage);
}
 
function PrevPage(evt) {
// Prevent the browser from navigating needlessly to #.
evt.preventDefault();
 
// Entertain the user while the previous page is loaded.
DisplayProgressIndication();
 
// Load and render the previous page of results, and
//  decrement the current page number.
DisplayRSSTable(--currentPage);
}

The call to preventDefault cancels the browser’s navigation to #. It doesn’t interfere with the paging functionality to allow that navigation, but allowing it will unnecessarily send mixed signals to your users.

Using the currentPage global variable, we can use our original DisplayRSSTable function to request one page up or down from the current page.

Progress indication during paging requests

Since each page request takes a couple seconds to complete, it’s important to provide progress indication during that time. This can be implemented in the same way as the initial page load progress indication. We’ll even reuse the same .loading CSS class.

function DisplayProgressIndication() {
// Hide both of the paging controls,
//  to avoid click-happy users.
$('.paging').hide();
 
// Clean up our event handlers, to avoid memory leaks.
$('.paging').unbind();
 
// Store the height of the content area of the table.
var height = $('#RSSTable tbody').height();
 
// Replace the entire content area with a single row/cell.
$('#RSSTable tbody').html('<tr><td colspan="2"></td></tr>');
 
// Set that row's height to be the same as previous.
$('#RSSTable tbody tr').height(height);
 
// Add our centered progress indicator animation to it.
$('#RSSTable tbody td').addClass('loading');
}

This function does the following:

  • Hides the paging controls and clears any event handlers on them.
  • Determines the current height of the table’s content area.
  • Drops the entire content area and replaces it with a single cell.
  • Sets that cell’s containing row’s height to the previously determined value.
  • Add the .loading class to that cell, so that a progress animation appears centered over the same areas that the content used to occupy.

Dynamically determine the size of our data

So far, we’ve been working under the assumption that there will always be 15 elements in the data source and that it will never change at run time. While this might be a workable assumption for my RSS feed, it certainly would not be for most other data sources.

We need to dynamically detect the size of our data.

The first step in implementing this functionality is to interrogate our data source on the server side. This page method will return the total number of items in the feed:

[WebMethod]
public static int GetFeedBurnerItemCount()
{
XDocument feedXML =
XDocument.Load("http://feeds.encosia.com/Encosia");
 
return feedXML.Descendants("item").Count();
}

Using the $.ajax() syntax that I have previously described, we can request the count of data items from our new page method. Using that and the global pageSize variable, it’s easy to determine the lastPage:

// Initialize this to 1, so that "Next" is disabled until
//  GetItemCount returns and we know there's a second page.
var lastPage = 1;
 
function GetRSSItemCount() {
$.ajax({
type: "POST",
url: "Default.aspx/GetFeedBurnerItemCount",
data: "{}",
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(msg) {
// msg.d will contain the total number of items. 
// Divide and round up to find total number of pages.
lastPage = Math.ceil(msg.d / pageSize);
 
// Wireup appropriate paging functionality.
UpdatePaging();
}
});
}

Now that we’re doing things more correctly, it makes more sense to initialize the lastPage variable at one. Until we’ve called our new GetFeedBurnerItemCount method and know the size of the data, we have no reason to assume there is more than one page of data.

To put this in action, we can add it to the $(document).ready handler:

$(document).ready(function() {
// On page load, display the first page of results.
DisplayRSSTable(1);
 
// Simultaneously, begin loading the total item count.
GetRSSItemCount();
});

The end result is that both paging controls will originally be in a disabled state, until GetRSSItemCount is able to verify that at least (pageSize + 1) items exist in the data source.

If you’re implementing paging against a relatively static data source, like my RSS feed, there’s no need to add this functionality. However, I’m providing the added flexibility in hopes that it will make this more easily adaptable to your particular situation.

Conclusion

This has been a long post, but hopefully each individual part was digestible. As with most things, deconstructing the problem into manageable pieces makes the overall task easy. A few more thoughts include:

Error Handling. If you use this code, please do add error and timeout handling to your service calls. For the sake of brevity and clarity, I don’t address error handling in posts not specifically about that topic. However, you should never do that in production.

Caching. Depending on your data source, you may want to make use of the Cache class in your page method. For instance, this RSS example would benefit massively from Caching. However, you may just as likely be paging into a huge data source, in which case it wouldn’t make any sense to Cache.

Do keep it in mind though. With caching, paging through this example is almost instantaneous after the initial load.

That’s it. I hope you found this helpful.

Download the source

Download Source: client-paging.zip (38kb)