20 February 2008

Of Document Classes and Timeline Code

My coworker Ezra and I just got through figuring out a bizarre bug in an application created with Flash CS3 (using ActionScript 3.0). Since I didn't see anything else about it online, I thought I'd post it here.

The Set-Up: A loader SWF loads in section SWFs. The section SWFs have timeline code that trigger events based on timeline animations.

The Problem: On their own, the section SWFs worked fine. But once loaded into the loader SWF, only their class code would execute; none of the timeline code would execute, not even traces.

The Really Strange Wrinkle: I created a minimal proof-of-concept test, and it worked fine. It even worked when I used the section SWF from the actual project. So the problem seemed to be in the loader (but it really wasn't).

Here's the code from the minimal proof-of-concept test. Here's the loader class:

package timelinetest {
import flash.display.Loader;
import flash.display.LoaderInfo;
import flash.display.Sprite;
import flash.errors.IOError;
import flash.events.ErrorEvent;
import flash.events.Event;
import flash.events.IOErrorEvent;
import flash.events.SecurityErrorEvent;
import flash.net.URLRequest;
public class TestLoader extends Sprite {
public function TestLoader() {
super();
addEventListener(Event.ADDED_TO_STAGE, loadContent);
}
private function loadContent(event:Event):void {
var loader:Loader = new Loader();
var info:LoaderInfo = loader.contentLoaderInfo;
info.addEventListener(IOErrorEvent.IO_ERROR, onContentIOError);
info.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onContentSecurityError);
info.addEventListener(Event.INIT, onContentInit);
loader.load(new URLRequest("./loaded.swf"));
}
private function onContentInit(event:Event):void {
var info:LoaderInfo = event.target as LoaderInfo;
addChild(info.content);
}
private function onContentIOError(event:IOErrorEvent):void {
throw new IOError(event.text);
}
private function onContentSecurityError(event:SecurityErrorEvent):void {
throw new SecurityError(event.text);
}
}
}


... and here's the section class:

package timelinetest {
import flash.display.MovieClip;
import flash.events.Event;
public class TestLoaded extends MovieClip {
public function TestLoaded() {
super();
stop();
addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
}
private function onAddedToStage(event:Event):void {
play();
}
public function onSpecialFrameReached():void {
trace("special frame reached");
stop();
}
}
}


... and on frame 10 of the root timeline in loaded.swf I had this code:

trace("calling onSpecialFrameReached()");
onSpecialFrameReached();


So on frame 10, it called onSpecialFrameReached(), which stopped playback. This worked fine. But similar code did not work fine in the actual project.

So What Was the Discrepancy?: Making the following modifications (underlined) to TestLoader.as caused the timeline code failure:

package timelinetest {
import flash.display.Loader;
import flash.display.LoaderInfo;
import flash.display.Sprite;
import flash.errors.IOError;
import flash.events.ErrorEvent;
import flash.events.Event;
import flash.events.IOErrorEvent;
import flash.events.SecurityErrorEvent;
import flash.net.URLRequest;
public class TestLoader extends Sprite {
private var loaded:TestLoaded;
public function TestLoader() {
super();
addEventListener(Event.ADDED_TO_STAGE, loadContent);
}
private function loadContent(event:Event):void {
var loader:Loader = new Loader();
var info:LoaderInfo = loader.contentLoaderInfo;
info.addEventListener(IOErrorEvent.IO_ERROR, onContentIOError);
info.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onContentSecurityError);
info.addEventListener(Event.INIT, onContentInit);
loader.load(new URLRequest("./loaded.swf"));
}
private function onContentInit(event:Event):void {
var info:LoaderInfo = event.target as LoaderInfo;
loaded = info.content as TestLoaded;
addChild(loaded);
}
private function onContentIOError(event:IOErrorEvent):void {
throw new IOError(event.text);
}
private function onContentSecurityError(event:SecurityErrorEvent):void {
throw new SecurityError(event.text);
}
}
}


See the difference? Before, there was no reference to TestLoaded in loader.swf. With this change, there is. Before, when loaded.swf was loaded, it contained the only reference to TestLoaded. This was as the document class, which had extra code from the timeline, so it was sort of a unique version of the class. But after the change, loader.swf had a copy of the TestLoaded class as well, a copy which had precedence over the unique version loaded in from loaded.swf. So the version with timeline code was ignored—no execution of timeline code.

The Remedy: This is something that was going to be done anyway—none of the loaded section SWFs should use the base version of the section/loaded class. They should all use their own subclasses. That way, nothing in loader.swf can block them.

Another solution (which isn't appropriate for our project, but it might be for others) would be to reference sections in loader.swf using an interface. (But even with this you'd still have to have different implementations in different SWFs, or they might conflict with each other—I think—should test that sometime.)

Really, though, there should be an option when assigning document classes, as there is when linking to a class from the library. You should be allowed to specify whether you want to use the class itself or an ad hoc subclass. If the former, the presence of timeline code should flag a warning message. Remind me to report this to Adobe.

11 comments:

  1. You, my friend, are my absolute savior!

    Came across this issue with only a couple of hours til deadline.

    Probably would have taken me all that to figure it out.

    My sincerest thanks,

    Mike

    ReplyDelete
  2. Nice! I knew I posted it for a reason.

    ReplyDelete
  3. I just came up with exactly the same problem. A reference to the baseClass of an loaded swf kills all the timeline code. Like you I'm hoping that a workaround with interfaces or at least subclasses will change something. I cannot image a good reason for that so I guess its really a bug in flash. Here is your reminder: Send a bug note to Adobe. I'm wondering if nobody else experienced this issue because it's a relatively common thing, don't you think?

    ReplyDelete
  4. My need was dire this problem was really hurting me, then i found you and there was peace again.

    Thank you, that's all I can say.

    ReplyDelete
  5. Long time since I thought about this topic. Is it possible that it happens because the main app is something like a singleton and by referencing the main app there is somehow a "second" version of it? The main app still exists and fires the events but the application listens to the second instance? I don't know :-)

    ReplyDelete
  6. @Anonymous
    as for the 2 reference, obviously somewhere in the Loaded.swf there is the complete class with timeline actionscript one means to implement. The real suprise for me is that the Loaded.swf timeline animation workes fine, while a trace on the timeline shows nothing. It must be inline script gets related to a class in a different way than animation. (but for that matter I always assumed timeline animation was also scriptified during compiling).

    We do get a hint of the script being outside the class, because timeline actionscript can only call public methods and never private ones of its class.

    ReplyDelete
  7. private var loaded:MovieClip;
    loaded = info.content as MovieClip;

    when u cast them as MovieClip it works fine.

    Whats the reason to use them as document class type?

    ReplyDelete
  8. Strict typing allows you to catch certain errors at compile-time rather than at run-time. (Otherwise, why not just cast everything as Object, right?)

    ReplyDelete
  9. Sure, agree, but its document class, that's the point. I always cast document classes as Movieclips in my projects and avoiding using document class for subsections, i see now why :)

    ReplyDelete
  10. That probably works okay for some projects, but some of us like to use strict typing when possible, especially on larger projects.

    ReplyDelete
  11. Thanks a lot. This post saved me hours of work.

    ReplyDelete