PJAX with ASP.Net MVC and jQuery Tutorial

date_range  14 November 2012

ASP.Net
jQuery

UPDATED POST FOR PJAX plugin.

Got a nice helpful post for everyone today. Since I started using GIT hub, I loved the UX they had created for the tree browser. They have quite a useful article on how they created it. What I shall go into here is how I’ve implemented a similar effect using ASP.Net MVC and jQuery to create PJAX; Partial Javascript And XML. What this will do is get the page content using an ajax request but leave the layout elements, such as the navigation and footer meaning the browser doesn’t have to render an entire page or parse Javascript. What we all need, however, is SEO optimisation, and loading pages using Ajax is great, but we loose some crawability of our site and analytical tracking. Hence, we combine the new HTML 5 API and jQuery’s $.ajax function.

First off, we should think about how we are implementing our server side code. We’re using an incredibly powerful presentation framework, so lets utilise it as much as we can. What I’m specifically referring to is separating our views in such a way that only very specific parts of our view are displayed. For instance the _Layout.cshtml only holds the HTML \u0026lt;head\u0026gt; tag, the wrapper for the sticky footer and the footer which is displayed on every page. Single Responsibility Principle being followed even in views. My lecturers would be proud. Our _ViewStart.cshtml looks like this:

@{ 
	Layout = Request.Headers["X-PJAX"] != null ? "~/Views/Shared/_PjaxLayout.cshtml" : "~/Views/Shared/_Layout.cshtml"; 
}

If the current request comes back with X-PJAX in the header we send back a layout that only renders the body:

@RenderBody()

Of course, because we’ve designed our views well, they display only a small amount of information and aren’t dependent on the layout hierarchy, only rendering the body will be sufficient as is shown by the view for this very article.

@{ 
    Layout = Request.Headers["X-PJAX"] != null ? string.Empty : "~/Views/Shared/_ArticleLayout.cshtml"; 
    ViewBag.Title = "James Dibble - " + Model.Title; ViewBag.Description = Model.Abstract; 
} 
@model Website.Models.Entity.Blog 
<article> 
    <h2>@Model.Title</h2> 
    <p class="lead">@Model.Abstract</p> 
    <section> 
        @Html.Raw(File.ReadAllText(Server.MapPath(Model.ContentFile))) 
    </section> 
</article>
@section scripts 
{ 
    <script type="text/javascript" src="@Url.Content("~/Scripts/article.js")"></script> 
}

As you can see, we’re checking for the X-PJAX HTTP header again. As the layout hierarchy is built up by this view we need to override it if we’re trying to get a partial page, just incase our _ViewStart didn’t do the job. We don’t need to do anything special in the controller as it’s not bothered what type of request it got. It should only be deciding what view is to be dished out and converting primitive types into object models anyway.

Our client-side code is where all the magic happens. I have included History.js to normalize the history API accross the different implementations in different browsers. Here’s the Javascript:

$(function(){    
    var pjax = { active: false }; 
    var popped = ('state' in window.History), initialURL = location.href; 
    $('a[data-pjax="true"]').click(function () { 
        if (!window.History.enabled) return true; 
        var path = this.href;
        if (path != location.href) { 
            window.History.pushState({ pjax: true }, document.title, path); 
            pjax.active = true; 
        } 
        return false; 
    }); 
    window.History.replaceState({ pjax: false }, document.title, location.pathname + location.search); 
    window.History.Adapter.bind(window, 'statechange', function (event) { 
        var initialPop = !popped \u0026amp;\u0026amp; location.href == initialURL; 
        popped = true; 
        if (initialPop) return true; 
        var state = window.History.getState(); 
        if (state \u0026amp;\u0026amp; state.data.pjax) { 
            $.ajax({ url: location.pathname + location.search, beforeSend: function (xhrObj) { 
                    xhrObj.setRequestHeader("X-PJAX", "true"); 
                }, success: function (data) { 
                    $('#pjaxContainer').fadeOut(200).html(data).fadeIn(500); 
                }
            }); 
            return false; 
        } else { 
            window.location = location.pathname + location.search; 
        } 
    }); 
});

Let’s break this down.

  1. We set up an object to hold the state of our use of PJAX.
  2. We create a boolean to save whether we have popped a page from the history yet as well as the root page url to stop us loading the page using ajax on the page load popstate event. (More on this later)
  3. We set up an event handler for click events with links which include the data attributedata-pjax=”true”
    1. We check where the history api is available, if not we don’t bother to load the link using ajax, just load the page normally
    2. If the link refers to the current page, don’t bother to load it again.
    3. We then add the current link to the browser history using window.History.pushState(). The first parameter is quite important; its an anonymous object that saves the fact that the link should be loaded using ajax. The second parameter is totally irrelevant as it is not used by most browsers but you never know, so, we save the title and finally the url of the page we are pushing.
    4. We activate the PJAX so that any pushes are acted upon.
  4. On $(document).ready() we want to replace the current record for this initial page load so that when it is popped by the user going back, it is not loaded using ajax. We are using window.History.replaceState(). As the function name suggests it will over write the current history record instead of adding a new one.
  5. We are then setting up the event to handle pops from the history stack. One point to note is that some versions of Chrome raise this event when the full page is loaded, not just when a Javascript call is made. History.js has a slightly different signature for this event;window.History.Adapter.bind(window, ‘statechange’, function (event) {. It creates a subscription to thepopstate HTML 5 event but normalized. Here’s what we do with it:
    1. We check for the Chrome error and prevent it from loading the pjax content.
    2. We then get the current top state in the history stack and then check it’s not null and the if has saved the fact that the request should be completed using ajax. Otherwise, we’ve got got our first replaceState and we should get the entire page as we won’t have our layout saved.
    3. Next we’re actually going to get our page content!
      1. We give the jQuery $.ajax function a parameter to send back our X-PJAX header. This is done in the beforeSend option by overriding it with an anonymous function.
      2. We’ll update our container with the new content in the success handler. I’ve added in some fading so it looks cool.

PHEW! I’ve made some pretty bold decisions there. Some of this is based from the pjax.js library but one key difference is that content is always loaded in the popstate handler. I’ve done this for maintainability purposes as well as continuity. If the content is always being loaded on popstate (or statechange as it should be called) we know our HTML 5 history stack change has been successful. If anyone knows of a reason I shouldn’t be doing this please let me know.

Well there we are. Hope this was helpful and if you have any questions please do contact me.

comments powered by Disqus

chevron_left Archive