Click here to Skip to main content
15,893,564 members
Articles / Web Development / ASP.NET
Tip/Trick

AHAH in ASP.NET MVC using ActionFilters, XMLHttpRequest and HistoryAPI

Rate me:
Please Sign up or sign in to vote.
4.71/5 (8 votes)
13 Mar 2014CPOL4 min read 14.9K   15  
Simple example outlining how to optionally employ AHAH based views in an MVC application.

Introduction

Async views in webpages are becoming more common with JavaScript heavy apps and even more so with single page apps. Post-backs are starting to feel pretty old-school.

The myriad frameworks (Durandal, Angular, Ember, etc.) and techniques being used in these applications generally do a great job, often by injecting JSON data from AJAX calls into view templates and provide a very snappy user experience.

But What If SEO is Important?

Sure, you can take advantage of AJAX enabled web crawlers ala 'escaped-fragment', but then you need to have two rendering pipelines and server code to recognize the special Ajax-enabled crawlers so that it can send the server rendered code to them.

- Why would you want to write twice as much code?

Another great length people have gone to satisfy the shortcomings of search crawlers is to setup a 'Snapshot Server'. This may be an acceptable method if you care about not having to write a bunch of special rendering code but still don't mind writing your server logic to be aware of the escaped-fragment mechanism. You will also be introducing another layer of maintenance to keep your page crawling list in sync with your site as pages are added. Oh and you'll also need plenty of additional server resources to setup a dedicated, JavaScript enabled crawler just for your site so that it can write the results to HTML files that then get served to the crawlers...

There is Another Solution

Having worked on projects employing both of these techniques as well as many others and variations, I decided to take a step back. It seems that the simplest solutions are often the best, and a solution that can dynamically swap out only the portions of a page that are actually different using 100% server rendering that also falls back to normal web page behavior (complete loads) for older browsers and standard search crawlers is a big win for my projects.

Background

AHAH (Async HTML and HTTP)

Seems so simple, right? (And it is!)

Using the Code

Example code is available on Github.

To enable AHAH in your MVC application, there are two components you need to setup:

  1. An ActionFilter to modify the View's Layout
  2. A small JavaScript to wire things up on the page

Server Side

C#
//
// Apply the attribute to all Actions you want to enable AHAH with
// 
[AhahLayout]
public ActionResult Index()
{
    return View();
} 

All the Attribute does is check to see if the browser is requesting AHAH via a header parameter, and either setting the MasterName(Layout) to a view specified in the web.config or null if not specified:

C#
public override void OnResultExecuting(ResultExecutingContext filtercontext)
{
    if (filtercontext != null && 
        filtercontext.HttpContext != null &&
        filtercontext.HttpContext.Request != null && 
        filtercontext.HttpContext.Request.Headers!= null)
     {
        var trigger = filtercontext.HttpContext.Request.Headers[AhahTrigger];
        if (!String.IsNullOrWhiteSpace(trigger))
        {
            var viewresult = filtercontext.Result as ViewResult;
            if (viewresult != null)
            {
                viewresult.MasterName = AhahLayout;
            }
        }
   }
} 

On the Client

JavaScript
(function () {
    /*Used to track links that have already been processed*/
    var _activeLinks = [];
    /*Default link class to apply AHAH workflow*/
    var _ahahLinkClass = 'ahah';
    /*Optional container attribute to determine where link href's content is injected to*/
    var _ahahContainerDataAttrib = 'data-container';
    /*Default Id for injected content*/
    var _ahahDefaultContainer = 'content';

    function supportsHistoryAPI() {
        return !!(window.history && history.pushState);
    }

    function updateContent(state) {
        /*Not worrying about ActiveX XMLHttpRequest since all IE browsers 
        that support history API (IE>8) support the standard 'XMLHttpRequest'*/
        var req = new XMLHttpRequest();

        req.open('GET', state.href, false);
        req.setRequestHeader("AHAH", "true");
        req.send(null);
        if (req.status == 200) {
            var container = document.getElementById(state.container);
            if (container) {
                container.innerHTML = req.responseText;
                /*Check new content for links that may support the AHAH workflow*/
                initAhahLinks();
                return true;
            }
        }
        return false;
    }

    function addLinkClickHandler(link) {
        /*Not worrying about older IE event hooking calls since all IE browsers
        that support history API (IE>8) support the standard 'addEventListener'*/
        link.addEventListener("click", function (e) {
            var state = {
                href: link.href,
                /*Try to use a defined container if specified, otherwise use the default*/
                container: link.getAttribute(_ahahContainerDataAttrib) || _ahahDefaultContainer 
            };

            if (updateContent(state)) {
                /*Content was updated so update the history API and prevent normal navigation*/
                history.pushState(state, null, state.href);
                e.preventDefault();
            }
        }, true);
    }

    function initAhahLinks() {
        /* Find all links with the desired class*/
        var links = document.getElementsByClassName(_ahahLinkClass);
        if (links) {

            for (var i = 0; i < links.length; i++) {
                /*If we have not already processed this link,
                then add a click handler and track it to prevent subsequent processing*/
                if (_activeLinks.indexOf(links[i]) < 0) {
                   
                    addLinkClickHandler(links[i]);
                    _activeLinks.push(links[i]);
                }
            }
        }
    }

    window.onload = function () {
        /*Bail if we are in an old browser*/
        if (!supportsHistoryAPI()) {
            return;
        }
        initAhahLinks();
        window.setTimeout(function () {
            window.addEventListener("popstate", function (e) {
               
                if (e.state) {
                    /*We came from an AHAH navigation, so revert using AHAH*/
                    updateContent(e.state);
                } else {
                    /*State has not been set previously so load current location*/
                    var state = {
                        href: location.href,
                        container: _ahahDefaultContainer
                    };
                    updateContent(state);
                }
            }, false);
        }, 1);
    }

})();
And that's it!

The end effect is that requests to the link's hrefs are responded to with full renderings wrapped in the layout pages as normal. However, if the HTTP header is present, then the exact same action method is post processed to remove the layout reference and only the called view's content is sent down the wire.

This ensures that older browsers can use your app/site in the same standard way they always have without the need to shim and hack new web standard's functionality into them. Perhaps more importantly, search engine crawlers can also index your app/site in the traditional manner.

Points of Interest

There are some obvious economies to be realized by reducing the payload between pages to only what needs to change. However, the largest savings are gained by removing the need to download, parse and execute the many JavaScript libraries and CSS files every time a user navigates to a "page".

Caveat

If you are using JavaScript plugins (photo galleries, data-bound widgets, etc.), you will need to design the sections you want to apply an AHAH workflow to in such a way that they will be able to reinitialize themselves after loading. In other words, you will need to reapply the plugins to the newly added DOM fragment.

I believe this caveat promotes modular design, and maybe that is not such a bad thing after all.

Performance

Full Page Load Transfer Time= 844ms transfer time

Image 1

AHAH Page Load Transfer Time= 145ms transfer time

Image 2

The payload savings alone come to around 700ms, not to mention the significant benefits achieved from avoiding multiple requests and loading of all your many framework level CSS and JavaScript files.

History

  • 3/11/2014- Initial commit
  • 3/13/2014- Moved the trigger logic to use headers instead of query string

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Sitewire
United States United States
Software Engineer, Software Architect and Systems Integrator. Specializing in creating robust systems for demanding applications, I equally enjoy laying out architecture as well as cranking out code in the trenches.

Comments and Discussions

 
-- There are no messages in this forum --