Twitter style paging with ASP.NET MVC and jQuery

paging-buttons I really like the simplicity of the AJAX paging at Twitter so I decided to use the same type of paging, including a similar more button, on the start page on this site. It actually surprised me how simple it was to build it, including fallback for visitors that doesn’t have javascript enabled (such as our dear friend Google), with ASP.NET MVC and a few lines of javascript with jQuery.

Basic controller logic

Being an advocate of progressive enhancement I started out by building a non-AJAX version of the feature. In the controller for the start page I let the default (Index) method have a nullable int parameter named entryCount which tells the method how many of the latest blog entries it should return for the view to display.

public class HomeController : Controller
{
    private const int defaultEntryCount = 10;

    public ActionResult Index(int? entryCount)
    {
        if (!entryCount.HasValue)
            entryCount = defaultEntryCount;
	
        //Retrieve the first page with a page size of entryCount 
        int totalItems;
        IEnumerable<Entry> entries = GetLatestEntries(1, entryCount.Value, out totalItems);
        
        if (entryCount < totalItems)
            AddMoreUrlToViewData(entryCount.Value);

        return View(entries);
    }

    private void AddMoreUrlToViewData(int entryCount)
    {
        ViewData["moreUrl"] = Url.Action("Index", "Home", new { entryCount = entryCount + defaultEntryCount });
    }
}

The method begins by making sure that the entryCount variable has a value, setting it to a default value if the parameter is null. It then retrieves as many of the latest blog entries as entryCount specifies by calling the GetLatestEntries method. I’ve omitted the GetLatestEntries method as it’s implementation will vary depending on blogging platform. The GetLatestEntries method also has an out parameter, totalItems, which tells us the total number of blog entries. I’m not a big fan of using out parameters but that’s the way the framework that I used for my blog (EPiServer Community) works so I decided to follow that pattern for consistency. If you use some other type of blogging platform I would recommend making a field of the totalItems.

The method moves on to check if there are more blog entries than the  number that will be displayed, in other words if a link for showing more entries should be displayed. If so, it calls the AddMoreUrlToViewData which, you guessed it, adds a route URL for displaying more entries to the ViewData dictionary.

Finally the method returns a ViewResult with the list of blog entries as the model.

Creating the views

The Index view for the Home controller is very simple. It simply renders a partial view named EntryTeaserList, passing along the list of blog entries (Model) and the it’s ViewData dictionary.

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<IEnumerable<Entry>>" %>
<asp:Content ContentPlaceHolderID="PrimaryMainContent" runat="server">
    <% Html.RenderPartial("EntryTeaserList", Model, ViewData); %>
</asp:Content>

The partial view EntryTeaserList offers a bit more excitement. It renders an ordered list and displays a teaser for each blog entry by rendering another partial view, EntryTeaser, inside a list item, passing in each individual blog entry as model to it. It also checks if the ViewData dictionary contains a URL for a more link. If it does it renders a link with that URL in the href attribute.

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IEnumerable<Entry>>" %>
<div id="entryTeaserList">
    <ol>
        <% foreach (Entry item in Model) { %>
            <li class="entryTeaser">
                <% Html.RenderPartial("EntryTeaser", item); %>
            </li>
        <% } %>
    </ol>
    <% if(ViewData["moreUrl"] != null) { %>
        <a href='<%= ViewData["moreUrl"] %>' id="moreLink">More</a>
    <% } %>
</div>

Finally I made the link look like a button with some CSS.

#moreLink { 
-moz-border-radius: 6px; 
-webkit-border-radius: 6px; 
border: 1px solid #666666; 
background:url('/styles/gfx/more-bg.gif') repeat-x; 
width: 100%; 
display: block; 
text-align: center; 
padding: 0.4em 0 0.4em 0; 
font-weight: bold; 
color:#9aa57c;  
}
#moreLink:hover { 
background:url('/styles/gfx/more-bg.gif') 0 -64px; repeat-x; 
border: 1px solid #888888;
text-decoration: none; 
}

As you might have noticed I used CSS to round the buttons corners. This will only work for some browsers. In this particular case I though that that was OK, but in many other situations I would instead have used images or javascript. You might also have noticed that the button has the same background image when it’s hovered over as when it isn’t. The background image is however offset vertically so it appears that it’s actually another image. I did this to keep the number of HTTP requests required to load the page to a minimum.

Spicing things up with AJAX

With the controller and views set up as described above I was done with the non-AJAX functionality. This will work fine for visitors that doesn’t have javascript enabled or debugging purposes, but this kind of paging is pretty pointless if the page has to reload. After all the point is that when someone clicks the more button the experience shouldn’t be that another page is displayed but that the list, more or less instantly, just grows a bit.

To add the AJAX functionality I begun by modifying the controllers Index method.

public ActionResult Index(int? entryCount)
{
    if (!entryCount.HasValue)
        entryCount = defaultEntryCount;

    int totalItems;

    if(Request.IsAjaxRequest())
    {
        int page = entryCount.Value / defaultEntryCount;

        //Retrieve the page specified by the page variable with a page size o defaultEntryCount
        IEnumerable<Entry> pagedEntries = GetLatestEntries(page, defaultEntryCount, out totalItems);
        
        if(entryCount < totalItems)
            AddMoreUrlToViewData(entryCount.Value);
        
        return View("EntryTeaserList", pagedEntries);
    }

    //Retrieve the first page with a page size of entryCount
    IEnumerable<Entry> entries = GetLatestEntries(1, entryCount.Value, out totalItems);
    
    if (entryCount < totalItems)
        AddMoreUrlToViewData(entryCount.Value);

    return View(entries);
}

The added code checks if the current request is an AJAX request, with the IsAjaxRequest extension method that ships with MVC. IsAjaxRequest determines if the current request is an AJAX request by looking for and at the X-Requested-With request header. If such an header, or actually any request parameter with that name, is set to “XMLHttpRequest” the method will return true. As jQuery’s AJAX methods sets that header this method works great in this example.

Anyway, if the current request is an AJAX request we know that the visitors browser already displays a number of blog entry teasers and instead of returning the full number of entries specified by the entryCount parameter we should only return those that haven’t yet been sent to the visitors browser. So, we calculate what page (as if we where using traditional paging) is requested by dividing entryCount with the defaultEntryCount constant. Then we retrieve a list of the entries on that page with a page size of defaultEntryCount. That is we retrieve the defaultEntryCount number of entries with an offset of page*defaultEntryCount.

Finally, if there are more entries we set the moreUrl in the ViewData dictionary by calling the AddMoreUrlToViewData method and return a ViewResult. This time around however we don’t return the default view for the method. Instead we return the EntryTeaserList partial view. This way we don’t return more HTML than necessary but we are able to reuse an already existing view. We could of course have returned the result as JSON or XML instead but that would have forced us to write javascript for rendering the markup to display the result and thereby duplicating the same markup in two places.

The last thing I did was to add a few lines of javascript to intercept clicks on the more link.

$(function() {
    addMoreLinkBehaviour();
});

function addMoreLinkBehaviour() {
    $('#entryTeaserList #moreLink').live("click", function() {
        $(this).html("<img src='/images/ajax-loader.gif' />");
        $.get($(this).attr("href"), function(response) {
            $('#entryTeaserList ol').append($("ol", response).html());
            $('#entryTeaserList #moreLink').replaceWith($("#moreLink", response));
        });
        return false;
    });
}

When the DOM is ready we add a function to the click event of the more link, and, since I’m using the live function, to any future objects matching that selector. When the link is clicked two things initially happen. First the link’s text is replaced with an image to give the visitor some visual feedback if the response of the AJAX request isn’t instantly returned. Then an AJAX request is made to the same URL as the link had in it’s href attribute. That is, there’s no special URL for the AJAX request. This works as the controller takes care of determining what type of request it is.

When the server has responded with the partial view, that is an ordered list and possibly a new more link the list items are appended to the existing ordered list and the more link is replaced with the new more link if it exists. This way the more link is automatically updated with a new URL in it’s href attribute and the loading image is replaced with the original text.

Conclusion

I personally find this solution pretty elegant. It requires quite few lines of code and almost no duplicate logic or markup at all. It also offers full fallback functionality for visitors without javascript. However, if I was really interested in offering the best possible experience to human visitors with javascript disabled I could also give each blog entry teaser an id with it’s number in the list and include a hash tag with entryCount + 1 - defaultEntryCount in the more link’s target URL so that they would automatically be scrolled to the first entry that was added to the list. In my case I deemed that to be overkill though.

PS. For updates about new posts, sites I find useful and the occasional rant you can follow me on Twitter. You are also most welcome to subscribe to the RSS-feed.

Comments

  1. Cameron's avatar

    Cameron 10 months ago

    Quickly browsed this one and the xml site map. Spot on for my needs and I am sure others too. Have to run now but will be back to test out and try and provide some feedback in the next couple of days. Great work. Stumble thumbs up!

  2. Atif Aslam's avatar

    Atif Aslam 10 months ago

    Can you please provide a Demo Link? We see it all day on Twitter but like to see your AJAX version...

  3. Joel Abrahamsson's avatar

    Joel Abrahamsson 10 months ago

    Hi Atif!

    Its on the front page of this site.

  4. Webdiyer's avatar

    Webdiyer 6 months ago

    I wrote a free asp.net mvc paging component called MvcPager,it support both basic url paging and Ajax paging using jQuery or MicrosoftAjax script library,you can view online demo and download a copy from http://en.webdiyer.com .

  5. michiel's avatar

    michiel 5 months ago

    For love or money I just can't get this to work correctly. I have modifed the code ever so slightly and the code works when not user the if(Request.IsAjaxRequest()) {}
    ie. javascript diabled, however when I try it with javascript enabled the result skip and are not in order

  6. Joel Abrahamsson's avatar

    Joel Abrahamsson 5 months ago

    Michiel, I'm sorry to hear that!
    It's hard to say what could be wrong without looking at your code though. Perhaps you could look at my implementation? The source code for this site is available at http://joelabrahamsson.codeplex.com/.

  7. Michiel's avatar

    Michiel 5 months ago

    After a good nite of 8 hours sleep, I was able to get it to work, thanks!

  8. sokhanh03's avatar

    sokhanh03 3 months ago

    u can upload your solution, plz?

  9. Joel Abrahamsson's avatar

    Joel Abrahamsson 3 months ago

    You can grab the entire solution here: http://joelabrahamsson.codeplex.com/releases/view/34699

  10. DemoGeek's avatar

    DemoGeek 27 days ago

    Joel - that's a nifty solution...everything worked great except one minor issue. When I hit the "More" button it gets the new set of data but scrolls all the way up to the top as if Ajax is not in effect. Can you please hint me on the right direction?

    Thanks in advance.

  11. Joel Abrahamsson's avatar

    Joel Abrahamsson 27 days ago

    Hmm, I've not run in to that problem myself under the circumstance that the page didn't actually do a full reload. Are you sure that the page didn't actually reload? If you want you can send me your code (mail@joelabrahamsson.com) and I'd be happy to take a look at it.

  12. DemoGeek's avatar

    DemoGeek 27 days ago

    Joel - it certainly looks like my AJAX is not working and I'm looking into that. Will keep you posted as I figure out. Thanks for your response.

  13. Ash Barati's avatar

    Ash Barati 11 days ago

    Oh Man, I am loving it
    Thanks for sharing with us.

  14. Pandiya Chendur's avatar

    Pandiya Chendur 3 days ago

    Absolutely wonderful article.. Just loved it...

  15. kad1r, asp.net, c#'s avatar

    kad1r, asp.net, c# 2 days ago

    It works. Thank you.

Add a comment

Allowed tags: <b>, <em>, <quote cite="">, <code>, <c-sharp-code>, <css-code>, <sql-code>, <xml-code>, <javascript-code>. If you want to display code examples, please remember to write &lt; for < and &gt; for >.

Follow me on Twitter

  1. Blogged: Learning Scala part eight – Scala’s type hierarchy and object equality http://bit.ly/doyszt 1 days ago
  2. @andreakn in markup or code behind? For the latter you can use the getpropertyname extension method 1 days ago
  3. @andreakn No, but there are plans to support the "built-in" props for subscriptions etc somehow 2 days ago
follow me

Latest comments

  1. Svante wrote "Hi, nice catch! However, as you state, you took the easy ..." on Something to beware of when using EPiAbstractions and an IoC container
  2. kad1r, asp.net, c# wrote "It works. Thank you." on Twitter style paging with ASP.NET MVC and jQuery
  3. Pandiya Chendur wrote "Absolutely wonderful article.. Just loved it..." on Twitter style paging with ASP.NET MVC and jQuery

About this site

This blog is built with EPiServer Community, EPiServer CMS, ASP.NET MVC and a bunch of other great products. The source code is available for download at the projects page, where you also can read more about this site and my other projects.

read more