MVC Service Based Web Applications - Part IV - JSONP and Content Server Simulation

One big advantage to client side templating that we have yet to demonstrate in our project is to move all our static HTML views to a content server. In order to simulate this kind of setup we'll need to add another project to our solution. Since everything related to the UI will no longer exist in our current project we're going to change some names around in the process and I'm also going to move some classes into a library project so we can reference them later. Here's the structure we'll be using for our simulated static content server.

UI.Web.Static


    Content/
        themes/
            <jquery theme files>
        ui/
            home/
                landing.htm
                test1.htm
            error.htm
            Site.css
    Scripts/
        Internal.js
        jquery-1.5.1.min.js
        jquery-ui-1.8.11.min.js
        jquery.ba-hashchange.min.js
        Util.js
    default.htm

The only important difference here is that instead of living in the Views folder on the ASP.Net MVC project our index.cshtml file is now in the root and has been called default.htm. We weren't using any of the Razor templating code on it before so no changes to the file itself are necessary. The next step is to create a class library project.

Utilities


    Exceptions/
        ClientException.cs
        ServerException.cs
    JSONMessageObject.cs

Both ClientException and ServerException need their namespaces changed to Utilities.Exceptions. This will break some of the references we've made in our Global.asax.cs file which we'll edit in a moment. The other important change is that since this is a class library project we'll need to add the System.Web.Extensions reference manually to allow JSONMessageObject to use the JavaScriptSerializer. Finally we'll create a new ASP.Net MVC project.

WebService


    Content/
        logs/
        ui/
            error.htm
    Controllers/
        BaseController.cs
        HomeController.cs
        MethodController.cs
    Global.asax
    Global.asax.cs
    Web.config

A few references will be broken but a quick replace of UI.Classes with Utilities should resolve most if not all of them. Finally we'll need to edit the solution to start both the UI.Web.Static and WebService projects. It may make sense to edit the WebService project to not open a page since we won't be accessing it directly any longer.

Upon running the application for the first time a window will pop up asking if you would like a Web.config file to be created for the UI.Web.Static project. We won't be debugging anything in this application so it's not really necessary but I created it anyway. While the hash navigation should still be functional the landing.htm page will not load the web service data. We'll need to make three signficant changes to our AJAXLoadData method to get it working again.

/UI.Web.Static/Scripts/Util.js


function AJAXLoadData(url, data, successCallBack) {
    url = "http://service.phase4.mvctutorial.netortech.com" + url; 

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

As you can see we're prepending the URL to the web service now. You can alter this URL to localhost and the port that Visual Studio provides but since we're already making a cross domain request there's no reason why my URL shouldn't work just as well for you. In addition we've switched from using the POST verb to GET. For cross domain requests such as JSONP you cannot submit POST data. While this is a bit of a drawback browsers are beginning to add support for these types of situations and in the mean time we can make do with query string parameters.

We're not quite done since we still need to add support for JSONP requests in our controller. After doing a bit of research on Google I ran across this article from Nerdworks Blogorama for Enabling JSONP calls on ASP.Net MVC. Their solution worked but I tweaked it somewhat for my own needs.

/Utilities/Services/JsonpResult.cs


public class JsonpResult : JsonResult
{
    public JsonpResult(string callback)
    {
        Callback = callback;
    }

    /// <summary>
    /// Gets or sets the javascript callback function that is
    /// to be invoked in the resulting script output.
    /// </summary>
    /// <value>The callback function name.</value>
    public string Callback { get; set; }

    /// <summary>
    /// Enables processing of the result of an action method by a
    /// custom type that inherits from <see cref="T:System.Web.Mvc.ActionResult"/>.
    /// </summary>
    /// <param name="context">The context within which the
    /// result is executed.</param>
    public override void ExecuteResult(ControllerContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");

        HttpResponseBase response = context.HttpContext.Response;
        if (!String.IsNullOrEmpty(ContentType))
            response.ContentType = ContentType;
        else
            response.ContentType = "application/javascript";

        if (ContentEncoding != null)
            response.ContentEncoding = ContentEncoding;

        if (Callback == null || Callback.Length == 0)
            Callback = context.HttpContext.Request.QueryString["callback"];

        if (Data != null)
        {
            string ser = (new JavaScriptSerializer()).Serialize(Data);
            response.Write(Callback + "(" + ser + ");");
        }
    }
}

You'll need to add the System.Web and System.Web.Mvc references to the Utilities project for this to work. The class is fairly simple and merely overrides the necessary method to encapsulate the JSON data into a callback function to make it JSONP compatible. Now all we need to do is get our methods to use it.

/WebService/Controllers/BaseController.cs


public const string DEFAULT_JSONP_CALLBACK_PARAMETER = "callback";

public virtual string JsonpCallback { get { return Request.QueryString[DEFAULT_JSONP_CALLBACK_PARAMETER] ?? string.Empty; } }

private JsonResult Message(Utilities.JSONMessageObject message)
{
    if (JsonpCallback != string.Empty)
        return new Utilities.Services.JsonpResult(JsonpCallback) { Data = message, ContentType = "application/javascript", JsonRequestBehavior = JsonRequestBehavior.AllowGet };
    else
        return new JsonResult() { Data = message, ContentType = "application/json", JsonRequestBehavior = JsonRequestBehavior.AllowGet };
}

Here we simply add a readonly property to the controller which reads the appropriate querystring value which represents the JSONP callback parameter. If it has been set we return a JSONP message. Let's make some changes to our global file to finish up adding JSONP support.

/WebService/Global.asax.cs


private void OutputMessage(Utilities.JSONMessageObject data)
{
    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") ||
        Request.QueryString.AllKeys.Contains(Controllers.BaseController.DEFAULT_JSONP_CALLBACK_PARAMETER))
    {
        Response.ContentType = "application/javascript; charset=utf-8";
        Response.Write(Request.QueryString[Controllers.BaseController.DEFAULT_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()));
    }
}

To avoid confusion we've replaced the previous const string with the one we've defined in BaseController. I also ran into some issues strictly using the ContentType to detect a JSONP request so I added a check for the query string callback parameter. Run the application and you should see the view again populate correctly.

Phase IV Links: Download | Demo

Conclusion

We now have our static content in the form of HTML and JavaScript completely separated from our dynamic content. Here are just a few benefits of the setup we've created.

  • All our static content can be replicated to servers across the globe which speeds up delivery.
  • HTML + JS views are simple. A perfect job for an inexperienced developer to work on with little oversight.
  • Complete separation of the views from the service means a client can be written on any platform. Even by 3rd parties.

In the next phase we'll be taking a look at how to create some basic user interfaces including a registration form and altering our exception handling to display ClientExceptions in a variety of different contexts.

Quick Links: << Previous: Anchor Navigation and Exception Handling | Next: Master Pages and Multi-lingual Support >>