Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Ajax In Action (2006).pdf
Скачиваний:
63
Добавлен:
17.08.2013
Размер:
8.36 Mб
Скачать

216CHAPTER 6

The user experience

with a visual cue that the process is under way, we improve the responsiveness of the application. By running all such notifications through a common framework, we ensure that presentation is consistent and make it simple for the user to work with the notifications because everything works in the same way.

Let’s start off by looking at the various ways in which we can notify the user of events taking place within the application.

6.2 Keeping the user informed

In an Ajax application, we may often need to run off across the network and fetch some resources from the server and then pick up the results in a callback function and do something with them. If we were handling server requests synchronously, we would have a relatively easy time working out how to handle this in user interface terms. The request would be initiated, the entire user interface would lock up and stop responding, and when the results come back from the server, the interface would update itself and then start responding to input. What’s good for the developer here is lousy for the user, of course, so we make use of an asynchronous request mechanism. This makes the business of communicating server updates through the user interface that much more complicated.

6.2.1Handling responses to our own requests

Let’s pick up a concrete example to work with. The planetary information viewer that we developed in chapter 5 allowed the user to update a couple of editable properties for planets: the diameter and the distance from the sun (see section 5.5). These updates are submitted to the server, which then responds, saying whether it has accepted or rejected them. With the introduction of the command queue concept in section 5.5.3, we allowed each server response to carry acknowledgments to several updates from a given user. A sample XML response document follows, showing one successful and one unsuccessful command:

<commands>

<command id='001_diameter' status='ok'/> <command id='003_albedo' status='failed' message='value out of range'/>

</commands>

From the user’s perspective, she edits the property and then moves on to some other task, such as editing the list of facts associated with that planet or even staring out of the window. Her attention is no longer on the diameter of the planet Mercury. Meanwhile, in the background, the updated value has been wrapped in

Keeping the user informed

217

 

 

a JavaScript Command object, which has entered the queue of outgoing messages and will be sent to the server shortly thereafter. The Command object is then transferred to the “sent” queue and is retrieved when the response returns, possibly along with a number of other updates. The Command object is then responsible for processing the update and taking appropriate action.

Let’s recap where we had left this code before we start refactoring. Here is our implementation of the Command object’s parseResponse() method, as presented in chapter 5:

planets.commands.UpdatePropertyCommand

.parseResponse=function(docEl){ var attrs=docEl.attributes;

var status=attrs.getNamedItem("status").value; if (status!="ok"){

var reason=attrs.getNamedItem("message").value; alert("failed to update "+this.field

+" to "+this.value+"\n\n"+reason);

}

}

This is good proof-of-concept code, ripe for refactoring into something more polished. As it stands, if the update is successful, nothing happens at all. The local domain model was already updated before the data was sent to the server, so everything is assumed to be in sync with the server’s domain model. If the update fails, then we generate an alert message. The alert message is simple for the developer but makes for poor usability, as we will see.

Let’s return to our user, who is probably no longer thinking about the albedo of the planet Mercury. She is suddenly confronted with a message in an alert box saying, “Failed to update albedo to 180 value out of range,” or something similar. Taken out of context, this doesn’t mean very much. We could upgrade our error message to say “Failed to update albedo of Mercury...,” but we would still be interrupting the user’s workflow, which was the reason that we switched to asynchronous message processing in the first place.

We would, in this particular case, also be creating a more serious problem. Our editable fields implementation uses the onblur event to initiate the process of submitting data to the server. The onblur() method is triggered whenever the text input field loses focus, including when it is taken away by an alert box. Hence, if our user has moved on to editing another property and is midway through typing into the second textbox, our alert will result in the submission of partial data and either mess up the domain model on the server or generate an error—and a further alert box if our validation code catches it!

218CHAPTER 6

The user experience

A more elegant solution than the alert box is needed. We’ll develop one shortly, but first let’s further complicate the picture by considering what other users are up to while we make our requests to the server.

6.2.2Handling updates from other users

Our planetary viewer application allows more than one user to log in at once, so presumably other users may be editing data while we are. Each user would presumably like to be informed of changes made by other users more or less as they happen. Most Ajax applications will involve more than one browser sharing a domain model, so this is again a fairly common requirement.

We can modify our XML response, and the Command queue object, to cope with this situation in the following way. For each update to the server-side model, we generate a timestamp. We modify the server-side process that handles updates to also check the domain model for recent updates by other users, and attach them to the response document, which might now look like this:

<responses updateTime='1120512761877'> <command id='001_diameter' status='ok'/>

<command id='003_albedo' status='failed' message='value out of range'/> <update planetId='002' fieldName='distance' value='0.76' user='jim'/>

</responses>

Alongside the <command> tags, which are identified by the ID of the Command object in the sent queue, there is an <update> tag, which in this case denotes that the distance from the sun of Venus has been set to a value of 0.76 by another user called Jim. We have also added an attribute to the top-level tag, the purpose of which we explain shortly.

Previously, our command queue sent requests to the server only if there were commands queued up. We would need to modify it now to poll the server even if the queue were empty, in order to receive updates. Implementing this touches upon the code in several places. Listing 6.1 shows the revised CommandQueue object, with the changes in bold.

Listing 6.1 CommandQueue object

net.cmdQueues=new Array(); b Global lookup

 

net.CommandQueue=function(id,url,onUpdate,freq){

c Extra parameters

this.id=id;

 

net.cmdQueues[id]=this;

 

this.url=url;

 

this.queued=new Array();

 

this.sent=new Array();

 

this.onUpdate=onUpdate;

Keeping the user informed

219

 

 

 

 

 

 

 

 

 

 

if (freq){

d Polling initializer

 

 

 

 

this.repeat(freq);

 

 

 

 

 

}

 

 

 

 

 

this.lastUpdateTime=0;

 

 

 

 

 

}

 

 

 

 

 

net.CommandQueue.prototype.fireRequest=function(){

 

 

 

if (!this.onUpdate && this.queued.length==0){

 

 

 

return;

 

 

 

 

 

}

 

 

 

Timestamp requests

var data="lastUpdate="+this.lastUpdateTime+"&data=";

 

 

for(var i=0;i<this.queued.length;i++){

 

 

 

 

var cmd=this.queued[i];

 

 

 

 

 

if (this.isCommand(cmd)){

 

 

 

 

data+=cmd.toRequestString();

 

 

 

 

this.sent[cmd.id]=cmd;

 

 

 

 

}

 

 

 

 

 

}

 

 

 

 

 

this.queued=new Array();

 

 

 

 

 

this.loader=new net.ContentLoader(

 

 

 

 

this.url,

 

 

 

 

 

net.CommandQueue.onload,net.CommandQueue.onerror,

 

 

 

"POST",data

 

 

 

 

 

);

 

 

 

 

 

}

 

 

 

 

 

net.CommandQueue.onload=function(loader){

 

 

 

 

var xmlDoc=net.req.responseXML;

 

 

 

 

var elDocRoot=xmlDoc.getElementsByTagName("responses")[0];

 

var lastUpdate=elDocRoot.attributes.getNamedItem("updateTime");

 

if (parseInt(lastUpdate)>this.lastUpdateTime){

 

 

 

this.lastUpdateTime=lastUpdate; e Updated timestamp

 

 

 

}

 

 

 

 

 

if (elDocRoot){

 

 

 

 

 

for(i=0;i<elDocRoot.childNodes.length;i++){

 

 

 

elChild=elDocRoot.childNodes[i];

 

 

 

 

if (elChild.nodeName=="command"){

 

 

 

 

var attrs=elChild.attributes;

 

 

 

 

var id=attrs.getNamedItem("id").value;

 

 

 

 

var command=net.commandQueue.sent[id];

 

 

 

 

if (command){

 

 

 

 

 

command.parseResponse(elChild);

 

 

 

 

}

 

 

 

 

 

}else if (elChild.nodeName=="update"){

 

 

 

 

if (this.implementsFunc("onUpdate")){

f Updated handler

 

this.onUpdate.call(this,elChild);

 

}

 

 

 

 

 

}

220

CHAPTER 6

 

The user experience

 

}

 

}

 

}

 

net.CommandQueue.prototype.repeat=function(freq){ g Server poller

 

this.unrepeat();

 

if (freq>0){

this.freq=freq;

var cmd="net.cmdQueues["+this.id+"].fireRequest()";

this.repeater=setInterval(cmd,freq*1000);

 

 

}

 

 

}

h Polling switch

net.CommandQueue.prototype.unrepeat=function(){

if (this.repeater){

 

 

clearInterval(this.repeater);

 

 

}

 

 

this.repeater=null;

 

 

}

 

 

 

 

 

 

 

 

We’ve added quite a bit of new functionality here. Let’s step through it.

First, we’ve introduced a global lookup of command queue objects b. This is a necessary evil given the limitations of the setInterval() method, which we’ll discuss shortly. The constructor takes a unique ID as an argument and registers itself with this lookup under this key.

The CommandQueue constructor now takes two other new arguments c. onUpdate is a Function object that is used to handle the <update> tags that we introduced into our response XML. freq is a numerical value indicating the number of seconds between polling the server for updates. If it is set, then the constructor initializes a call to the repeat() function g, which uses JavaScript’s builtin setInterval() method to regularly execute a piece of code. setInterval() and its cousin setTimeout()accept only strings as arguments under Internet Explorer, so passing variable references directly into the code to be executed is not possible. We use the global lookup variable and the unique ID of this queue to develop a workaround to this problem in the repeat() method. We also keep a reference to the repeating interval, so that we can stop it using clearInterval() in our unrepeat() method h.

In the fireRequest() method, we previously exited directly if the queue of commands to send was empty. That test has been modified now so that if an onUpdate handler is set, we will proceed anyway and send an empty queue in order to fetch any <update> tags waiting for us. Alongside our own edited data, we send a timestamp telling the server the date that we last received updates d, so that it

Keeping the user informed

221

 

 

can work out to send us relevant updates. This is stored as a property of the command queue and set to 0 initially.

We pass these timestamps as UNIX-style dates, that is, the number of milliseconds elapsed since January 1, 1970. The choice of timestamp is based on portability. If we chose a date format that was easier to read, we would run into issues with localization, differences in default formats across platforms and languages, and so on. Getting localization right is an important topic for Ajax applications, since the application will be exposed to users worldwide if it is on the public Internet or the WAN of a large organization.

In the onload() function, we add the code required to update the last updated timestamp when a response comes in eand to parse <update> tags f. The onUpdate handler function is called with the command queue as its context object and the <update> tag DOM element as the sole argument.

In the case of our domain model of the solar system, the update handler function is shown in listing 6.2.

Listing 6.2 updatePlanets() function

function updatePlanets(updateTag){ var attribs=updateTag.attributes;

var planetId=attribs.getNamedItem("planetId").value; var planet=solarSystem.planets[planetId];

if (planet){

var fld=attribs.getNamedItem("fieldName").value; var val=attribs.getNamedItem("value").value;

if (planet.fld){ planet[fld]=val;

}else{

alert('unknown planet attribute '+fld);

}

}else{

alert('unknown planet id '+planetId);

}

}

The attributes in the <update> tag give us all the information that we need to update the domain model on the JavaScript tier. Of course, the data coming from the server may not be correct, and we need to take some action if it isn’t. In this case, we have fallen back on an alert() statement compounding the problems that were discussed in section 6.2.1.

We’ve added quite a bit more clever code to our command queue object in the process of handling updates from other users, including passing timestamps