Asp.net MVC 2 with JSONP and jQuery

I’m willing to bet as a web developer there will be some project that you are working on that requires you to expose your data and or services across domain boundaries through an REST API and want them to be called via javascript.  Maybe you wrote a jQuery plugin that retrieves data from your site, or maybe you have several domains that share the same services.  Whatever the reason is, you will quickly discover the browser same-origin policy, which states “a web page served from domain1.com cannot normally connect to or communicate with a server other than domain1.com”.

Fortunately there is a way around this.  It’s called JSONP or “JSON with padding”.  To summarize,  JSONP works around the cross-domain browser restriction by making a GET request using a script tag, which isn’t limited by the same-origin policy.  It traditionally will also provide a callback method in the url so the response from the server will look something like this:

callback({"title": "hello world!", "rank": "number 1"})

This call is then executed, and voila you have your data.  Now the complexity of how to do this in JavaScript is masked by most JavaScript libraries, like jQuery.  I’m going to focus on how you enable this in Asp.net MVC 2.

  • The first thing you need is the ability for your controller to return JSON or JSONP base on the request. As far as I know, there is not an out of the box JSONP Action Result in MVC, so we will create one.
       public class JsonpResult<T> : ActionResult where T : class, new()
       {
         
           public const string DefaultJqueryCallbackParameter = "callback";
           private string callbackParameter;
           private bool useDataContract = false;
           private bool useDefaultCallback = true;
           
           #region Constructors
    
           public JsonpResult(T data, bool useDefaultCallbacks, bool useDataContract)
           {
               this.Data = data;
               this.useDefaultCallback = useDefaultCallbacks;
               this.useDataContract = useDataContract;
           }
    
           public JsonpResult(T data, string callbackParamter) : this(data, callbackParamter, false)
           {           
           }
    
           public JsonpResult(T data, string callbackParamter, bool useDataContract)
           {
               this.Data = data;
               this.callbackParameter = callbackParamter;
               this.useDefaultCallback = false;
           }
    
           public JsonpResult(T data) : this(data, true, false)
           {
           }
    
          
    
           #endregion Constructors
    
           #region Properties
    
           public Encoding ContentEncoding { get; set; }
    
           public string ContentType { get; set; }
    
           public T Data { get; set; }
    
           public JsonRequestBehavior JsonRequestBehavior { get; set; }
    
           public bool UseDataContractSerialization
           {
               get { return useDataContract; }
               set { useDataContract = value; }
           }
    
           #endregion Properties
    
           #region Methods
    
           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/json";
               }
    
               if (ContentEncoding != null)
               {
                   response.ContentEncoding = ContentEncoding;
               }
    
               if (Data != null)
               {
    
                   HttpRequestBase request = context.HttpContext.Request;
                   var jsonp =  IsJsonp(request.Params);
                   if(jsonp.Item1)
                   {
                       this.JsonRequestBehavior = JsonRequestBehavior.AllowGet; //this is new in MVC 2 otherwise an error will be thrown                    
                       response.Write(jsonp.Item2 + "(" + Data.ToJSON(useDataContract) + ")");                    
                   }                
                   else
                   {
                       //normal json here
                       response.Write(Data.ToJSON(useDataContract));
                   }
    
               }
           }
    
           private Tuple<bool, string> IsJsonp(NameValueCollection parameters)
           {                     
               if (useDefaultCallback && parameters[DefaultJqueryCallbackParameter] != null)
               {
                   return new Tuple<bool, string>(true, parameters[DefaultJqueryCallbackParameter]);
               }
               else if(!string.IsNullOrEmpty(callbackParameter) && null !=  parameters[callbackParameter] ){
                   return new Tuple<bool,string>(true, parameters[callbackParameter]);
               }
               return new Tuple<bool,string>(false, string.Empty);            
                   
           }
    
           #endregion Methods
       }
  • The real meat of the class is where we write out the JSON result to the response object with it wrapped in the callback function:
    response.Write(jsonp.Item2 + "(" + Data.ToJSON(useDataContract) + ")");    
  • Notice there is also a line there that changes the JSonRequestBehavior settings to AllowGet.  If we don’t do this then MVC 2 will barf on the response because of a known security vulnerability with GET requests and JavaScript arrays (more info here).  
  • I’m also using some JSON serialization extension methods that make serializing a bit simpler.    Here is the source for ToJSON, it takes a boolean which indicates whether you want the DataContractJsonSerializer or JavaScriptSerializer.  Each has it’s own benefits.
public static string ToJSON<T>(this T o, bool useDataContract) where T : class, new()
{
    string json = "";
    if (useDataContract)
    {
        var serializer = new DataContractJsonSerializer(typeof(T));

        using (var ms = new MemoryStream())
        {
            serializer.WriteObject(ms, o);
            json = Encoding.Default.GetString(ms.ToArray());
            ms.Close();
        }
    }
    else
    {
        var serializer = new JavaScriptSerializer();
        json = serializer.Serialize(o);

    }

    return json;
}

Finally all you need to do to use this new action result is pass it your data and other options you care about.  By default, it will use JavaScriptSerializer with the jQuery default callback name.

return new JsonpResult<SampleSiteInfoModel>(model);

Test:  To test that JSONP does in fact work across domains, use jsfiddle.net , my new favorite JavaScript test site, and point it at your local services.  In the sample app on codeplex there is a link to the fiddle that I created at the bottom of the master page.  Or you can look at it here, http://jsfiddle.net/FSnpp/1/.  To do the request  in jQuery, just change the Ajax options parameter dataType to ‘jsonp’ or use getJSON and add the callback parameter manually.

Important: The implementation of JSONP in jQuery is not without some flaws, one being it doesn’t raise errors when timeouts or error responses are given.  Fortunately there is a plugin that handles that for you.  I recommend looking at it if you get serious in consuming JSONP.  http://code.google.com/p/jquery-jsonp/

Note:  It the action result will default to just returning regular JSON if a callback parameter can’t be found in the request.  This means you can use this instead of the JsonResult for even normal requests and they will automatically handle JSONP.

Source: Full Source and Demo is available on Codeplex

kick it on DotNetKicks.com
Bookmark and Share
blog comments powered by Disqus
  • Menu

  • Tags