Document Classes… with Interfaces!

Often, a Flash developer will want to load an external swf and assign the contents to a strongly typed variable so as to take advantage of the compile-time and run-time safeguards associated with strong typing, as well as the intellisense capabilities of mature coding environments like FlashDevelop. Unfortunately, it can be less straightforward than we’d like to simply import the class definition for the external swf’s Document Class, particularly if our external content has any library items set to export with a Class name, or relies on Flash to automatically declare stage instances.

For an example of how this goes wrong, see the following zip file:

If you compile the LoadedContent.fla everything works just fine. The problem comes when you try to compile the ContentLoader, which mysteriously complains about problems in LoadedContent.as. You may wonder why there are suddenly problems in that class now when it compiles perfectly fine in the LoadedContent.fla. Let’s open up LoadedContent.as, as the answer is there:

package  {
	import flash.display.MovieClip;
	import flash.text.TextField;
 
	/**
	 * ...
	 * @author The Horseman @ Scriptocalypse.com
	 */
	public class LoadedContent extends MovieClip{
		public function get asset1():LoadedContentAsset {
			return _asset1; 
		}
 
		private var _asset1:LoadedContentAsset;
		private var _asset2:LoadedContentAsset;		
		private var _someClip:MovieClip;
 
 
		public function LoadedContent() {			
 
		}
 
		public function showImages():void {
			_asset1 = new Asset1(); // Declared in the .fla library
			_asset2 = new Asset2(); // Declared in the .fla library
 
			_asset1.x = 100;
			_asset1.y = 100;
			_asset2.x = _asset1.x + _asset1.width;
			_asset2.y = 100;
 
			this.addChild(_asset1);
			this.addChild(_asset2);
 
			_someClip = new SomeClip(); // Declared in the .fla library
 
			_someClip.x = _asset2.x + _asset2.width;
			_someClip.y = 100;
 
			this.addChild(_someClip);
		}
 
		public function sayHello():String {
			this.text_txt.text = "Hello World"; // exists on the timeline.
			return this.text_txt.text;
		}
 
	}
 
}

Notice how, when we compile the ContentLoader.fla and read the error messages they’re all pointing to lines of code that I’ve marked with a comment. The reason LoadedContent.fla compiles correctly is because, in the case of Asset1, Asset2, and SomeClip, the classes it’s instantiating exist in its library. LoadedContent.fla knows where to look for them, and has a meaningful point of reference to them. ContentLoader does not. It just sees a Class definition trying to perform actions with other classes that don’t exist. The same problem exists with the reference to this.text_txt in LoadedContent.as. Because LoadedContent.fla is using the default compiler setting of “Automatically Declare Stage Instances”, it can simply reference the text field without actually calling this.getChildByName() or similar. All ContentLoader sees when it reads that line in the LoadedContent definition is “You’re trying to perform an action on an undeclared variable!”

What to do? We could simply cast the loaded content as MovieClip or Object, since those are Dynamic classes. That would silence the error, but it would leave us without the benefits of a strongly typed environment. Since The Horseman openly admits his bias in favor of strongly typed code, he feels compelled to illustrate how this can still be done. The key is interfaces.

An interface is nothing more than a “contract”. When a Class implements an Interface, it promises that the functions and properties of the interface will be available for use by other data, eg: declared as public. Absolutely no implementation details are present in the Interface. When put in context of the LoadedContent situation, it means that it doesn’t define, know about, or care about any such thing as Asset1, Asset2, SomeClip, or this.text_txt. Here’s an example:

package  {
 
	/**
	 * ...
	 * @author The Horseman @ Scriptocalypse.com
	 */
	public interface ILoadableContent {
 
		function showImages():void;
		function sayHello():String;		
 
	}
 
}

By convention in ActionScript, Interfaces always start with the letter ‘I’. This differentiates them from Class definitions at a glance. What this Interface tells us is that any Class that implements it is guaranteed to have public functions with the same function signatures as shown above. Luckily for us, LoadedContent already has those functions defined! It just needs to officially implement the interface like so:

package  {
	import flash.display.MovieClip;
	import flash.text.TextField;
 
	/**
	 * ...
	 * @author The Horseman @ Scriptocalypse.com
	 * 
	 *    Implement the interface here in the LoadedContent.
	 * 
	 */
	public class LoadedContent extends MovieClip implements ILoadableContent{
 
	// and down further are the public functions expected by the Interface.

Now in our ContentLoader we just need to replace the references to LoadableContent with references to ILoadableContent.

package  {
	import flash.display.DisplayObject;
	import flash.display.Loader;
	import flash.display.MovieClip;
	import flash.events.Event;
	import flash.net.URLRequest;
 
	/**
	 * ...
	 * @author The Horseman @ Scriptocalypse.com
	 */
	public class ContentLoader extends MovieClip{
 
		// Type the content as the Interface, not the concrete Class.
		private var _loadedContent:ILoadableContent;
		private var _loader:Loader;
 
 
		public function ContentLoader() {
			_loader = new Loader();
			_loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onEventCompleteLoader);
			_loader.load(new URLRequest("LoadedContent.swf"));
		}
 
		private function onEventCompleteLoader(e:Event):void {
			e.currentTarget.removeEventListener(Event.COMPLETE, onEventCompleteLoader);
			_loadedContent = e.target.content as ILoadableContent;
			_loader.unload();
			_loader = null;
 
			// Casting because an interface cannot be a DisplayObject, sadly.
			this.addChild(_loadedContent as DisplayObject); 
 
			_loadedContent.showImages();
			var hello:String = _loadedContent.sayHello();
			trace(hello);
		}
 
	}
 
}

Now the ContentLoader.fla will compile correctly. Since it isn’t looking at the concrete LoadedContent, but instead only looking at the Interface ILoadedContent there are no more conflicts to resolve. The only thing about this that is slightly bothersome is the fact that if you have a reference to a DisplayObject but only know of it by its interface, you cannot use it as an argument in a call to addChild() without first casting it as a DisplayObject. This is however, a very small tradeoff for the compile-time, run-time, and author-time benefits of working in a strongly-typed manner with a modern coding environment.

Here is a zip file containing the new and improved code.

The implications of this reach further than loading, however. If you code your components so that they implement interfaces, you can switch them out with each other more easily than if they are simply Classes, and referenced as such.

EDIT: These days instead of coercing the ILoadableContent into being a DisplayObject, I would instead give the ILoadableContent interface a get view function like this

package  {
 
	/**
	 * ...
	 * @author The Horseman @ Scriptocalypse.com
	 */
	public interface ILoadableContent {
 
		function get view():DisplayObject;
		function showImages():void;
		function sayHello():String;		
 
	}
 
}

That way, when you call addChild you no longer have to blindly assume that the ILoadableContent is a DisplayObject. This is also nice in that it allows the ILoadableContent to choose a displayObject to act as its own view. Maybe it should be the view and it returns ‘this’. Maybe it has some other object it wants to be the view instead? It doesn’t have to matter with the get view function.

Tags: , , , ,

  1. #1 written by Geoff March 30th, 2011 at 14:23

    Thanks!

    This was really helpful, it’s really nice to be able to work with strongly type loaded content, FDT complained about it quite a bit but after implementing the above technique it has no complaints about typing the loaded content to an interface.