MVC Service Based Web Applications - Part II - Script Level Caching

Last week we set up a very basic web service API using ASP.Net MVC and demonstrated some simple HTML templating techniques. Today we'll be concentrating on the client side of things by implementing HTML views and script level caching.

What is Script Level Caching?

Simply put, script level caching is a method for storing static web page resources using javascript. While this certainly isn't a replacement for typical caching strategies it gives you, the developer, an added layer of cache control which can save you some headaches and your server some CPU cycles. For this tutorial we'll be using jQuery's data() method to store information but this approach could be used with any javascript framework.

Getting Started

The first thing we'll need to do is create another javascript file so we can define some internal behavior.

/Scripts/Internal.js


function __GetAppStateVariableName() {
    return "__state";
}
function __URLCache(url, val) {
    var urlCache = AppState().urlCache;
    if (!val) {
        var ret;
        if (!urlCache) return false;
        for (var index = 0; index < urlCache.count; index++) {
            if (urlCache.urls[index] == url) {
                ret = urlCache.cache[index];
                break;
            }
        }

        if (ret) 
            AppState().pushMessage("Cache for URL '" + url + "' found.");
        else
            AppState().pushMessage("No cache for URL '" + url + "' found.");

        return ret;
    } else {
        var index = -1;
        $.each(urlCache.urls, function (_index, item) { if (item == url) { index = _index; return false; } });
        if (index == -1) index = urlCache.count;

        urlCache.count++;
        urlCache.urls[index] = url;
        urlCache.cache[index] = val;
    }
}

The first function is simply an easy way to set the string value we'll be using to store all our app state information. I've chosen the string "__state" but you can use whatever you like. __URLCache allows us to get and set keys (which are simply the URLs requested) in the cache store. Next we'll need to make some edits to our existing Util.js file.

/Scripts/Util.js


function AppState() {
    var dataName = __GetAppStateVariableName();
    var state = $.data(document, dataName);
    if(!state) {
        var state = {
            pushMessage: function(msg) { this.messages.push(msg); },
            messages: [],
            urlCache: { count: 0, urls: [], cache: [] }
        };
        $.data(document, dataName, state);
    }
    return state;
}
function AJAXLoadHTML(url, data, successCallBack) {
    var cache = __URLCache(url);
    if (!cache) {
        $.ajax({
            type: "GET",
            data: data,
            url: url,
            cache: false,
            contentType: "application/json; charset=utf-8",
            dataType: "html",
            success: function (msg) {
                __URLCache(url, msg);
                if (successCallBack) successCallBack(msg);
            },
            error: function (msg) {
                AppState().pushMessage("AJAXLoadHTML failed for url: " + url);
            }
        });
    } else {
        if (successCallBack) successCallBack(cache);
    }
}

This function will be used to access all client side application state information. Later on in the project we'll add some more functionality here but for now it's purpose is to store all the cached HTML files and their URLs. There's also some functionality for storing debug messages. AJAXLoadHTML is nearly an identical copy to our existing AJAXLoadData function but we'll be using this method strictly for downloading HTML files. As you can see, it first checks if the __URLCache method returns anything for a particular URL. If not it falls back to the AJAX request. The page will be stored until the user refreshes the page or closes the browser. Let's also change AJAXLoadData to take advantage of our debug message system.

function AJAXLoadData(url, data, successCallBack) {
    $.ajax({
        type: "POST",
        data: data,
        url: url,
        contentType: "application/json; charset=utf-8",
        dataType: "json",
        success: function (msg) {
            if (successCallBack) successCallBack(msg);
        },
        error: function (msg) {
            AppState().pushMessage("AJAXLoadData failed for url: " + url);
        }
    });
}

In order to test this out the script level caching we'll have to create an html view and make some alterations to our single MVC view.

/Content/ui/home/landing.htm


<script language="javascript" type="text/javascript">
    $(document).ready(function () {
        AJAXLoadData("/Method/GetSpoofedInfo"nullfunction (msg) {
            $(".obj_lstNames li").not(".obj_static").remove();

            var template = $(".obj_lstNames .obj_template");
            $.each(msg.Data.Names, function (index, item) {

                var ListItem = template.clone();
                ListItem.text(item.Name);
                ListItem.insertBefore(template);
                ListItem.removeClass("obj_template").removeClass("obj_static").show();
            });
        });
    })
</script>

<h1>Welcome!</h1>

<ul class="obj_lstNames">
    <li class="obj_template obj_static" style="display: none;"></li>
    <li>Loading...</li>
</ul>    

You may notice that this is the exact code that originally comprised the body of Index.cshtml in the last phase of the tutorial. So what does that file look like now?

/Views/Home/Index.cshtml


<!DOCTYPE html>

<html>
<head>
    <script src="/Scripts/jquery-1.5.1.min.js" language="javascript" type="text/javascript"></script>
    <script src="/Scripts/jquery-ui-1.8.11.min.js" language="javascript" type="text/javascript"></script>
    <script src="/Scripts/Internal.js" language="javascript" type="text/javascript"></script>
    <script src="/Scripts/Util.js" language="javascript" type="text/javascript"></script>
</head>
<body>
    <script language="javascript" type="text/javascript">
        $(document).ready(function () {
            $(".obj_btnShowCacheInfo").click(function () {
                alert("There are currently " + AppState().urlCache.count + " url cache entries stored.\n"
                    + "AppState().messages: \n"
                    + "--------------------- \n"
                    + AppState().messages.join().replace(/,/g, "\n")
                );
            });
            $(".obj_btnLoadWorkspace").click(function () {
                AJAXLoadHTML("/Content/ui/home/landing.htm"nullfunction (msg) {
                    $("#workspace").html(msg);
                });
            });
        });
    </script>

    <input type="button" class="obj_btnLoadWorkspace" value="Load Workspace" />
    <input type="button" class="obj_btnShowCacheInfo" value="Show Cache Info" />

    <div id="workspace"></div>


</body>
</html>

Notice that a script reference has been added for Internal.js. Run the project and using the buttons you can observe how the caching method is performing.

Phase II Links: Download | Demo

Conclusion

So at this point we've established a basic method for grabbing static HTML views which in turn are able to retrieve information and populate the templates. Still, we're a ways off from having a fully functional application. Due to the fact that the browser never changes locations the back and forward buttons which users have grown accustomed to are non-functional and while calling AJAXLoadHTML to load different views certainly works it's not a very seamless approach. In the next phase we'll implement anchor based navigation to resolve both these issues. In addition we'll begin making our way back into server side code as we implement ClientException handling.

Quick Links: << Previous: Introduction | Next: Anchor Navigation and Exception Handling >>