AsyncResult — making async in ActionScript suck less
October 09, 2010 at 09:47 PM | ActionScript | View CommentsLet's face it: the tools Adobe provides for dealing with asynchronous operations in ActionScript are basically crap. We've got AsyncToken, which seems pretty good... Until you try and use it yourself, and realize that there's no way to inject your own result. And then we've got the IResponder interface, which also seems pretty good... Except that sometimes your responder gets a ResultEvent and other times it gets the actual result. And then there are the half million events you can listen for (and remember to remove your event listeners for!). And... Well, if you've spent any time in ActionScript, I don't need to say any more.
But I'm not (usually) one to complain without offering a solution.
So let me present: AsyncResult.
In the simplest case - only handling a result / error - it looks something like this:
UserService.loadUser(userID).complete(function(user:User):void {
// ... do stuff with the User ...
}, function(error:AsyncError):void {
// ... handle the error ...
});
But that's not what makes AsyncResult cool.
Imagine, for a minute, what that UserService.loadUser
function would look
like. Maybe it fires off a request to a server which sends back some JSON,
which is then loaded into an instance of User then returned. Pretty
straightforward... Except it probably takes a small army of event listeners,
callbacks and error handlers (which all need to be documented and tested and
maintained and...).
This is the kind of situation which AsyncResult was designed for. Check it out:
public function loadUser(userID:String):AsyncResult {
var token:AsyncToken;
var result:AsyncResult = new AsyncResult();
// (HTTP transaction intentionally simplified)
token = HTTPService.sendGET("http://.../users/" + userID);
// The AsyncToken is plugged into the Flex AsyncToken
// through 'getResponder()'
token.addResponder(result.getResponder());
// This is where the magic happens:
result.complete(function(jsonData:String):User {
var userData:Object = JSON.loads(jsonData);
var user:User = User.loadFromObject(userData);
return user;
});
return result;
}
Two things to notice about the above code: first, the complete
callback
accepts JSON data (not an event) and, more importantly, returns an instance of
User
. Second, no error handling is done.
This is possible because AsyncToken chains all of its handlers. When a result
(or error) is received, it is passed to the first handler. If that handler
returns a new value (that is, something other than undefined
), that new value
is passed to the next handler, and so on.
For example, this is what happens if the complete handlers add1
and add2
are used:
>>> function add1(x) { return x + 1};
>>> function add2(x) { return x + 2};
>>> function printx(x) { trace("got:", x); };
>>> r = new AsyncResult();
>>> r.complete(printx);
>>> r.complete(add1);
>>> r.complete(printx);
>>> r.complete(add2);
>>> r.complete(printx);
>>> r.gotResult(0)
got: 0
got: 1
got: 3
>>>
In addition, if any of the handlers return an AsyncResult, the original AsyncResult will wait for the new AsyncResult to complete, then pass the new result to the next handler.
For example, going back to the User
example, imagine that we need a (slightly
contrived) loadFavoritesForUserID(userID:String)
function. It could look
something like this:
function loadFavoritesForUserID(userID:String):AsyncResult {
var result:AsyncResult = UserService.loadUser(userID);
result.complete(function(user:User):AsyncResult {
var token:AsyncToken;
token = = HTTPService.httpGET(user.getFavoritesURL());
token.addResponder(result.getResponder());
return result;
});
result.complete(function(jsonData:String):Array {
return JSON.loads(jsonData);
});
return result;
}
Well, that's a short introduction to my awesome AsyncResult. If you're interested, check out the code and hit me up on Twitter, @wolever. And if you're not interested, let me know why in the comments.
A Parameterized Testrunner for FlexUnit
February 10, 2010 at 10:54 AM | ActionScript | View CommentsA couple of days ago, I complained about FlexUnit's Theories. Well, with a bit of encouragement from @drewbourne, I broke down and wrote a proper parameterized testrunner:
[RunWith("utils.testrunners.ParameterizedRunner")]
class AdditionTests {
public static var numbersToTest:Array = [
[1, 2, 3],
[4, 5, 9],
[-1, 1, 0]
};
[Parameterized("numbersToTest")]
public function testAddition(a:int, b:int, expected:int):void {
assertEqual(a+b, expected);
}
}
And that code will do exactly what you'd expect: run three test cases, one for each of the inputs. If one test fails, the others will still run. Helpful error messages will be provided when a test fails.
The source can be downloaded from: http://gist.github.com/299871 (and it will be getting a new home when ever I get around to releasing all of my AS utilities)
Look useful? Give it a try and tell me what you think – I'd love to know.
Update
Since this article was written, parameterized tests have been added to FlexUnit core. The documentation is over on the FlexUnit wiki: http://docs.flexunit.org/index.php?title=Parameterized_Test_Styles
And this is the above test, using the FlexUnit syntax:
[RunWith("org.flexunit.runners.Parameterized")]
public class AdditionTests {
public static var numbersToTest:Array = [
[1, 2, 3],
[4, 5, 9],
[-1, 1, 0]
};
[Test(dataProvider="numbersToTest")]
public function testAddition(a:int, b:int, expected:int):void {
assertEqual(a+b, expected);
}
}
I've just been playing around with FlexUnit 4's nifty new "Test Theories"… And I have come to the conclusion that, right now, they are basically worthless.
Here's why:
- They aren't documented. At all. There are approximately two examples on the internet, and the wiki page is a joke. Nothing explains what algorithm is used to calculate which DataPoints are applied to which theories.
- They make test error messages less helpful. If a a theory fails, instead of getting a helpful error message like "Theory `checkUrl("this is a bad url")` failed with message: could not determine protocol", it gives a message like this: "checkUrl urls[1]". Yea, thanks guys.
- They don't do anything "cool". At all. It would be cool if, given five data points, five distinct tests would be generated. It would be cool if one data point could fail while others succeed. Heck, anything which would make them better than a for loop would be cool. But, alas…
So, don't waste your time on Theories just yet. For now, just use a for
loop:
[Test]
public function runTestsOnData():void {
for each (datum in testData)
doSomeTest(datum);
}
Or, if you want to test the cartesian product of a set of data, use my handy cartesian product function:
[Test]
public function runTestsOnCartesianProduct():void {
for each (data in cartesian_product(testData0, testData1))
doSomeTest.apply(this, data);
}
Followup: as Alan (see comments) said, what I really want is a parameterized testrunner. So I've gone ahead and written one. See my post on a Parameterized Testrunner for FlexUnit.
(alternate title: The "Fail Early, Fail Often" Principle in Action)
I would like to award Adobe the "best ever implementation of indexOf
". Ready for it? Here it is:
override flash_proxy function callProperty(name:*, ... rest):* {
return null;
}
This beauty can be found in mx.collections.ListCollectionView
.
And the kicker? The function's documentation:
/**
* @private
* Any methods that can't be found on this class shouldn't be called,
* so return null
*/
The author clearly knew that unknown methods* shouldn't be called… But instead of doing something sensible – like throwing an exception – they do something wholly nonsensical and return null
. Or, of course, they could also have left the function unimplemented, which would result in a strange and unhelpful exception… But, of course, I would expect nothing but the best from the Flex standard library.
Next up: why implicit conversions from null
to int
are always wrong.
</rant>
*: the magic callProperty
method is called on subclasses of Proxy
when a method can't be found. For example if bar
is not a method of foo
, executing foo.bar()
will call foo.callProperty("bar")
.
PS: Instead of calling indexOf
, I should have been calling getItemIndex
.
Making Flex's PopUpMenuButton More Helpful
August 25, 2009 at 06:09 PM | ActionScript | View CommentsIf you're anything like me, you cringe every time you think about implementing a popup menu in Flex: the options, PopUpButton
and PopUpMenuButton
, are both terrible.
For example, PopUpButton
makes you jump through the hoops of creating a Menu
and PopUpMenuButton
forces you to create an event handler just to get the currently selected item.
Well, cringe no more! I've done a bit of hacking on the PopUpMenuButton
to make the most common use-case, "showing a list of options", a little bit less painful by adding a 'selectedItem' field:
<mx:XML id="colors"> <colors> <color name="Red" rgb="0xFF0000" /> <color name="Green" rgb="0x00FF00" /> <color name="Blue" rgb="0x0000FF" /> </colors> </mx:XML> <HelpfulPopupMenu id="colorMenu" dataProvider="{colors}" labelField="@name" showRoot="false" /> <mx:Label color="{XML(colorMenu.selectedItem).@rgb}" text="You've chosen: {XML(colorMenu.selectedItem).@name}" />
Which can also be used to set a temporary default option:
<mx:Script><![CDATA[ var people:Array = [ { name: "Bob", age: 16 }, { name: "Alice", age: 42 }, { name: "Jo", age: 31 } ]; ]]></mx:Script> <HelpfulPopupMenu id="peopleMenu" dataProvider="{people}" labelField="name" selectedItem="{ { name: '(select a person)', age: null } }" showRoot="false" /> <mx:Label visible="{ peopleMenu.selectedItem.age !== null }" text="That person is { peopleMenu.selectedItem.age } years old." />
Useful? I'd like to hope so.
You can see a demo by clicking the image below (sorry, I don't actually know the first thing about embedding SWFs into HTML, and I don't feel up to learning right now):
And you can download the source (which is MIT licensed) from more or less the same place: http://wolever.net/~wolever/HelpfulPopupMenu/.
Also, this is a small part of a larger library of helpful ActionScript utilities which I'm going to be releasing… At some point.