ASP.NET MVC and AJAX Updating

In the work I’ve been doing to update Washington State University’s online Schedules of Classes, I wanted to support users who had access to HTML5 History, by letting them save time on page loads by only bringing back the part of the page where content was actually changing for a fair number of clicks that would happen on this page.

Now, the utility I use to accomplish this in JavaScript, YUI3’s History module would support, more or less transparently, using the Hash to let users who aren’t on HTML5 compatible browsers utilize this feature, but that would have broken my URL bar, adding complexity to the use case of copying and pasting the URI into an e-mail or whatever else to share with other users.

With MVC, I knew that I’d be able to support the exact same URIs for the pages that were requested via AJAX as those requested via a more normal usage pattern. However, I also need to be sure that, when requested via AJAX, I use a different master page for the serving.

On my pages, there are two Content Areas on the page of interest. Fist, MainContnt is the inside of a div with the id of ‘main’ on my Site.master. Second, ScriptContent is at the bottom of the page, so I can optionally add JavaScript content to be served on a page.

First, you’ll need to define you new Master page. I named mine AJAX.master, and it appears as follows:

<@ Master Language="C#" AutoEventWireup="false" %>
<asp:ContentPlaceHolder ID="MainContent" runat="server">
</asp:ContentPlaceHolder>
<asp:ContentPlaceHolder ID="ScriptContent" visibility="false" runat="server">
</asp:ContentPlaceHolder>

Visual Studio will complain about this not being valid, since it isn’t valid XHTML, but it’s a warning that, for this at least, you’ll just need to accept. Also, very important, is that visibilty="false" attribute on the “ScriptContent” placeholder. While declaring a ContentArea in a MasterPage does not require it to appear in a View, if you use it in your views, you will require it on the MasterPage, but with the visibility set to false, it won’t actually be rendered to the wire.

The JavaScript to make this request looks like this:

YUI().use('io-base', 'node', function(Y) {
    var show = function(node) {
        node.setStyle('display', 'block');  
    }, hide = function(node) {
        node.setStyle('display', 'none');
    },
    sessionTimestamp = (new Date()).getTime(),
    mainContent, loadingContainer, dataContainer;

    mainContent = Y.one('#main');
    dataContainer = Y.Node.create('<div></div>');
    loadingContainer = Y.Node.create('<div style="display: none;"><img src="/loading.gif" /></div>');

    mainContent.setContent("");
    mainContent.append(dataContainer);
    mainContent.append(loadingContainer);

    Y.io("/uri/to/request", {
        data: "ts=" + sessionTimestamp,
        on: {
            start: function (id) {
                show(loadingContainer);
                hide(dataContainer);
            },
            success: function (id, o) {
                dataContainer.setContent(o.responseText);
            },
            failure: function (id, o) {
                var error = dataContainer.one('p.error');
                if (!error) {
                    dataContainer.prepend("<p class='error'></p>");
                    error = dataContainer.one('p.error');
                }
                error.setContent("There was an error retrieving your search results. Our staff has been notified.");
            },
            end: function (id) {
                show(dataContainer);
                hide(loadingContainer);
            }
        }
    });
});

The above modified #main so that we can show a loading image, I generate mine onn ajaxload.info, while we load the new page. Now, in truth, this is wrapped in a delegate event handler attached to all the ‘a’ links on the page, and a check to make sure it’s one that should be handled in this way, but I’m trying to simplify as much as possible.

Okay, so we have our AJAX.master set up, and the JavaScript written, but that doesn’t make my Views work correctly. Luckily, there is a de facto standard in AJAX libraries these days that they all set the HTTP Header “X-Requested-With” to “XMLHttpRequest” when making AJAX calls. I know that YUI and jQuery both do this, I’m fairly certain that Prototype, MooTools and Dojo do as well.

By adding the following class, you can avoid needing to perform this check inside of each individual controller action method.

namespace foxxtrot.MVC.AJAX {
    using System.Web.Mvc;

    public class AJAXViewEngine : WebFormViewEngine {
        public override ViewEngineResult FindView(ControllerContext context,
                                                  string viewName,
                                                  string masterName,
                                                  bool useCache)
        {
            if (context.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest") {
                masterName = "AJAX";
            }

            return base.FindView(context, viewName, masterName, useCache);
        }
    }
}

It’s that simple. Now, anytime one of my Controllers gets called with an AJAX-y header, it will return back only the MainContent. To activate it, just add the following lines to your Application_Start method in your Global.asax.cs file.

ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new foxxtrot.MVC.AJAX.AJAXViewEngine());

Soon I’ll have time to finish speeding up my database layer, so you’ll be able to experience this technique on a live site.