Thursday, June 21, 2007

Fun With AJAX

Update: someone has come up with a slightly different solution to the problem discussed (See here)
Meanwhile...
... I mean, what other sort of experience would one have with Asynchronous Javascript And Xml?

Indeed, there's a lot to like about a technique that allows you to update your webpage with a snippet of information retrieved from the server *without* having to reload the whole sorry spectacle. It allows for much greater range of interaction with the user, as well. As I type this blog entry, a small message at the foot of the page is reassuring me that my draft is being stored automatically. How? Presumably a little java applet is being fired at regular intervals, which invokes an XMLhttpRequest (or XMLObject, if you're using IE 6 or earlier), and sends the current data back to the server.

Well, having eulogized the technique, what have I found to grouch about?

The Problem
As an old song put it: 'nice legs, shame about the face!'. One thing, in particular, has been annoying me about the callback mechanism. On just about every tutorial on the subject (and since there are plenty, rather than reinvent the wheel here, I suggest you go google 'Ajax tutorial'! ) you will see something like the following code:

var ajaxHandler = false;

onClick (element)
{
try
{
// Firefox, Opera 8.0+, Safari
ajaxHandler=new XMLHttpRequest();
}
catch (e)
{
// Internet Explorer
try
{
ajaxHandler=new ActiveXObject("Msxml2.XMLHTTP");
}
catch (e)
{
try
{
ajaxHandler=new ActiveXObject("Microsoft.XMLHTTP");
}
catch (e)
{
alert("Your browser does not support AJAX!");
}
}
}

if (ajaxHandler)
{
ajaxHandler.open("GET", "http://www.etc.etc/myurl", true);
ajaxHandler.onreadystatechange =
function()
{
if (ajaxHandler.readyState == 4)
{
// Handle the reponse!
}
}

ajaxHandler.send(null);
}
}


Perfectly workable as is, and quite adequate for a simple starting tutorial on how to make an AJAX call.

Except...

Except, how does the callback function know where to retrieve the results? It doesn't get provided as a call parameter. Nope! The callback function has to go grubbing around for a global variable! This is ugly, and I don't just mean in the cosmetic sense either.

It Gets Worse
As you gain confidence with javascript and Ajax, you will start to use the 'asynchronous' part of the technique more and more: making several Ajax calls simultaneously. Possibly making several calls to the same Ajax function simultaneously... using the same global object! As anyone familiar with multiple processing will attest, this situation is not just ugly, but downright bad.

While Quirksmode has an interesting account of what can happen, and how to avoid the worst of it, there is an astonishing paucity of advice on this topic. So, I had a bit of a tinker, and here is my solution.

Preliminary Tidying Up
First of all, I like a place for everything, and everything in its place. Just as a server process might create a short lived thread/process to handle each request it receives, I would like to see each Ajax call result handled by a single object.

If XMlhttpRequest passed itself to the call back function, we would have a solution (and I wouldn't have a topic). Can we simulate this?

Of course we can, by re-writing the above listing as a wrapper function that implements a chain of command. The main routine then becomes:

function MyCallback (handler)
{
if (handler.readyState == 4)
{
// Prettier, but!
}
}

function onClick (element)
{
CallAjax (MyCallback, "http://www.etc.etc/myurl");
}


This separates the code handling AJAX, which now reads as follows.

var ajaxHandler = AjaxHandler ();

function AjaxHandler ()
{
try
{
// Firefox, Opera 8.0+, Safari
handler=new XMLHttpRequest();
}
catch (e)
{
// Internet Explorer
try
{
handler=new ActiveXObject("Msxml2.XMLHTTP");
}
catch (e)
{
try
{
handler=new ActiveXObject("Microsoft.XMLHTTP");
}
catch (e)
{
alert("Your browser does not support AJAX!");
}
}
}

return handler;
}

function CallAjax (callback, url)
{
if (ajaxHandler)
{
ajaxHandler.open("GET", url, true);
ajaxHandler.onreadystatechange =
function()
{
callback (ajaxHandler);
}

ajaxHandler.send(null);
}
}

An Obvious Solution...
Well, this might look a bit tidier but, apart from that, nothing much has been achieved: while the callback function proper now has a handler provided to it, it's still going to be the same handler each time a call is made. We want to create a request object each time we make a call.

So, would this do?

:

function CallAjax (callback, url)
{
var ajaxHandler = AjaxHandler();

if (ajaxHandler)
{
ajaxHandler.open("GET", url, true);
ajaxHandler.onreadystatechange =
function()
{
callback (ajaxHandler);
}

ajaxHandler.send(null);
}
}

...That Doesn't Work!
Unfortunately, no. The problem here is that, although a new handler object is being created, with its own callback object, the language is javascript, ie the command is interpreted, so the dummy function is referring to whatever the handler is at the time the callback was received, which probably isn't what you expected!

A Refinement...
However, there is a way around this: retain the global handler as an array, and refer to the required handler by its array index:

var ajaxHandler = new Array();

function AjaxHandler ()
{
:
var handle = ajaxHandler.length;
ajaxHandler[handle] = handler;
return handle;

}

function CallAjax (callback, url)
{
var handle = AjaxHandler();
if (handle >= 0)

{
var handler.open("GET", url, true);
handler.onreadystatechange =
function()
{
callback (ajaxHandler[handle]);
}

handler.send(null);
}
}


Now we're beginning to cook! Each new request handler is stored in a static array, and the dummy callback function refers to it by its index. This is important because to refer to a handler specifically will cause the same problem as before: several callback functions referring to an indeterminate handler.

Why this is so has something to do with the way the dummy function is generated (it is a class) . To refer to the handler object itself is to refer to it by reference (ie the address of the temporary script variable) to refer to the array index is to refer to it by value (an integer). The latter approach ensures that each callback method refers to a specific handler.

Commentary
I haven't been using it long enough to give a definitive judgement but, so far, this technique seems to work well. I do have some qualms about that array of request objects growing in the corner like a stack of dirty dishes. It should get cleared away each time the page is refreshed. However, it may cause memory problems if the page persists for long. I am loath to suggest removing old objects from the array since it would upset the index references of current Ajax calls. If this is a concern, you could replace each object entry with an integer as it completes.

An Embellishment
One final embellishment to the procedure. Since we now have a single definition of a callback function, we can use it to perform useful universal tasks such as telling whether or not the server side script ran properly. eg, when returning the result as XML, this is readily done by checking to see whether or not the responseXML field was generated. If it wasn't, then the raw text may contain the warning diagnostics. Similar results can be achieved by searching the result Text field for error indicators.


function CallAjax (callback, url)
{
var handle = AjaxHandler();if (handle >= 0)
{
var handler.open("GET", url, true);
handler.onreadystatechange =
function()
{
var response = ajaxHandler[handle];
if (response.responseXML)
{
callback (response );
}
else
{
alert (response.responseText);
}
}

handler.send(null);
}
}


OK? Now go and play.

Final Listing
The following is the complete final listing:

var ajaxHandler = new Array();

function AjaxHandler ()
{
try
{
// Firefox, Opera 8.0+, Safari
handler=new XMLHttpRequest();
}
catch (e)
{
// Internet Explorer
try
{
handler=new ActiveXObject("Msxml2.XMLHTTP");
}
catch (e)
{
try
{
handler=new ActiveXObject("Microsoft.XMLHTTP");
}
catch (e)
{
alert("Your browser does not support AJAX!");
}
}
}

var handle = ajaxHandler.length;
ajaxHandler[handle] = handler;
return handle;
}

function CallAjax (callback, url)
{
var handle = AjaxHandler();

if (handle >= 0)
{
var handler = ajaxHandler[handle];
handler.open("GET", url, true);
handler.onreadystatechange =
function()
{
var response= ajaxHandler[handle];
if (response.responseXML)
{
callback (response);
}
else
{
alert (response.responseText);
}
}

handler.send(null);
}
}

3 comments:

Anonymous said...

Impressive piece of work and most helpfull to get me going on Ajax/PHP approach.

My only question; I can't get rid of the idea that the handle array, that's supposed to secure the threadsafety, is stored on the browserside not on the serverside.

If it is, then what's the point of creating such a complex Ajax handler. For, it seems to me that the serverside part (PHP script) is taking care of it's own "problems"; all the vars in the PHP script are used locally and eventhough it easily could happen that a PHP script is triggered more-then-once, each PHP-execution retrieves a thread of it's own and stays on that thread, thus keeping track of it's own vars and solving it's own request.

Maybe I'm missing something...

Roland Labruyere

Tony Fisk said...

Thanks for the kudos, Roland.

While I've got a fair degree of computing experience, I'm fairly new to javascript and its idiosynchrocies. This post was my solution to a problem that I was having, and which no one else seems to have tackled satisfactorily (although I think Quirksmode had a go... can't find the reference though :-(.

If someone has a neater solution, let's hear it!!

Anyway, to your question...

As you say, the server is taking care of its own problems, and handling its own variables.

If the ajax callback mechanism referred to its own response handler, then there would be no problem. Unfortunately, it doesn't and the handler has to be set up as a global variable so that the callback function can access it. With this restriction, the only way I can think of to handle situations where several calls of the same type are made is to store handlers in a global array.

The complexity of the ajax handler has several causes:

1. the need to cater for different ajax objects, depending on the browser being used (this will hopefully become less of a problem as IE 6 phases out, meantime...). I should probably break this out into a separate function 'AjaxObject' which only AjaxHandler calls.

2. the need to apply the result of each ajax call to the context in which it was called (especially if the same object is being used. If *different* Ajax calls were made, then each could be given its own static handler. This gets messy with large applications, however).

3. The need to refer to the stored handler by its array index, rather than directly. I believe this was necessary to prevent the dummy function from interpreting the value of 'handler' incorrectly.

I suppose I could set up a demonstration, but the most pragmatic response for the moment is to suggest you experiment and see if you encounter problems if you do any simplifications.

Let me know how you go.

Simon Wright said...

Light dawned for me when I realised that a function nested in an object can see the object's instance variables; so on object creation, preserve 'this' in an instance variable.

Utility script at http://embed-web-srvr.cvs.sourceforge.net/viewvc/*checkout*/embed-web-srvr/ews/src/HttpInteraction.js?revision=1.6

Example at http://embed-web-srvr.cvs.sourceforge.net/viewvc/*checkout*/embed-web-srvr/ews/doc/ajax.js?revision=1.6

You may have some trouble running the example unless you're an Ada hacker.