An Asynchronous Cookbook (Part One)

If you haven’t read Sho Kuwamoto’s series of blog posts on dealing with asynchronous events, I highly recommend them. Any Flex developer that builds applications that make remote calls is eventually going to run into the problem of how best to structure their code. In the course of developing NoteTag, we tried dozens of solutions to “the asynchronous problem”. The bad news: there is no one-size-fits-all solution. The good news: there are a few key rules-of-thumb which can guide you towards choosing the solution that’s right for the situation.

I’ll walk through the specific problems we’re trying to solve, using the example of a class which provides support for retrieving an Atom service document. (In Atom Publishing Protocol parlance, a service document is an index of blogs that are available from a particular account on a blog server.)

Problem #1: Asynchronous calls don’t return right away, but you often need to process the result.

Solution #1: Create a result handler method, and add it as an event listener to the object which is making the asynchronous call.

public class AtomPublishingProtocol extends EventDispatcher
{
// Dispatches a GetServiceEvent after retrieving the service document.
public function getService(serviceURI:URI):void
{
var service:HTTPService = new HTTPService();
service.url = serviceURI.toString();
service.method = "GET";
service.resultFormat="e4x";
service.addEventListener("result",onGetServiceResult);
service.send();
}

private function onGetServiceResult(event:ResultEvent):void
{
var xml:XML = new XML(event.result);
var atomEvent:GetServiceEvent = new GetServiceEvent();

// Convert the XML to a domain object.
atomEvent.service = ModelFactory.createServiceFromXML(xml));

dispatchEvent(atomEvent);
}

// ...
}

This is the simplest problem, with a correspondingly simple solution. (I’ve left out the fault listener to keep this simplest of examples as simple as possible.) One thing to point out: we didn’t need to unregister the HTTPService event listener because we created a new instance and “threw it away”. But that won’t always be the case…

Problem #2: Asynchronous calls are made using an object that can be called from multiple places in the application. (Imagine that our app reuses one instance of HTTPService for all remote calls.)

Solution #2: For every event registration, there must be a corresponding “unregistration”. Failure to unregister an event listener could lead to it being called again if the event-generating object (HTTPService, in the following example) is called from somewhere else.

public class AtomPublishingProtocol extends EventDispatcher
{
private var service:HTTPService; // initialized elsewhere

// Dispatches a GetServiceEvent after retrieving the service document.
public function getService(serviceURI:URI):void
{
service.url = serviceURI.toString();
service.method = "GET";
service.resultFormat="e4x";
service.addEventListener("result",onGetServiceResult);
service.addEventListener(”fault”,onFault);
service.send();
}

private function onGetServiceResult(event:ResultEvent):void
{
// Manually unregister the listeners in case service
// is used from somewhere else.
service.removeEventListener(“result”,onGetServiceResult);
service.removeEventListener(“fault”,onFault);

var xml:XML = new XML(event.result);
var atomEvent:GetServiceEvent = new GetServiceEvent();

// Convert the XML to a domain object.
atomEvent.service = ModelFactory.createServiceFromXML(xml));

dispatchEvent(atomEvent);
}

private function onFault(event:FaultEvent):void
{
// Manually unregister the listeners here too!
service.removeEventListener(“result”,onGetServiceResult);
service.removeEventListener(“fault”,onFault);

// Do fault processing...
}

// ...
}

Problem #3: A method needs to perform multiple asynchronous calls before returning. Let’s make the use case a bit more complicated: we now use our AtomPublishingProtocol implementation to return the first feed listed in the service document.

Solution #3a: Chain the calls together manually.

public class FeedRetriever extends EventDispatcher
{
private var protocol:AtomPublishingProtocol; // initialized elsewhere

// Dispatches a GetFeedEvent after retrieving the first feed listed
// in the service document.
public function getFirstFeed(serviceURI:URI):void
{
// Manually set up the event listeners.
protocol.addEventListener(“getService”,onGetService);
protocol.addEventListener(“atomFault”,onGetServiceFault);

// Make the asynchronous call to retrieve the service document.
protocol.getService(serviceURI);
}

private function onGetService(event:GetServiceEvent):void
{
// Manually unregister the event listeners.
protocol.removeEventListener(”getService”,onGetService);
protocol.removeEventListener(”atomFault”,onGetServiceFault);

// Get the collection that represents the first feed.
var collection:ICollection = event.service.workspaces[0].collections[0];

// Manually set up the next set of event listeners.
protocol.addEventListener(“getFeed”,onGetFeed);
protocol.addEventListener(“atomFault”,onGetFeedFault);

// Make the asynchronous call to retrieve the first feed.
protocol.getFeed(collection);
}

private function onGetServiceFault(event:AtomFaultEvent):void
{
// Manually unregister the event listeners.
protocol.removeEventListener(”getService”,onGetService);
protocol.removeEventListener(”atomFault”,onGetServiceFault);

// Do fault processing...
}

private function onGetFeed(event:GetFeedEvent):void
{
// Ugh!  How many times do we have to manually
// add and remove event listeners?!?!?
protocol.removeEventListener(”getFeed”,onGetFeed);
protocol.removeEventListener(“atomFault”,onGetFeedFault);

// Dispatch a result event to the caller.
var newEvent:GetFeedEvent = new GetFeedEvent();
newEvent.feed = event.feed;
dispatchEvent(newEvent);
}

private function onGetFeedFault(event:AtomFaultEvent):void
{
// Manually unregister the event listeners.
protocol.removeEventListener(”getFeed”,onGetFeed);
protocol.removeEventListener(”atomFault”,onGetFeedFault);

// Do fault processing...
}

// ...
}

As you can see, it’s a real pain to manually handle registration and unregistration of event listeners. A helper class which can take care of the event registration for us will simplify our code.

Solution #3b: Use a helper class (e.g. AsyncResponder to manage event registration. (For reasons of space, I don’t include the source for the helper class, but you can find it in the Connections library, or in the NoteTag download.)

public class FeedRetriever extends EventDispatcher
{
private var protocol:AtomPublishingProtocol; // initialized elsewhere

// Dispatches a GetFeedEvent after retrieving the first feed listed
// in the service document.
public function getFirstFeed(serviceURI:URI):void
{
// Use a utility class to handle registration and unregistration
// of event listeners.
var responder:AsyncResponder = new AsyncResponder();
responder.setNextEventHandler(protocol,
onGetService, // result handler
“getService”, // result event
onFault,      // fault handler
“atomFault”); // fault event

// Make the asynchronous call to retrieve the service document.
protocol.getService(serviceURI);
}

private function onGetService(event:GetServiceEvent):void
{
// Get the collection that represents the first feed.
var collection:ICollection = event.service.workspaces[0].collections[0];

// Use a utility class to handle registration and unregistration
// of event listeners.
var responder:AsyncResponder = new AsyncResponder();
responder.setNextEventHandler(protocol,
onGetFeed,    // result handler
“getFeed”);   // result event
onFault,      // fault handler
“atomFault”); // fault event

// Make the asynchronous call to retrieve the first feed.
protocol.getFeed(collection);
}

private function onGetFeed(event:GetFeedEvent):void
{
// Dispatch a result event to the caller.
var newEvent:GetFeedEvent = new GetFeedEvent();
newEvent.feed = event.feed;
dispatchEvent(newEvent);
}

private function onFault(event:AtomFaultEvent):void
{
// Do fault processing...
}

// ...
}

This chaining of methods works quite well, and is reasonably easy for a reader to follow. (NoteTag uses this approach in a number of places, both explicitly with AsyncResponder and indirectly via the ChainedCommand class, which uses AsyncResponder to chain asynchronous method calls within Cairngorm Commands.)

In the next post, I’ll add another wrinkle to this problem: what if one method in the asynchronous method chain needs access to data that was generated earlier in the chain?