Handling scope issues and keeping the compiler happy in AS 2.0 classes has received a lot of attention on mailing lists and so forth in the last month or two. The following is some observations and results in trying to figure out the best methods of handling certain situations. I am by no means an OOP expert, so there might be some technical details that aren’t quite right here, but overall my final solution works, is clean and the compiler loves it!
One of the most confusing issues with AS 2.0 classes is when you declare some object as a member of the class and then you need to assign a callback or listener for that object. Perhaps the most common example is where your class needs to load some XML. Loading XML on a timeline is pretty straightforward. You create an XML object, probably set ignoreWhite to true, assign an onLoad handler, and call the load method:
myXML = new XML();
myXML.ignoreWhite = true;
myXML.onLoad = function(success) {
if (success) {
trace(this.firstChild);
}
};
myXML.load("some.xml");
When you start using AS 2.0 classes, it is tempting to do pretty much the same thing. Something like this:
class MyClass extends MovieClip {
private var myXML:XML;
function MyClass() {
myXML = new XML();
myXML.ignoreWhite = true;
myXML.onLoad = function(success) {
if (success) {
trace(this.firstChild);
}
};
myXML.load("some.xml");
}
}
And this pretty much works as written. But you probably want to do something with the loaded XML rather than just trace it. Probably you want to process and display it in the movie clip/component you are building. Say you just want to display the XML in a Text Field that is on the stage. Here’s the first problem. The XML object has no direct reference to the clip it is within. So you have no way of addressing this Text Field from within the onLoad function. Say you have a Text Field named “myText” in the movie clip. You declare it as a member of the class and try to assign the loaded XML to it:
class MyClass extends MovieClip {
private var myXML:XML;
private var myText:TextField;
function MyClass() {
myXML = new XML();
myXML.ignoreWhite = true;
myXML.onLoad = function(success) {
if (success) {
myText.text = this.firstChild;
}
};
myXML.load("some.xml");
}
}
Nothing happens. A lot of people mistakenly try to use _parent here, thinking the XML’s _parent is the movie clip holding it:
this._parent.myText.text = this.firstChild;
But the XML object does not have a _parent property. That is only for movie clips. The next obvious solution is to create a “parent”, or “mc” or “holder” property on the XML object, and assign a reference to the holding object with “this”, like so:
myXML.parent = this;
Then in the onLoad handler, you have a reference to the “parent” object and can reference it like so:
this.parent.myText.text = this.firstChild;
From the viewpoint of paths and scopes and so forth, that’s valid, but the compiler is going to complain, because the XML object does not have a property called “parent”. With AS 2.0’s strict typing, the compiler knows this and doesn’t let you just tack on any property to a class like that.
There are a few tricks to get around this. One is to declare myXML as type “Object” rather than “XML”, or not giving it a type at all. But that just bypasses any and all benefits of having strict typing at all, and is kind of drastic.
Another idea is to cast myXML as an object in order to assign it a property by saying:
Object(myXML).parent = this;
This works, but feels like cheating. Another cheat is to use array notation to assign variables:
myXML["parent"] = this;
Again, these work, but you are really just using some tricks to bypass the protection of classes. A more valid method is to declare a local variable in the class method. This will then be available inside the onLoad method. Something like this:
class MyClass extends MovieClip {
private var myXML:XML;
private var myText:TextField;
function MyClass() {
myXML = new XML();
myXML.ignoreWhite = true;
var parent = this;
myXML.onLoad = function(success) {
if (success) {
parent.myText.text = this.firstChild;
}
};
myXML.load("some.xml");
}
}
The local variable “parent” is available inside onLoad and still refers to the object. Note that this must be a local variable. If you make a class member variable, outside any function, and then assign “this” to it, it won’t be available within onLoad.
This is the best method yet, but I still don’t like the idea of defining a function (onLoad) inside another function, particularly if your function starts getting long, as XML parsing functions can. Normally, I would just assign a function name to onLoad and define the function later. In timeline programming this is no problem. But here’s how it looks when you transfer it to a class:
class MyClass extends MovieClip {
private var myXML:XML;
private var myText:TextField;
function MyClass() {
myXML = new XML();
myXML.ignoreWhite = true;
var parent = this;
myXML.onLoad = myOnLoad;
myXML.load("some.xml");
}
function myOnLoad(success) {
if (success) {
parent.myText.text = this.firstChild;
}
}
}
Now, if you run this, the compiler is going to tell you that there is no property with the name “parent”. Since the onLoad handler is outside the scope of the function with the local variable “parent”, it is lost. So, we use one of our cheats, say casting myXML as an object, and then in myOnLoad we say:
this.parent.myText.text = this.firstChild;
Again, the compiler complains! Because now, myOnLoad is actually a method of MyClass, and MyClass does not have a property called “parent”. The compiler doesn’t know that this method is going to be attached to another object. So again we have to trick it! We can say:
this["parent"].myText.text = this.firstChild;
Or, we create a dummy class property named “parent”, which never gets used by the class, but pacifies the compiler.
Even if we go ahead and do one of these things, again the compiler is going to complain! Because MyClass does not have a property named “firstChild”. We can continue tricking the compiler like this, making a dummy property for every property of the XML class that we will use, or access every variable with array notation, or casting everything as some other type. We are going down a very ugly coding path, though. Time to backtrack and look for a better route.
The main problem with the above code is that we are creating a class method and not using it as a class method, but as a callback. So one decent way of handling it is to make a brief inline callback which then calls the class method. This looks something like this:
class MyClass extends MovieClip {
private var myXML:XML;
private var myText:TextField;
function MyClass() {
myXML = new XML();
myXML.ignoreWhite = true;
var parent = this;
myXML.onLoad = function(success) {
if (success) {
parent.myOnLoad(this.firstChild);
}
};
myXML.load("some.xml");
}
function myOnLoad(xmlData) {
myText.text = xmlData;
}
}
Here we go back to the local variable, “parent” containing a reference to “this”. The onLoad function then calls parent’s myOnLoad function, passing it the loaded XML. Since myOnLoad is a method of MyClass, it has direct access to the other members, such as the Text Field, myText, and can assign the XML data to it.
This is the cleanest solution yet, but it can still be abstracted some more.
I knew all during investigating this that the proper handling was probably to subclass the XML class to give it the properties and behaviors it needs to work well within a class setting. But this seemed like a lot of work to go through just to load some XML – a function which is pretty straightforward on the timeline.
However, once I sat down and did it, it proved pretty painless, and this subclass is now stored in my class path available to use whenever I need it in any project. It winds up being far less work in the long run than all these cheats.
The idea is that since an XML object does not contain a property to reference the object that holds it, we extend the XML class to give it such a property. The class will have all the built in properties of XML, plus this new one.
Here is the class I came up with:
class com.bit101.XMLMember extends XML {
private var loadHandler:String;
private var parent:Object;
public function XMLMember(p:Object, lh:String) {
parent = p;
loadHandler = lh;
}
private function onLoad(success) {
parent[loadHandler](this, success);
}
}
You simply save it in whatever class path you like. I used com.bit101, where I put all my classes. Just change the class definition in the first line to reflect the actual path.
The class adds two private properties to the XML class, “parent” and “loadHandler”. Parent is the containing object, and loadHandler is the string name of a function in parent which you want to call.
These are passed to the object in the constructor. When the object has loaded the XML file, the onLoad function fires. This is defined in the class, and calls the loadHandler method of the parent object, sending it the XML data and the success variable. Here is how it is used inside a class:
class MyClass extends MovieClip {
private var myXML:XML;
private var myText:TextField;
function MyClass() {
myXML = new com.bit101.XMLMember(this, "myOnLoad");
myXML.ignoreWhite = true;
myXML.load("some.xml");
}
function myOnLoad(xmlData) {
myText.text = xmlData;
}
}
As you can see, this is the cleanest, simplest code yet. The scopes are all correct, no tricks, and the compiler is very happy with it. I’m sure the implementation of XMLMember could be improved, but I like its simplicity. When I get back to work Monday, I’m going to see how many lines of code I can delete by using this idea.