Working with asfunction in AS2 Class Files

ActionScript 2.0

Flash Player has supported a limited subset of the HTML specification since version 6 — just set a text field’s htmlText property to an HTML-formatted string and you’re good to go.  Fortunately, <a> (anchor) tags are among the supported few, which means you can even put working hyperlinks inside your text.  Not only that, but Flash includes a special protocol, asfunction, that allows you to trigger functions from those hyperlinks, in case you prefer to do that instead of visiting URLs.  ActionScript 3.0 uses a different approach, but if you’re coding in AS1 or 2, just replace http://someURL.com with asfunction:someFunction,someParam, as described elsewhere on this blog.  If you’re coding in timeline keyframes, it’s all pretty straightforward.  But asfunction can seemingly break when used in custom class files.  Here’s what’s going on and how to fix it. 

A bit of backstory

When a text field responds to an asfunction trigger, it looks for the specified function inside the movie clip that contains the text field in question (that is, the container object of that text field, or its parent).  Again, if you’re coding on the timeline, everything tends to fall into place.  Imagine the following ActionScript 2.0 in a keyframe of the main timeline:

function linkHandler(param:String):Void {
  // do something neat with the hyperlink
}

myTextField.htmlText = "<a href='asfunction:linkHandler,param'>click me</a>";

When a user clicks the words “click me” in this text field, asfunction looks for a function named linkHandler() in the movie clip (aka the timeline) that contains this text field.  And of course, there it is, in the very same keyframe.

In some custom class situations, the above concept still works without a hitch.  If your class extends MovieClip, for example, then the class itself becomes the “timeline” that contains both the text field and asfunction’s function.

class CustomClass extends MovieClip {
  private var tf:TextField;
  public function CustomClass() {
    tf = this.createTextField("tf", 0, 0, 0, 100, 22);
    tf.html = true;
    tf.htmlText = "<a href='asfunction:linkHandler,clicked'>click me</a>";
  }
  private function linkHandler(param:String):Void {
    trace(param);
  }
}

In the preceding code, an arbitrarily named property, tf, is declared and — inside the constructor function — set to an instance of the TextField class by way of the MovieClip.createTextField() method.  This method succeeds here when invoked on the global this property because the class extends MovieClip (i.e., it is a movie clip).  In the following line, tf’s TextField.html property is set to true, which allows it to accept HTML input, then its htmlText property is set to the same HTML-formatted string as before.  Note that asfunction still points to a custom linkHandler() function, this time passing in the parameter “clicked” (a string).  The private linkHandler() method (which could be public too, either way) hears asfunction’s call when the hyperlink is clicked.  All is still well — the Output panel traces the word “clicked” because param’s value is that word.

Note:  To test the above class, you’ll have to save the preceding code as a simple text file named CustomClass.as and put it into the same folder as a new FLA.  Inside that FLA, draw a quick rectangle and convert it to a movie clip symbol.  Right-click / Command-click the symbol in the Library and choose Linkage from the context menu.  Check “Export for ActionScript” and type the name of the class (CustomClass) into the Class field.

So what’s the problem?

The problem is, your class might not extend MovieClip (or extend anything!) at all.  Which would be fine.  There’s no reason to extend MovieClip, or any class, unless the class you’re writing actually is an example of the class you’re extending.  In the following code — almost identical — asfunction is no longer able to find its linkHandler() function:

class CustomClass {
  private var tf:TextField;
  public function CustomClass(target:MovieClip) {
    tf = target.createTextField("tf", 0, 0, 0, 100, 22);
    tf.html = true;
    tf.htmlText = "<a href='asfunction:linkHandler,clicked'>click me</a>";
  }
  private function linkHandler(param:String):Void {
    trace(param);
  }
}

What’s the difference?  This time, the class doesn’t extend anything.  It’s instantiated in the main timeline …

new CustomClass(this);

… and fed a MovieClip instance — here, the global this property — as a parameter.  The only reason for the parameter is so that the target reference inside the constructor function can be used in the very next line as the MovieClip instance required for the createTextField() method.  The difference may be subtle, but when this class is instantiated, the text field becomes an immediate child of the main timeline.  The problem is, the class itself — which contains the linkHandler() method — is also a child of the main timeline, which means asfunction is looking in the wrong place for linkHandler().  As always, asfunction is looking in the movie clip that contains the text field.  Here, the movie clip is the main timeline, but the desired function is inside an object (this CustomClass instance) inside the main timeline.  So close, but yet so far, eh?

So what’s a solution?

The easiest way to fix the disconnect is to re-route the function, which can be done with a single line.  Here’s a quick look at the constructor function only:

public function CustomClass(target:MovieClip) {
  tf = target.createTextField("tf", 0, 0, 0, 100, 22);
  tf.html = true;
  tf.htmlText = "<a href='asfunction:linkHandler,clicked'>click me</a>";
  tf._parent.linkHandler = this.linkHandler;
}

The workaround here takes advantage of the fact that the MovieClip class is dynamic, which means it can have properties and methods added to it at runtime.  In this case, a reference is made to the movie clip that contains the text field — tf._parent (the TextField._parent property of tf) — and then a new function is declared in that movie clip and set to the function defined inside the class.  Because the constructor function accepts a target parameter, you could also use the following in this scenario, which means the exact same thing:

target.linkHandler = this.linkHandler;

It’s interesting to note that linkHandler(), as defined in the class, is private, yet the twin function, dynamically set to a different MovieClip instance, is able to invoke this private method without any issues.  That’s just the broken way it works in AS2.  It shouldn’t do that, but hey, you’re a developer.  That makes you like a stage hand.  You keep the magician’s secrets.

Is there a better solution?

There is a better solution — though “better” is subjective, and you really only need the alternate approach if the scope of your function needs to reside inside the class.  Under the current setup, if you replace trace(param); with trace(this);, you’ll see _level0 in the Output panel.  Why?  Because the dynamically added function exists in the main timeline, so the this reference thinks that’s where it is.  (It only happens like this because the parent of the text field happens to be the main timeline.  The point, however, is that this refers to an object that isn’t the CustomClass instance.)

To re-route not only the function, but its scope, use the Delegate class as follows:

import mx.utils.Delegate;
class CustomClass {
  private var tf:TextField;
  public function CustomClass(target:MovieClip) {
    tf = target.createTextField("tf", 0, 0, 0, 100, 22);
    tf.html = true;
    tf.htmlText = "<a href='asfunction:linkHandler,clicked'>click me</a>";
    tf._parent.linkHandler = Delegate.create(this, linkHandler);
  }
  private function linkHandler(param:String):Void {
    trace(param);
  }
}

Leave a Reply