One objection that people always raise against Ajax applications concerns browser history. Because an Ajax application updates its content by performing sneaky Ajax postbacks, the browser backwards and forwards buttons don’t work as you would normally expect.
In a normal, non-Ajax application, when you click the browser back button, you return to a previous state of the application. For example, if you are paging through a set of movie records, you might return to the previous page of records.
In an Ajax application, on the other hand, the browser backwards and forwards buttons do not work as you would expect. If you navigate to the second page in a list of records and click the backwards button, you won’t return to the previous page. Most likely, you will end up navigating away from the application entirely (which is very unexpected and irritating).
Bookmarking presents a similar problem. You cannot bookmark a particular page of records in an Ajax application because the address bar does not reflect the state of the application.
The Ajax Solution
There is a solution to both of these problems. To solve both of these problems, you must take matters into your own hands and take responsibility for saving and restoring your application state yourself. Furthermore, you must ensure that the address bar gets updated to reflect the state of your application.
In this blog entry, I demonstrate how you can take advantage of a jQuery library named bbq that enables you to control browser history (and make your Ajax application bookmarkable) in a cross-browser compatible way.
The JavaScript Libraries
In this blog entry, I take advantage of the following four JavaScript files:
- jQuery-1.4.2.js – The jQuery library. Available from the Microsoft Ajax CDN at http://ajax.microsoft.com/ajax/jquery/jquery-1.4.2.js
- jquery.pager.js – Used to generate pager for navigating records. Available from http://plugins.jquery.com/project/Pager
- microtemplates.js – John Resig’s micro-templating library. Available from http://ejohn.org/blog/javascript-micro-templating/
- jquery.ba-bbq.js – The Back Button and Query (BBQ) Library. Available from http://benalman.com/projects/jquery-bbq-plugin/
All of these libraries, with the exception of the Micro-templating library, are available under the MIT open-source license.
The Ajax Application
Let’s start by building a simple Ajax application that enables you to page through a set of movie database records, 3 records at a time.
We’ll use my favorite database named MoviesDB. This database contains a Movies table that looks like this:
We’ll create a data model for this database by taking advantage of the ADO.NET Entity Framework. The data model looks like this:
Finally, we’ll expose the data to the universe with the help of a WCF Data Service named MovieService.svc. The code for the data service is contained in Listing 1.
Listing 1 – MovieService.svc
using System.Data.Services;
using System.Data.Services.Common;
namespace WebApplication1
{
public class MovieService : DataService<MoviesDBEntities>
{
public static void InitializeService(DataServiceConfiguration config)
{
config.SetEntitySetAccessRule("Movies", EntitySetRights.AllRead);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
}
}
}
The WCF Data Service in Listing 1 exposes the movies so that you can query the movie database table with URLs that looks like this:
The HTML page in Listing 2 enables you to page through the set of movies retrieved from the WCF Data Service.
Listing 2 – Original.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Movies with History</title>
<link href="Design/Pager.css" rel="stylesheet" type="text/css" />
</head>
<body>
<h1>Page <span id="pageNumber"></span> of <span id="pageCount"></span></h1>
<div id="pager"></div>
<br style="clear:both" /><br />
<div id="moviesContainer"></div>
<script src="http://ajax.microsoft.com/ajax/jquery/jquery-1.4.2.js" type="text/javascript"></script>
<script src="App_Scripts/Microtemplates.js" type="text/javascript"></script>
<script src="App_Scripts/jquery.pager.js" type="text/javascript"></script>
<script type="text/javascript">
var pageSize = 3, pageIndex = 0;
// Show initial page of movies
showMovies();
function showMovies() {
// Build OData query
var query = "/MovieService.svc" // base URL
+ "/Movies" // top-level resource
+ "?$skip=" + pageIndex * pageSize // skip records
+ "&$top=" + pageSize // take records
+ " &$inlinecount=allpages"; // include total count of movies
// Make call to WCF Data Service
$.ajax({
dataType: "json",
url: query,
success: showMoviesComplete
});
}
function showMoviesComplete(result) {
// unwrap results
var movies = result["d"]["results"];
var movieCount = result["d"]["__count"]
// Show movies using template
var showMovie = tmpl("<li><%=Id%> - <%=Title %></li>");
var html = "";
for (var i = 0; i < movies.length; i++) {
html += showMovie(movies[i]);
}
$("#moviesContainer").html(html);
// show pager
$("#pager").pager({
pagenumber: (pageIndex + 1),
pagecount: Math.ceil(movieCount / pageSize),
buttonClickCallback: selectPage
});
// Update page number and page count
$("#pageNumber").text(pageIndex + 1);
$("#pageCount").text(movieCount);
}
function selectPage(pageNumber) {
pageIndex = pageNumber - 1;
showMovies();
}
</script>
</body>
</html>
The page in Listing 3 has the following three functions:
- showMovies() – Performs an Ajax call against the WCF Data Service to retrieve a page of movies.
- showMoviesComplete() – When the Ajax call completes successfully, this function displays the movies by using a template. This function also renders the pager user interface.
- selectPage() – When you select a particular page by clicking on a page number in the pager UI, this function updates the current page index and calls the showMovies() function.
Figure 1 illustrates what the page looks like when it is opened in a browser.
Figure 1
If you click the page numbers then the browser history is not updated. Clicking the browser forward and backwards buttons won’t move you back and forth in browser history.
Furthermore, the address displayed in the address bar does not change when you navigate to different pages. You cannot bookmark any page except for the first page.
Adding Browser History
The Back Button and Query (bbq) library enables you to add support for browser history and bookmarking to a jQuery application. The bbq library supports two important methods:
- jQuery.bbq.pushState(object) – Adds state to browser history.
- jQuery.bbq.getState(key) – Gets state from browser history.
The bbq library also supports one important event:
- hashchange – This event is raised when the part of an address after the hash # is changed.
The page in Listing 3 demonstrates how to use the bbq library to add support for browser navigation and bookmarking to an Ajax page.
Listing 3 – Default.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Movies with History</title>
<link href="Design/Pager.css" rel="stylesheet" type="text/css" />
</head>
<body>
<h1>Page <span id="pageNumber"></span> of <span id="pageCount"></span></h1>
<div id="pager"></div>
<br style="clear:both" /><br />
<div id="moviesContainer"></div>
<script src="http://ajax.microsoft.com/ajax/jquery/jquery-1.4.2.js" type="text/javascript"></script>
<script src="App_Scripts/jquery.ba-bbq.js" type="text/javascript"></script>
<script src="App_Scripts/Microtemplates.js" type="text/javascript"></script>
<script src="App_Scripts/jquery.pager.js" type="text/javascript"></script>
<script type="text/javascript">
var pageSize = 3, pageIndex = 0;
$(window).bind('hashchange', function (e) {
pageIndex = e.getState("pageIndex") || 0;
pageIndex = parseInt(pageIndex);
showMovies();
});
$(window).trigger('hashchange');
function showMovies() {
// Build OData query
var query = "/MovieService.svc" // base URL
+ "/Movies" // top-level resource
+ "?$skip=" + pageIndex * pageSize // skip records
+ "&$top=" + pageSize // take records
+" &$inlinecount=allpages"; // include total count of movies
// Make call to WCF Data Service
$.ajax({
dataType: "json",
url: query,
success: showMoviesComplete
});
}
function showMoviesComplete(result) {
// unwrap results
var movies = result["d"]["results"];
var movieCount = result["d"]["__count"]
// Show movies using template
var showMovie = tmpl("<li><%=Id%> - <%=Title %></li>");
var html = "";
for (var i = 0; i < movies.length; i++) {
html += showMovie(movies[i]);
}
$("#moviesContainer").html(html);
// show pager
$("#pager").pager({
pagenumber: (pageIndex + 1),
pagecount: Math.ceil(movieCount / pageSize),
buttonClickCallback: selectPage
});
// Update page number and page count
$("#pageNumber").text(pageIndex + 1);
$("#pageCount").text(movieCount);
}
function selectPage(pageNumber) {
pageIndex = pageNumber - 1;
$.bbq.pushState({ pageIndex: pageIndex });
}
</script>
</body>
</html>
Notice the first chunk of JavaScript code in Listing 3:
$(window).bind('hashchange', function (e) {
pageIndex = e.getState("pageIndex") || 0;
pageIndex = parseInt(pageIndex);
showMovies();
});
$(window).trigger('hashchange');
When the hashchange event occurs, the current pageIndex is retrieved by calling the e.getState() method. The value is returned as a string and the value is cast to an integer by calling the JavaScript parseInt() function. Next, the showMovies() method is called to display the page of movies.
The $(window).trigger() method is called to raise the hashchange event so that the initial page of records will be displayed.
When you click a page number, the selectPage() method is invoked. This method adds the current page index to the address by calling the following method:
$.bbq.pushState({ pageIndex: pageIndex });
For example, if you click on page number 2 then page index 1 is saved to the URL. The URL looks like this:
Notice that when you click on page 2 then the browser address is updated to look like:
/Default.htm#pageIndex=1
If you click on page 3 then the browser address is updated to look like:
/Default.htm#pageIndex=2
Because the browser address is updated when you navigate to a new page number, the browser backwards and forwards button will work to navigate you backwards and forwards through the page numbers. When you click page 2, and click the backwards button, you will navigate back to page 1.
Furthermore, you can bookmark a particular page of records. For example, if you bookmark the URL /Default.htm#pageIndex=1 then you will get the second page of records whenever you open the bookmark.
Summary
You should not avoid building Ajax applications because of worries concerning browser history or bookmarks. By taking advantage of a JavaScript library such as the bbq library, you can make your Ajax applications behave in exactly the same way as a normal web application.