The user experience
6.1 Getting it right: building a quality application
Usability is an especially hot topic for Ajax because web app users can be an extremely fickle bunch. The downside of being able to download and run your app with zero effort is that the users have invested no time and effort in it when they start to use the application and will be willing to throw it away and move on to the next of the 8 billion web pages that Google can point them to. To complicate matters further, with Ajax we are seeing the convergence of two different usability traditions, namely the desktop application and the web page. Getting the mixture right can be quite a challenge, and failing to get it right can consign your hard work to obscurity.
In chapter 1, we looked at usability from the users’ point of view. What do they want in an application? And what are they willing to put up with? Let’s turn the question around now and ask what qualities we need in our code to meet the goal of usability. With this as a starting point, we can figure out what we can do in practical terms to make our application work. The following sections detail a number of key features that add quality to your application.
6.1.1 Responsiveness
The most basic frustration that a computer user can suffer from is to have workflow interrupted while the computer struggles to catch up with him. Basic design mistakes, such as locking up the entire user interface while writing some lengthy configuration file to disk, can cause the user to lose track of what he is doing and force him to make the mental leap between the domain model in which he is engaged to the harsh reality of computer hardware.
When looking at responsiveness, it can be important to understand your target audience and its typical system setups. In the case of writing a configuration file, the speed may be acceptable on the developer’s high-speed 7200-RPM SATA disk drive on the local workstation, but the customer writing the file to a congested network share or a USB thumb drive may have a different experience. In the specific case of web app development, a similar mistake is often made in only testing the application running over the loopback interface, that is, the web server running on the same development machine as the web browser. This doesn’t give a useful evaluation of network latency issues, and all web apps ought to be either tested over a real LAN or WAN or simulated using a traffic-shaping tool.
Beyond network issues, the performance of the client code can make a huge difference to responsiveness. Performance is a big issue, so we’ll defer a closer look at it to chapter 7.
6.1.2 Robustness
An application is robust if it can withstand the usual conditions encountered at a busy workstation. How does it cope with a temporary network outage? If a badly behaved program hogs the CPU for five minutes, will your application still work afterwards? At one recent project that I was involved in, we would test our application’s robustness by pounding randomly on the keyboard for 10 seconds or so, and by “scribbling” the mouse across the page while clicking it. A crude sort of test—but effective—and quite good fun!
What can such a test reveal? For one thing, it can highlight inefficiencies in event-handler code. Keypresses, mouse moves, and the like need to return quickly, since they are apt to be called very frequently. Further, they can reveal unintentional dependencies between components. A particular condition may arise in a GUI, for example, where a modal dialog is blocking access to the main application, but an open menu item is blocking access to the modal dialog. If such a situation depends on precise timing in the opening of the dialog and the menu, it might take a single user two months of daily work to discover it. Once released to an audience of several thousand, however, it might start showing up within hours and be extremely hard to reproduce from field reports. Identifying the problem up front, and correcting it, increases the overall robustness of the application.
There is more to robustness than randomly thumping the keyboard. Just as valuable is the process of watching someone other than the developer trying to use the application. In the case of a complete newcomer, this can provide helpful information on the overall usability design, but it is also useful to let someone closely acquainted with the domain knowledge, even the product, test-drive a new bit of functionality. When the person who wrote the code runs the program, she can “see” the code behind it and may subconsciously avoid specific combinations of actions or particular actions in specific contexts. The end user won’t have this insider knowledge, of course, and neither will the developer sitting next to you (unless you do pair programming). Getting someone else to informally run through your app workflow can help to build up robustness early on.
6.1.3 Consistency
As we already noted, the usability patterns of Ajax are still evolving from a mishmash of desktop application and web browser conventions. Some Ajax toolkits, such as Bindows, qooxdoo, and Backbase, even present widget sets deliberately styled to look like desktop application buttons, trees, and tables. The right answer to this conundrum is still being worked out by webmasters, usability gurus, and everyday users of the Web as it continues to evolve. In the meantime, the best advice available is to keep things consistent. If one part of your application uses web-style single clicks to launch pop-up windows, and another part requires double-clicks on similar-looking icons, your user will quickly become confused. And if you must have a talking pig that guides your users around the site, make sure that it doesn’t suddenly change its accent, costume, or hairstyle halfway through!
From the point of view of your codebase, consistency and reuse go hand in hand. If you cut and paste functionality from one location to another, and then respond to a change request in three copies of the button-rendering code but miss a fourth, the consistency of your interface will erode over time. If there is only one copy of the button-rendering code that everyone uses, then the consistency of your application is likely to remain high. This applies not only to visual UI behavior but also to less-visible parts of the interface, such as network timeouts and responses to bad data.
6.1.4 Simplicity
Finally, we need to stress the importance of simplicity. Ajax allows you to do a number of wild and creative things, the likes of which have never been seen in a web page before. Some of these things have never been seen because the necessary technology is only just arriving on the scene. In other cases, there are good reasons for not implementing a feature. Spring-loaded menus that bounce onto the screen and gradually dampen their oscillations may be great fun to code and great fun for a short-term user dropping by for five minutes to let off steam. If the user is going to use the application for several hours a day, though, she is less likely to appreciate the fun by the day’s end.
It is always worth asking whether a new feature will actually improve the end experience. In many cases with Ajax, the answer will be yes, and the developer can concentrate on coding features that will be genuinely beneficial.
6.1.5 Making it work
It’s probably the case that your code doesn’t exhibit all the features we just mentioned.
Mine certainly doesn’t. These are merely ideals that we’ve presented. Making the effort to move toward these ideals can pay big dividends when it comes to maintaining your codebase in the future, and refactoring existing code can introduce these qualities as you go along. Choosing where to concentrate the effort is something of a black art, and the only way to get good at it is by practicing. If you’re new to refactoring, start with something small and gradually work outward. Remember, refactoring is an incremental process, and you can add quality to your code without pulling it apart and leaving bits on the floor for weeks on end.
In the remainder of this chapter, we’ll look at some specific features that you can build into Ajax applications. A large part of the chapter focuses on notification frameworks, which are ways of keeping the user informed while background processes such as calculations or network requests take place. By providing the user 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.1 Handling 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:
message='value out of range'/>
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 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! 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.2 Handling 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:
Alongside the tags, which are identified by the ID of the Command object in the sent queue, there is an 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.
net.cmdQueues=new Array();
net.CommandQueue=function(id,url,onUpdate,freq){
this.id=id;
net.cmdQueues[id]=this;
this.url=url;
this.queued=new Array();
this.sent=new Array();
this.onUpdate=onUpdate;
if (freq){
this.repeat(freq);
}
this.lastUpdateTime=0;
}
net.CommandQueue.prototype.fireRequest=function(){
if (!this.onUpdate && this.queued.length==0){
return;
}
var data="lastUpdate="+this.lastUpdateTime+"&data=";
for(var i=0;ivar 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;
}
if (elDocRoot){
for(i=0;ielChild=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")){
this.onUpdate.call(this,elChild);
}
}
}
}
}
net.CommandQueue.prototype.repeat=function(freq){
this.unrepeat();
if (freq>0){
this.freq=freq;
var cmd="net.cmdQueues["+this.id+"].fireRequest()";
this.repeater=setInterval(cmd,freq*1000);
}
}
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 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 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 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 e and to parse tags f. The onUpdate handler function is called with the command queue as its context object and the 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.
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 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 between the client and web tiers, and adding a pluggable update handler function.
Eventually we come full circle to the issue of informing the user of changes and asynchronous updates as they take place. In the next section, we look at our options for presenting this information to the user in a more workable fashion, and we’ll factor out that pesky alert() function.
6.3 Designing a notification system for Ajax
The alert() function that we’ve been relying on up to now is a primitive throwback to the earlier, much simpler days of JavaScript, when web pages were largely static and the amount of background activity was minimal. We can’t control its appearance in any way through CSS, and for production-grade notification, we’re much better off developing our own notification mechanisms using the techniques employed to build the rest of our Ajax user interface. This also provides a much greater degree of flexibility.
If we look across the full spectrum of computer systems, we see that notifications come in many shapes and sizes, varying considerably in their impact on the user. At the low end of the scale regarding obtrusiveness are changes to the mouse cursor (such as the Windows hourglass or the Mac “spinning beach ball”) or the addition of secondary icons or emblems to an image denoting the status of the files or other items in a folder. These simple indicators offer relatively little information.
A status bar can provide a bit more detail on background events, and finally the full-blown dialog can show a greater degree of detail than either. Figure 6.1 illustrates a range of notification conventions being used in the KDE desktop for UNIX. The folder called lost+found is not accessible to the current user, so a secondary image of a padlock has been superimposed over that folder. The status bar at the bottom of the main window gives further information on the contents of the folder being viewed, without interrupting the user. Finally, the error window that is presented when the user tries to open the locked folder presents a stronger notification requiring immediate action by the user. Compared to these notifications, the use of alert() is essentially ad hoc, as well as being simplistic and ugly. In our quest for robustness, consistency, and simplicity, it makes sense to develop a framework for presenting notifications to the user that we can reuse throughout our application. In the following sections we’ll do just that.
Figure 6.1 Various conventions for providing status information in a user interface: modifying the icons to reflect particular characteristics (here access permissions), a status bar providing summary information, and a modal dialog. The interface shown here is the KDE desk top for UNIX workstations, but similar conventions are found in most popular graphical interfaces.
6.3.1 Modeling notifications
As a first step, let’s define what a notification message looks like. It contains a text description for the user and optionally an icon to display alongside the message. When we notify the user of background activity, some messages are going to be more urgent than others. Rather than working out each time whether or not to show a particular message, let’s define some generic priority levels, which we can then assign to each message.
In general, we want to tell the user something, which they can acknowledge and dismiss. Some messages might be important enough to stay around indefinitely until the user does dismiss them, whereas others will be relevant only for a limited time. Where a message does remove itself without user intervention, it may be in response to a callback, if it is telling the user that some background process such as a network download is under way and the process finishes before the user dismisses the notification. In other cases, such as a news feed, we may simply wish to assign a given lifetime to a message, after which it will tidy itself away. With these requirements in mind, listing 6.3 presents a Message object, which provides a generic behind-the-scenes representation of a notification that is to be presented to the user. Once we’ve established this model of notification messages, we can dress them up in various ways, as we will see later.
var msg=new Object();
msg.PRIORITY_LOW= { id:1, lifetime:30, icon:"img/msg_lo.png" };
msg.PRIORITY_DEFAULT={ id:2, lifetime:60, icon:"img/msg_def.png" };
msg.PRIORITY_HIGH= { id:3, lifetime:-1, icon:"img/msg_hi.png" };
msg.messages=new Array();
msg.Message=function(id,message,priority,lifetime,icon){
this.id=id;
msg.messages[id]=this;
this.message=message;
this.priority=(priority) ? priority : msg.PRIORITY_DEFAULT.id;
this.lifetime=(lifetime) ? lifetime : this.defaultLifetime();
this.icon=(icon) ? icon : this.defaultIcon();
if (this.lifetime>0){
this.fader=setTimeout(
"msg.messages['"+this.id+"'].clear()",
this.lifetime*1000
);
}
}
msg.Message.prototype.clear=function(){
msg.messages[this.id]=null;
}
msg.Message.prototype.defaultLifetime=function(){
if (this.priority<=msg.PRIORITY_LOW.id){
return msg.PRIORITY_LOW.lifetime;
}else if (this.priority==msg.PRIORITY_DEFAULT.id){
return msg.PRIORITY_DEFAULT.lifetime;
}else if (this.priority>=msg.PRIORITY_HIGH.id){
return msg.PRIORITY_HIGH.lifetime;
}
}
msg.Message.prototype.defaultIcon=function(){
if (this.priority<=msg.PRIORITY_LOW.id){
return msg.PRIORITY_LOW.icon;
}else if (this.priority==msg.PRIORITY_DEFAULT.id){
return msg.PRIORITY_DEFAULT.icon;
}else if (this.priority>=msg.PRIORITY_HIGH.id){
return msg.PRIORITY_HIGH.icon;
}
}
We define a global namespace object msg for our notification system, and within that an associative array from which any message can be looked up by a unique ID. The ID generation scheme will depend on the application using the framework.
We define three constants defining the three priority levels—low, default, and high—to which a message can be assigned. Each priority defines a default icon and lifetime (in seconds), both of which may be overridden by optional arguments in the constructor function. A lifetime value of -1, assigned to high-priority messages, indicates that the message will not expire automatically but will need to be dismissed explicitly, either by the user or by a callback function.
6.3.2 Defining user interface requirements
In MVC terms, we have provided a Model for our notification system here. To make it useful, we need to define a View. There are many possible ways of visually representing notifications. For this example, we have chosen to provide a status bar of sorts, upon which notifications are represented as icons, as illustrated in figure 6.2.
Figure 6.2 Status bar user interface for our notification system. Individual messages are represented by their icons. The red X icon is a standard icon provided for low-level notifications by our system.
The third message object on the status bar has provided its own icon, a blue, shaded sphere, which overrides the default X. Each notification that is added to this status bar can be inspected as a tooltip device, as shown in figure 6.3.
Figure 6.3 Messages on the status bar can be inspected by rolling the mouse over the icon, causing a tooltip to appear.
This mechanism is designed to be unobtrusive. The status bar occupies relatively little screen space, but it is apparent to the user when a new notification has arrived by the presence of an additional icon. For more urgent messages, we may
Figure 6.4 Higher-priority messages are shown in a pop-up dialog, in which messages are listed in order of priority. wish for something more immediate. To this end, we will initially display only lowpriority messages in the status bar; default and high-priority messages will first appear in a pop-up dialog, as illustrated in figure 6.4, before being dismissed.
The dialog can be modal or nonmodal. In the case of modal dialogs, we use a semitransparent layer to block off the rest of the user interface until the user dismisses the dialog. When dismissed, the dialog is represented by an icon on the right side of the status bar. In the following two sections, we’ll look at implementations of these features.