It was hard to come up with a title to this post because I somehow needed to convey the awesomeness for a problem which I don’t think a lot of people realise they have.
Quite simply, it is to do with the asynchronous manner in which we make JSONP calls (if you’re not sure how JSONP works, I recommend this simple article from Rick Strahl). As you know (or will after reading the article), JSONP relies on dynamically injecting a <script/>
tag into our document. Within this tag are two parts of JavaScript:
- The object you are returning from the server (in JSON format)
- A function wrapper which ‘pads’ the object (the ‘P’ in JSONP).
For example, if I wish to return Person
object from the server, I would stream down:
- On the server, create the object in JSON (using
Response.Write()
for example in .NET):
Person{FirstName : Larry, LastName : Phillips}
- Wrap the object in a function named ‘
mycallback
’:
mycallback({Person{FirstName : Larry, LastName : Phillips }})
On the awaiting HTML page, I would have already written the following JavaScript function:
function mycallback(person){
alert(person.FirstName);
}
The script
tag is injected into the page, the response is sent back, the browser automatically runs the JavaScript within the script
tag, the awaiting function is thereby called and voila – an alert()
box is shown.
Problem: The Callback is Decoupled from the Caller
This looks tidy enough with one example, but when you have dozens or even hundreds of callbacks (such as I have on www.stringsof.me, which prompted this solution), it becomes very hard to manage because for every server call you need to write a separate corresponding callback. It’s not shown in the example, but often the callback needs to tie back to the caller in some way to alter its state, which makes things even more complicated.
This is extra frustrating because if you are working with jQuery (for example), you are dealing with nice ‘inline’ callbacks when using regular AJAX calls:
$.ajax({
url: that.SiteRoot + route,
data: params,
type: 'GET',
dataType:"json",
success:function(person){
alert(person.FirstName);
}
});
See? The callback is written right within the AJAX call. For programmers, it is tidy and easy to follow. Unfortunately, if you specify the dataType
property as ‘jsonp
’, the callback doesn’t work.
Unfortunately, We Can’t Do This With a Cross-domain Call….
The reason the callback above doesn’t work for jsonp/cross-domain is (presumably) because it is not technically an AJAX call. From JavaScript’s point of view, it is just injecting a new DOM element into the page (the <script/>
tag). Once the tag’s src
is downloaded, JavaScript has already moved on to the next task. It is the hack I described above which allows us to link the two.
…until now! Using jQuery’s bind and trigger
Enter jQuery’s bind and trigger functionality. Observe…if I write…
$(‘body’).bind(‘foo’, function(){alert("I’m called");});
…and then at any time later, I write….
$('body').trigger('foo');
…the popup appears. So I saw this, and I thought, perhaps I can fake a callback between the two JSONP events. So, here goes…
var Data = function () {
var that = this;
return {
CallJSON : function(route, params, callback) {
var triggerName = new Date().getTime().toString();
params._JsonPCbTrigger = triggerName;
params._JsonPCb = 'Data.OnCallJSON';
$("body").bind(triggerName, function(e, result) {
callback(result);
});
$.ajax({
url: 'http://www.stringsof.me/ + route,
data: params,
type: 'GET',
dataType:"json"
});
},
// This is the generic handler for *all* JSONP calls.
OnCallJSON : function(result, triggerName) {
$("body").trigger(triggerName, result);
// We unbind afterwards, simply to release memory
$("body").unbind(triggerName);
}
};
} ();
Now, anywhere in my code, I can call (for example):
var params = { personID: 1 };
Data.CallJSON('GetPersonByID', params, function(person) {
alert(person.FirstName);
});
The Server Code
That actually concludes the article, but for completeness and in case readers are still a little confused over JSONP, I’ll include the server code that is required to make this work. It’s in C#, but in essence it is simply writing regular JavaScript to the response.
var callBackTrigger = context.Request.Params["_JsonPCbTrigger"];
var callbackFunctionName = context.Request.Params["_JsonPCb"];
var personID = int.Parse(context.Request.Params["personid"]);
var person = new PersonManager().GetPerson(personID);
var personJSON = Newtonsoft.Json.JsonConvert.SerializeObject(person);
var parameters = personJSON + ", " + callBackTrigger;
var functionCall = callbackFunctionName + "(" + parameters + ")";
context.Response.Write(functionCall);
context.Response.End();