jQuery, ASP.NET, and Browser History

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:

  1. 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
  2. jquery.pager.js – Used to generate pager for navigating records. Available from http://plugins.jquery.com/project/Pager
  3. microtemplates.js – John Resig’s micro-templating library. Available from http://ejohn.org/blog/javascript-micro-templating/
  4. 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:

clip_image002

We’ll create a data model for this database by taking advantage of the ADO.NET Entity Framework. The data model looks like this:

clip_image004

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.

clip_image006

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:

  1. jQuery.bbq.pushState(object) – Adds state to browser history.
  2. jQuery.bbq.getState(key) – Gets state from browser history.

The bbq library also supports one important event:

  1. 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:

clip_image008

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.

Discussion

  1. RN says:

    Nice use of URL Fragments

  2. Raghuraman says:

    Very Nice and detailed example !!! Thanks for the Write Up Stephen.

  3. Praveen Prasad says:

    asp.net ajax is already having this feature

    sys.application.addHistoryPoint();

    is MS going to remove that feature in future releases!!

  4. Roland says:

    That’s great! I am building a fully AJAX application and I was wondering why a “state-of-the-art application” couldn’t take advantage of the most basic features such as backward and forward buttons :p

    Stephen, as I was reading your snippet code, I noticed you use the notation “$(window)” when calling your jQuery function. Is your notation allows you to operate on DOM elements when they are loaded — like $(document).ready() provides for it?

    Regards,

    R.

  5. Abdul Rauf says:

    Nice, this was the information I was looking for.

  6. Damien says:

    Very nice blog! The only thing that is missing is a downloadable solution to play around with 🙂

  7. Thanks for the great write-up, Stephen!

    Also, if any readers are interested, there are a number of working examples linked to on the jQuery BBQ project page that illustrate how BBQ can be leveraged to handle far more complex bookmarkable states, and how it can work with caching AJAX requests (among other things)

  8. dsweb1017 says:

    Nice blog… excellent approach for calling WCF services using Client Side API’s. It offers an alternative approach to using server side logic

  9. Sohbet says:

    Thanks, Forever blog page..

  10. Mike Clark says:

    Darn. I just bought ASP.NET MVC Framework Unleashed a few days ago, then I downloaded the RTM of VS2010. I made the mistake of starting the book’s examples while using VS2010 and things are subtly different. Make that wildly different. Instead of:

    < %= Html.Textbox("Id") %>

    There is:

    < %: Html.TextBoxFor(model => model.id) %>

    !!!!

    Now I am depressed. Will there be a new edition of the book, covering, apparently, ASP.NET 4? Or whatever changed?

  11. tiny says:

    Welcome to Christian Louboutin Shoes UK on-line store! Now an extensive selection of super-fashion

    Christian Louboutin Shoes with different colors and styles are provided at the most competitive prices. You

    can buy Christian Louboutin Pumps,Christian Louboutin Boots,Christian Louboutin sandals at our online

    store. Our goal is to make all of you enjoy the top fashion and become the most charming women with our

    beautiful shoes.except that,Here are new arrivals of all kinds of Yves Saint Laurent and Jimmy Choo

    Shoes.will you have a good time in our YSL and Jimmy Choo Shoes On Sale.
    site: http://www.nicelouboutin.com.

  12. JashuaNet says:

    Hi, You can do moreover with PokeIn library under codeplex.

  13. Your article helped me so much on my works. Thanks.

  14. linkslondons says:

    Links of London Jewellery online sales,links of london friendship bracelet UK 50% OFF!Including Links of London Bracelets,Links of London Charm,Links oLinks of London Jewellery online sales,links of london friendship bracelet UK 50% OFF!Including Links of London Bracelets,Links of London Charm,Links o

  15. beddingpedia says:

    Thanks for the great write-up, Stephen!

  16. Thanks for sharing your stuff in coding Ajax application. I’m looking forward some donwloadable stuff in your next blog.

  17. wecol says:

    thank u for sharing, i archived

  18. Very Nice and detailed example. I also want that type of blog

  19. Itinerarist says:

    Very Nice and detailed example!

  20. 1 oz silver says:

    Its really annoying when clicking the browser forward and backwards buttons won’t move you back and forth in browser history.

  21. Don’t you know that it is high time to get the lowest-rate-loans.com, which will help you.

  22. cam says:

    Very well written post, thanks for going into the details and really explaining everything in easy to grasp terms.