21 The Task Framework

The Task Framework is a general abstraction for asynchronous operations. It allows nesting / grouping of Tasks in TaskGroup instances for concurrent or sequential execution.

21.1 Introduction

In Flash or Flex applications there are usually a great number of asynchronous operations: Loading configuration files, loading SWFs or modules, loading images, loading and playing sounds, playing animations, invoking remote services and more. While each of these operations are quite easy to handle on their own, it can soon become quite messy if you have to combine lots of these operations. This is because most of them come with their own set of events or callbacks that designate the completion of the task or an error condition. So chaining them together for sequential execution for example requires to register numerous event listeners and usually the event listener method then has to deal with the flow of the application and start the next operation. As soon as you have to add a new operation in the middle of the flow you have to modify an existing listener method and move the invocation of the next operation to the listener method of the added task. Several years ago we had a project with a very complex application startup logic with dozens of operations, some of them optional, and it really became a maintenance nightmare. So the Task Framework was born. It was first created in AS2 and used as part of an inhouse framework for several years.

The following example code gives you an impression of how an application startup logic for example might look like when using the Task Framework:

var group:TaskGroup = new SequentialTaskGroup("some description for log output");
group.ignoreChildErrors = false;
group.addTask(new XmlConfigurationLoaderTask("config.xml"));
group.addTask(new SwfLoaderTask("graphics.swf"));
if (Environment.userLoggedIn) {
    group.addTask(new UserProfileRemoteServiceTask());
}
group.addTask(new IntroAnimationTask());
group.addEventListener(TaskEvent.COMPLETE, onAppStartComplete);
group.addEventListener(ErrorEvent.ERROR, onAppStartError);
group.start();

As you see, instead of dealing with events of the individual tasks, we only care for the events fired by the TaskGroup instance. It will fire its COMPLETE event when the final operation of the chain is complete. Alernatively it will fire an ERROR event if one of the child tasks dispatches an ERROR event and then stop its execution and discard the remaining tasks. We force this behaviour through setting the ignoreChildErrors property of the TaskGroup to false. Otherwise it would treat ERROR events of child tasks the same way as COMPLETE events.

All Task implementations that are added to the chain in the example above are not part of Spicelib, they are just contrived to illustrate the flow of actions with their class names. But future versions of Spicelib might add some builtin Task implementations for the most common asynchronous operations like loading files and images or playing sounds and animations. For an example of how to write your own Task implementation see 21.3 Implementing a custom Task. For more information on nesting and chaining tasks see 21.4 Using TaskGroups.

21.2 Task Events and States

Each Task supports a set of events, some of them related to optional features:

TaskEvent.START Fires when the Task is started, either by application code calling the public start method or when a TaskGroup starts own of its child tasks in which case you do not manually start the child task. The internal state of the task changes from TaskState.INACTIVE to TaskState.ACTIVE.
TaskEvent.COMPLETE Fires when the Task is completed, usually when the custom Task implementation calls the protected complete method in the abstract Task superclass. The internal state of the task changes to TaskState.INACTIVE (for restartable tasks) or to TaskState.FINISHED for non-restartable tasks.
TaskEvent.CANCEL Fires when the Task is cancelled, usually through its public cancel method. Can only occur if the cancelable property is set to true for that task. The internal state of the task changes to TaskState.INACTIVE (for restartable tasks) or to TaskState.FINISHED for non-restartable tasks.
ErrorEvent.ERROR Dispatched when an error occurred usually signalled by the custom Task implementation invoking the protected error method of the abstract Task superclass. The internal state of the task changes to TaskState.INACTIVE (for restartable tasks) or to TaskState.FINISHED for non-restartable tasks.
TaskEvent.SUSPEND Fires when a Task is suspended, usually through its public suspend method. Can only occur if the suspendable property is set to true for that task. The internal state of the task changes to TaskState.SUSPENDED.
TaskEvent.RESUME Fires when a Task is resumed, usually through its public resume method. Can only occur if the tasks state before the event was TaskState.SUSPENDED. The internal state of the task switches back to TaskState.ACTIVE.

The following state diagram shows the relationship between states and events:

21.3 Implementing a custom Task

To illustrate the steps required to implement a custom Task, we create a class that just loads and plays a sound. To keep the example simple the sound cannot be stopped or paused. This is the source for the example task:

package example {
	
import flash.events.ErrorEvent;
import flash.events.Event;
import flash.events.IOErrorEvent;
import flash.media.Sound;
import flash.media.SoundChannel;
import flash.net.URLRequest;
	
	
public class SoundTask extends Task {
	
    private var filename:String;

    function SoundTask (file:String) {
        super();
        filename = file;
        setCancelable(false);
        setSuspendable(false);
        setSkippable(false);	
    }
	
    protected override function doStart () : void {
        var sound:Sound = new Sound();
        sound.addEventListener(IOErrorEvent.IO_ERROR, onError);
        sound.load(new URLRequest(filename));
        var channel:SoundChannel = sound.play();	
        channel.addEventListener(Event.SOUND_COMPLETE, onComplete);	
    }
	
    private function onComplete (event:Event) : void {
        complete();
    }
	
    private function onError (event:ErrorEvent) : void {
        error("Error playing sound file " + filename + ": " + event.text);
    }
	
}

}

All Task implementations must extend the abstract Task class. In the constructor we first set some of the Task properties. Since we don't want to make this SoundTask cancelable or suspendable for now, we set those properties to false.

Next we override the protected doStart method, which will be invoked when the task is started. In most cases you should not override the public start method, since this deals with a lot of additional internal stuff (i.e. state management). The same pattern applies to other methods and events too. If we wanted this task to be cancelable we would override the protected doCancel method and not the public cancel method.

In our doStart implementation we set up the Sound and SoundChannel instances and add event listeners. We listen to the IOErrorEvent.IO_ERROR which will fire for error conditions like "file not found". In the listener method we invoke the protected error method which in turn leads to an ErrorEvent.ERROR to be fired. Likewise we register a listener for the Event.SOUND_COMPLETE and invoke the protected complete method in the listener method. This will then cause a TaskEvent.COMPLETE to be fired.

As you see one of the main requirements when implementing tasks is to map custom events to the generic TaskEvents to unify the event model for asynchronous operations. The simple class shown above is all you need to prepare a sound playback operation to be part of a TaskGroup. These classes are explained in the next section.

21.4 Using TaskGroups

For your custom Task implementations to be really useful, they have to be part of a TaskGroup. A SequentialTaskGroup chains tasks for sequential execution and fires its COMPLETE event when the final task has completed. A ConcurrentTaskGroup starts all child tasks concurrently and fires its COMPLETE event when all child tasks have completed. Both classes are subclasses of Task themselves (Composite Design Pattern), so they can be nested as well. This allows complex compositions of nested TaskGroups.

The properties of TaskGroups like cancelable or suspendable are only true if the collection itself and all of its child tasks have the property set to true:

var t:Task = new SoundTask("mySound.mp3"); // our example task is not cancelable
var group:TaskGroup = new SequentialTaskGroup();
group.addTask(t);
group.cancelable = true;
trace(group.cancelable); // output: false

In the example above setting the cancelable property of the chain instance only sets its own internal cancelable flag to true, but the getter method also takes the property values of all children into account.

Finally TaskGroups have a autoStart property. If this is set to true, an empty TaskGroup will start immediately when the first Task is added. This is useful for building execution queues where you want to avoid that a particular group of tasks runs concurrently but nevertheless want to start each of them as soon as possible.