MVC Service Based Web Applications - Part III - Anchor Navigation and Exception Handling

In the last phase we demonstrated a simple, yet cumbersome method for loading static HTML views. For this phase of the project we will be looking at a less explicit method for view loading using anchors which will support the back and forward history navigation in the user's browser and in addition will allow bookmarks to function normally.

The first hurdle in this process will be detecting when there has been an anchor change. While some browsers have an event that can be hooked into legacy support will require the script use a polling method as a fallback. Writing a client side script to do this certainly isn't an insurmountable task but personally I use a script written by Ben Alman which he calls jquery-hashchange-plugin. Let's download this script and add the reference to our primary ASP.Net MVC view. We'll also remove the manual view loading button.

/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/jquery.ba-hashchange.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>
    <script language="javascript" type="text/javascript">
        $(document).ready(function () { __Init(); });
    </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")
                );
            });
        });
    </script>

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

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

You will also notice that we have added a call to the aptly named __init() function which we will use to handle any necessary initialization calls. We will need to add this function along with some supporting functions to our existing script.

/Scripts/Internal.js


function __Init() {
    $(window).hashchange(function () {
        AppState().hashInfo = __GetHashInfo();
        AppState().data = AppState().hashInfo.data;
        __Route();
    });
    $(window).hashchange();
}
function __GetHashInfo() {
    var info = {};

    if (window.location.hash && window.location.hash.length > 1) {
        info.hash = window.location.hash.substr(1, window.location.hash.length - 1);
        info.parts = info.hash.split("/");
        if (info.hash.indexOf("?") > -1) {
            info.nav = info.hash.substr(0, info.hash.indexOf("?"));
            if (info.hash.indexOf("?") + 1 < info.hash.length) {
                var datastr = info.hash.substr(info.hash.indexOf("?") + 1, info.hash.length - (info.hash.indexOf("?") + 1));
                var dataparts = datastr.split("&");
                info.data = { count: 0, vars: {} };
                $.each(dataparts, function (index, item) { info.data.count++; info.data.vars[item.split("=")[0]] = item.split("=")[1]; });
            }
        } else {
            info.nav = info.hash;
        }
    }
    return info;
}
function __Route() {
    var base = "/Content/ui";
    var resource = base;

    if (!AppState().hashInfo.nav) // default
    {
        AJAXLoadHTML(base + "/home/landing.htm", null, function (msg) { $("#workspace").html(msg); });
        return;
    }

    if (AppState().hashInfo.parts.length == 1)
        resource += "/home/" + AppState().hashInfo.parts[0];
    else {
        $.each(AppState().hashInfo.parts, function (index, item) {
            resource += "/" + item;
        });
    }

    AJAXLoadHTML(resource + ".htm", null, function (msg) { $("#workspace").html(msg); });
}

As you can see, the __Init() method first defines the behavior whenever the anchor (aka hash) is changed. __GetHashInfo() is responsible for cutting up the anchor value into pieces that are easier to work with. It even does some processing for psuedo querystring values if that's the approach you want to take but ultimately the __Route() method allows you to process this information however you like. For now we're defaulting to our landing.htm in the event that there is no hash information and we are defaulting to the /Content/ui/home/ directory if there is only one piece of information. Later on in the project we'll alter this method to add support for HTML master pages. Let's add a new view so we can see this in action.

/Content/ui/home/test1.htm


This is the test1.htm view file.

<a href="#">Load Landing</a><br />

Simple enough. Let's also add a line to our existing view.

/Content/ui/home/landing.htm


<a href="#test1">Load Test1</a><br />

Run the project and test it out. You can also use the button we created in the previous to tutorial so you can see how the script level caching handles both views.

Getting Back to the Server Side of Things

We've been concentrating a lot on the client lately so let's start to switch gears back to the server. In the last post I promised we'd take a look at exception handling. While a good exception handling strategy is difficult enough in a typical web application an AJAX service based application compounds the problem due to the fact that we need to be aware of what kind of response the client is expecting. Here are just a few things we'll be keeping in mind.

  • We need to differentiate between exceptions that are the users fault and exceptions caused by a bug or server malfunction. We have already created the classes ClientException and ServerException to make this distinction.
  • Our server should be aware of the type of response expected. Returning a JSON result for an HTML request can leave a very poor impression on your users.
  • The application needs to be able to display client exceptions in a variety of ways. For example form based exceptions we'll want to show the message near the field while other exceptions would best be shown in a dialog box.
  • Multilingual support is an important consideration in any application. All unique messages delivered to the client should be given a GUID so we can create a message table for each language.
  • Since both client and server exceptions can occur almost anywhere in code exception bubbling should be leveraged as much as possible.

Since we're trying to come up with a global exception handling strategy we'll start in the most obvious place.

/Global.asax.cs


protected void Application_Error()
{
    Exception ex = Server.GetLastError();

    if (ex is Classes.Exceptions.ClientException)
    {
        OutputMessage(new Classes.JSONMessageObject() 
        {
            ClientExceptions = { (Classes.Exceptions.ClientException)ex }
        });
    }
    else 
    {
        WriteToFile(ex, (Exception ex1) =>
        {
            OutputMessage(new Classes.JSONMessageObject()
            {
                ServerExceptions = { new Classes.Exceptions.ServerException("Failed to log error.") }
            });
        });
    }

    Server.ClearError();
}

public void WriteToFile(Exception ex) { WriteToFile(ex, null); }
public void WriteToFile(Exception ex, Action<Exception> failed)
{
    string Path = Server.MapPath("/Content/logs/");
    Guid LogId = Guid.NewGuid();
    while(System.IO.File.Exists(Path + LogId.ToString() + ".txt")) LogId = Guid.NewGuid();

    try
    {
        System.IO.File.AppendAllText(Path + LogId.ToString() + ".txt"FormatException(ex, Request));
        OutputMessage(new Classes.JSONMessageObject()
        {
            ServerExceptions = { new Classes.Exceptions.ServerException("Error saved to file log. Id: " + LogId.ToString()) }
        });
    }
    catch
    {
        if (failed != nullfailed(ex);
    }
}

public static string FormatException(Exception ex, System.Web.HttpRequest request)
{
    string InnerException = "null";
    if (ex.InnerException != null) InnerException = FormatException(ex.InnerException, request);

    return String.Format("\r\n\r\nApplication Error\r\n\r\n" +
                                         "MESSAGE: {0}\r\n" +
                                         "SOURCE: {1}\r\n" +
                                         "FORM: {2}\r\n" +
                                         "QUERYSTRING: {3}\r\n" +
                                         "TARGETSITE: {4}\r\n" +
                                         "STACKTRACE: {5}\r\n" +
                                         "TIME: {6}\r\n" +
                                         "INNER EXCEPTION BEGIN ***\r\n{7}\r\nINNER EXCEPTION END ***\r\n",
                                         ex.Message,
                                         ex.Source,
                                         request.Form.ToString(),
                                         request.QueryString.ToString(),
                                         ex.TargetSite,
                                         ex.StackTrace,
                                         DateTime.Now.ToString(),
                                         InnerException);
}

While this is a pretty large block of code it's purpose is fairly simple. If the unhandled exception is a ClientException create a new JSONMessageObject and populate it's ClientExceptions property with the exception. Any other kind of exception we will assume the user can't do anything about and may contain sensitive information so we'll log that information to a file and create a JSONMessageObject with a ServerException element. As you can see logged messages will be placed in the /Content/logs/ folder which you will need to create. In order to prevent anonymous users from accessing this information you will need to remove access for the anonymous IIS user in the Windows Permissions or IIS itself. Ideally log information should be stored in the database but since we haven't created a database layer at this point in the project a file log will do just fine. This approach satisfies the first condition of our exception handling strategy. In order to satisfy the second condition we'll need to add some code that intelligently displays this information based on the content type requested by the client. This can be done using the following function.

private void OutputMessage(Classes.JSONMessageObject data)
{
    const string JSONP_CALLBACK_PARAMETER = "callback";

    data.LoggedIn = false;
    data.IsAdmin = false;
    data.Success = false;

    Response.StatusCode = 200;

    if (Request.ContentType.ToLower().Contains("application/json"))
    {
        Response.ContentType = "application/json; charset=utf-8";
        Response.Write(data.toJSON());
    }
    else if (Request.ContentType.ToLower().Contains("application/jsonp"))
    {
        Response.ContentType = "application/javascript; charset=utf-8";
        Response.Write(Request.QueryString[JSONP_CALLBACK_PARAMETER].ToString() + "(" + data.toJSON() + ")");
    }
    else
    {
        Response.ContentType = "text/html; charset=utf-8";
        System.Text.StringBuilder MessageBuilder = new System.Text.StringBuilder();
        if(data.ServerExceptions.Count > 0)
        {
            MessageBuilder.Append("<h2>Server Exceptions</h2><ul>");
            foreach(var Exception in data.ServerExceptions) 
                MessageBuilder.Append("<li>" + Exception.Message + "</li>");
        }
        if(data.ClientExceptions.Count > 0)
        {
            MessageBuilder.Append("<h2>Client Exceptions</h2><ul>");
            foreach(var Exception in data.ClientExceptions) 
                MessageBuilder.Append("<li>" + Exception.Number + ": <b>" + Exception.Source + "</b> caused error: " + Exception.Message + ", value: " + Exception.Value + "</li>");
        }

        Response.Write(String.Format(System.IO.File.ReadAllText(Server.MapPath("/Content/ui/error.htm")), MessageBuilder.ToString()));
    }
}

As you can see this method supports JSON and JSONP requests. If it is determined that the requested content type is neither of these we simply fall back to a HTML response which we construct using a template file we can create.

/Content/ui/error.htm


<!DOCTYPE html>

<html>
<head>
</head>
<body>
    An error occurred while processing this request. The error description(s) are as follows:

    {0}
</body>
</html>

Of course this is just a very simple demonstration. You would most likely want to add in some styling and such so it looks like the page belongs with the rest of your web application. In order to get your application to actually use these changes you'll need to add a tag to your configuration.

/Web.config


<system.web>
    <customErrors mode="Off" />

    <!-- more settings -->

</system.web>

We're almost finished with the necessary changes to the server side of things. All we need to do now is add a few methods to MethodController so we can demonstrate the behavior.

/Controllers/MethodController.cs


public JsonResult DemonstrateClientError()
{
    throw new Classes.Exceptions.ClientException("None""Nothing""This is just a demonstration of a client exception.", Guid.Parse("0EEDFAD3-C8C3-4CFD-BF4C-E64B9F3136F3"));
}

public JsonResult DemonstrateServerError()
{
    throw new Exception("This is just a demonstration of a server exception.");
}

Let's edit one of our HTML views to call both of these methods.

/Content/ui/home/test1.htm


<script language="javascript" type="text/javascript">
    $(document).ready(function () {
        $(".obj_btnTestClientException").click(function () {
            AJAXLoadData("/Method/DemonstrateClientError"nullfunction (msg) { alert("You shouldn't see this message."); });
        });
        $(".obj_btnTestServerException").click(function () {
            AJAXLoadData("/Method/DemonstrateServerError"nullfunction (msg) { alert("You shouldn't see this message."); });
        });
    });
</script>

This is the test1.htm view file.<br />
<br />
<input type="button" class="obj_btnTestClientException" value="Trigger Client Exception" /><br />
<input type="button" class="obj_btnTestServerException" value="Trigger Server Exception" /><br />
<br />
<a href="#">Load Landing</a><br />

The final piece to the exception handling is to alter our AJAX functions to be aware of these exceptions.

/Scripts/Util.js


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, status, xhr) {
                var responseType = xhr.getResponseHeader("content-type") || "";
                if (responseType.toLowerCase().indexOf("html") > -1) {
                    __URLCache(url, msg);
                    if (successCallBack) successCallBack(msg);
                } else {
                    ProcessMessage($.parseJSON(msg));
                }
            },
            error: function (msg) {
                AppState().pushMessage("AJAXLoadHTML failed for url: " + url);
            }
        });
    } else {
        if (successCallBack) successCallBack(cache);
    }
}

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

function ProcessMessage(msg, successCallBack) {
    if (msg.ServerExceptions.length > 0) {
        $.each(msg.ServerExceptions, function (index, item) {
            alert("Server encountered an error: " + item.Message);
        });
    } else if (msg.ClientExceptions.length > 0) {
        $.each(msg.ClientExceptions, function (index, item) {
            alert(item.Number + ": " + item.Source + " caused error " + item.Message + ", value: " + item.Value);
        });
    } else if (successCallBack) {
        successCallBack(msg);
    }
}

As you can see, both functions now call ProcessMessage in order to display the various exceptions if present. Later on we'll alter this function to allow for custom client exception handling for a number of different scienarios but for now we'll just display them in javascript alert boxes. Run the project and use the buttons to test out both types of exceptions. In addition, try navigating to a resource that doesn't exist to see how it automatically switches to an HTML response when necessary.

Phase III Links: Download | Demo

Conclusion

At this point our project is beginning to take shape. We have some basic error handling and navigation strategies in place and we've demonstrated how we can populate an HTML view with non-static content. However we still haven't taken full advantage of our client side templating as all the static resources are currently in the same project as the web service. In the next phase of the project we'll be creating some additional projects in our solution in order to simulate a content server and demonstrating how we can easily add support for JSONP requests. Additionally we'll demonstrate some client exception handling with regard to form based requests.

Quick Links: << Previous: Script Level Caching | Next: JSONP and Content Server Simulation >>