Archive for the 'Cairngorm' Category
Dynamically Assigned Event Listeners (Gotcha)
Do you ever come across a problem and wonder how you've managed not to stumble upon it before?! This is one such case ...
Today I wanted to recurse over a hierarchical structure (in my case, to start with a class and find all the classes that it extends) and then create a button representing each class. The recursion wasn't an issue, but assigning event listeners was.
Take a look at the following example which highlights the issue:
Here is the code for the above example:
-
private function cc():void
-
{
-
var classes:Array = new Array("MovieClip", "Sprite", "Object"); //fake the data
-
-
/*
-
NOTE: Labels assigned correctly but Alert displays wrong item (the last one)
-
- because listener is passed by reference
-
*/
-
for each (var item:String in classes)
-
{
-
var b:Button = new Button();
-
b.label = item;
-
var func:Function = function(e:Event):void{ Alert.show(b.label, "You clicked:"); }
-
b.addEventListener(MouseEvent.CLICK, func);
-
addChild(b);
-
}
-
}
As you can see, every button has the correct label (assigned within the loop), but every button also traces the same thing - "Object" even though it was also assigned in the same loop. How can that happen? This is a sure sign that something is being passed by reference, not value.
The issue here is that when assigning an event listener, the second argument func is indeed passed by reference, and so it will always point to the last anonymous function created ... and in our case that is the one that traces "Object":
-
b.addEventListener( MouseEvent.CLICK, func ); //func is passed by reference
Most people will tell you that you can't pass the value in to a method like this either:
-
b.addEventListener( MouseEvent.CLICK, func(b.label) ); //try to pass arg to func - fail
However, I found one relatively simple way to achieve what I wanted, and the addEventListener code looks exactly the same as above, except there is a subtle difference in what you expect to happen, because func() returns a class of type Function.
The trick is to make func in to a call to a method which returns an anonymous function. Here is the example. As you can see, each button now traces the correct value:
func is still a reference, but it is a reference to a method which returns a unique anonymous function. The code is as follows.
-
private function cc():void
-
{
-
var classes:Array = new Array("MovieClip", "Sprite", "EventDispatcher"); //fake the data
-
-
/*
-
NOTE: Labels assigned correctly AND Alert displays correct item!
-
- because func returns a unique method for each button
-
*/
-
for each (var item:String in classes)
-
{
-
var b:Button = new Button();
-
b.label = item;
-
b.addEventListener(MouseEvent.CLICK, func(b.label));
-
addChild(b);
-
}
-
}
-
-
private function func (link:String):Function
-
{
-
return function(mouseEvent:MouseEvent):void
-
{
-
Alert.show(link, "You clicked:");
-
};
-
}
Note: The use of anonymous functions is not an issue as I don't ever wish to remove the listeners.
PureMVC Cairngorm WebOrb
I met Jens at this years 360|Flex Europe event in Milan;
Some time ago I had taken Alex Uhlmann's 'Cairngorm Login' example and used it as the basis for my 'Cairngorm For Beginners' series of tutorials.
Jens then took the end product (which was simply an updated version of the original) and used it to write his own tutorial demonstrating how to get the login example running with [ the PHP version of ] WebOrb (an open source technology for Flex-PHP Remoting)
Then he took it one step further and used that to write another tutorial showing how to convert the php login example from Cairngorm to pureMVC.
Well, I've been meaning to get around to learning both remoting and pureMVC, and as I'm familiar with the login example this is a great place to start. I took a look at the first of the two tutorials this evening and it served as a good intro to WebOrb/remoting. Tomorrow I'll follow on with the pureMVC example. Thanks Jens, thanks Alex.
I love the way the web works.
6 commentsCreating Classes Dynamically By Name
On occasion you may want to create classes dynamically by name. To do this in Flex 3 you can use getDefinitionByName (it's previous incarnation was getClassByName). Here's a quick example:
-
//cc() is called upon creationComplete
-
private function cc():void
-
{
-
var obj:Object = createInstance("flash.display.Sprite");
-
}
-
-
public function createInstance(className:String):Object
-
{
-
var myClass:Class = getDefinitionByName(className) as Class;
-
var instance:Object = new myClass();
-
return instance;
-
}
The docs for getDefinitionByName say:
"Returns a reference to the class object of the class specified by the name parameter."
...so you may be wondering why in the above code we needed to specify the return value as a Class? This is because getDefinitionByName can also return a Function (e.g. 'flash.utils.getTimer' - a package level function that isn't in any class). As the return type can be either a Function or a Class the Flex team specified the return type to be Object and you are expected to perform a cast as necessary.
The above code closely mimics the example given in the docs, but in one way it is a bad example because everything will work fine for "flash.display.Sprite", but try to do the same thing with a custom class and you will probably end up with the following error:
ReferenceError: Error #1065: Variable [name of your class] is not defined.
The reason for the error is that you must have a reference to your class in your code - e.g. you need to create a variable and specify it's type like so:
-
private var forCompiler:SomeClass;
Without doing this your class will not be compiled in to the .swf at compile time - and there is reason behind this madness. The compiler only includes classes which are actually used (and not just imported). It does so in order to optimise the size of the .swf. So the need to declare a variable should not really be considered an oversight or bug, although it does feel hackish to declare a variable that you don't directly use.
Here is the code your would need to create a custom class called Person from a string (where Person resides in the top level package along with your default application).
-
//cc() is called upon creationComplete
-
private var forCompiler:Person; //REQUIRED! (but otherwise not used)
-
-
private function cc():void
-
{
-
var obj:Object = createInstance("Person");
-
}
-
-
public function createInstance(className:String):Object
-
{
-
var myClass:Class = getDefinitionByName(className) as Class;
-
var instance:Object = new myClass();
-
return instance;
-
}
You can declare properties for all possible classes that you intend to create but if you are not happy with doing that inside your main app Alex Harui has another suggestion (something I've not personally tried yet):
"The code for the class has to be in a SWF. It can be in the main SWF or
loaded via a module later. There is a compiler option (-includes) that
allows you to stuff other classes into a SWF without explicitly naming
them in your source code."
Passing in Parameters
Another thing you may find yourself wanting to do is to pass parameters to the constructor of your class (indeed, this is what got me on to the subject today). As far as I'm aware this isn't possible to do this in AS3 (and isn't part of the ECMA spec so it is unlikely to change - please leave a comment or email if you know otherwise ), so you may want to set up another method which can accept and set multiple parameters . If you pass in an array you may stumble across another problem - the original array you passed in becomes the first item of a new array ... okay, that probably sounds confusing so see here for a similar example and solution. In short, the solution is to use Function.apply, and you can see how I used it below:
-
private var forCompiler:Person;
-
-
private function cc():void
-
{
-
var obj:Object = createInstance("Person", ["bob", 30]);
-
}
-
-
public function createInstance(className:String, args:Array):Object
-
{
-
var myClass:Class = getDefinitionByName(className) as Class;
-
var instance:Object = new myClass();
-
instance.initArgs.apply(null, args);
-
return instance;
-
}
-
-
/*********************************************************************
-
//In person.initArgs()....
-
//Note: the constructor also accepts name & age, assigning them default values
-
//if not specified.
-
public function initArgs(name:String, age:uint):void
-
{
-
this.name = name;
-
this.age = age;
-
}
-
*********************************************************************/
Now, I'm not sure if this is the best approach, but it works, and with a deadline approaching that's good enough for now :]
6 comments