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.
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.
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.
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.
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.
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
Comments
Cameron 2 years 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!
Atif Aslam 2 years ago
Can you please provide a Demo Link? We see it all day on Twitter but like to see your AJAX version...
Joel Abrahamsson 2 years ago
Hi Atif!
Its on the front page of this site.
Webdiyer 2 years 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 .
michiel 1 years 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
Joel Abrahamsson 1 years 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/.
Michiel 1 years ago
After a good nite of 8 hours sleep, I was able to get it to work, thanks!
sokhanh03 1 years ago
u can upload your solution, plz?
Joel Abrahamsson 1 years ago
You can grab the entire solution here: http://joelabrahamsson.codeplex.com/releases/view/34699
DemoGeek 1 years 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.
Joel Abrahamsson 1 years 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.
DemoGeek 1 years 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.
Ash Barati 1 years ago
Oh Man, I am loving it
Thanks for sharing with us.
Pandiya Chendur 1 years ago
Absolutely wonderful article.. Just loved it...
kad1r, asp.net, c# 1 years ago
It works. Thank you.
james@bbs.com 1 years ago
nice work my man
Bruno 1 years ago
This is awesome thank you!!!
Albert Atienza 8 months ago
Hi joel,
Before anything else i would like to say that this is a nice post. I have a question though. Im using table (tr,td) instead of order list, how can i implement the javascript part? im having a hard time getting it. Hope you can help me with this. Thanks in advance. I will really appreciate your immediate response. Thanks.