Oct 9, 2008

Asynchronous execution of functions

Many a times in flex, we are faced with situations where we need to set some data that currently available to us in a component which has not yet been created! e.g. Say I am dynamically adding a VBox to a tab navigator, and that VBox contains a Label as child, I have a value that I want to set to the text of that Label but since that Label has not been created yet, I just cannot set it. To make it more clear look at the code snippet:
function someMethod() {
// somewhere deep down:
var str : String = "some value";
var vBox : MyVBox = new MyVBox();
tabNavigaor.addChild(vBox);
// this vBox contains a Label, whose text I want to set to 'str'.
}

since vBox has just been added, it has not been fully created and added to display list and hence Label also is not created yet. Infact if we try to access label, we will get null!

So, what's the solution?
One solution is that I define an inline (anonymous function) and attach it as event handler for 'creationComplete'. And in that functon I set the 'str' to Label. something like:

function someMethod() {
// somewhere deep down:
var str : String = "some value";
var vBox : MyVBox = new MyVBox();
tabNavigaor.addChild(vBox);
// this vBox contains a Label, whose text I want to set to 'str'.
vBox.addEventListener('creationComplete' function(e : Event) : void {
e.target['myLabel'].text = str;
}
}

This will solve the problem. But I have always been a bit finicky about 'inlined' functions. I mean what if the value of 'str' changes before the creationComplete has been fired?

Java solves this problem (in case of anonymous inner classes) by forcing that variable to be final so that it cannot be changed, but I dont see such a thing happening for flex.

Therefore, to ensure that this does not happen, we can use the following solution:
1. Define a normal function which has the same code as the anonymous function.
2. Write a AsyncFunction class which 'queues' the exection of a function and its params for later.
3. The AsyncFunction listens for some trigger (in this case 'creationComplete' and then dequeues the queued method and its params and executes them.

Following is the code for AsyncFunction :

package modules.model {
import flash.utils.Dictionary;
import mx.core.UIComponent;
import mx.utils.UIDUtil;
import flash.events.Event;

public class AsyncFunction {

private static const dict : Dictionary = new Dictionary();

public static function callLater(method : Function, triggerEventName : String, 
triggerComponent : UIComponent, ... args) : void {
triggerComponent.addEventListener(triggerEventName,asyncHandler);
var uid : String = UIDUtil.createUID();
triggerComponent.setStyle("___$uid",uid);
dict[uid] = {f : method, params : args};
}

private static function asyncHandler(triggerEvent : Event) : void {
var uid : String = triggerEvent.target.getStyle('___$uid');
if(dict.hasOwnProperty(uid)) {
var method : Function = dict[uid].f as Function;
method.apply(triggerEvent.target,dict[uid].params); 
// rollback:
triggerEvent.target.removeEventListener(triggerEvent.type,asyncHandler);
triggerEvent.target.setStyle('___$uid',null);
delete dict[uid];
}
}

}
}

Then, in our current situation just do this:
function someMethod() {
// somewhere deep down:
var str : String = "some value";
var vBox : MyVBox = new MyVBox();
tabNavigaor.addChild(vBox);
// this vBox contains a Label, whose text I want to set to 'str'.
AsyncFunction.callLater(setUpLabel, 'creationComplete', vBox, vBox, str);
}

private function setUpLabel(vBox : MyVBox, str : String) : void {
vBox['myLabel'].text = str;
}

That's it! Just one line of code : AsyncFunction.callLater(setUpLabel, 'creationComplete', vBox, vBox, str);
and you are guaranteed that your code will execute fine.

No comments:

Post a Comment