Freelancer behind 5 1/2 math and physics enthusiast Patrick has a knack for making seemingly simple things overly complicated. Perfect for a tutorial writer. As your Actionscript projects become more involved it will become increasingly important to separate functionality between classes. Compartmentalizing helps you debug your scripts more easily, facilitates collaboration between multiple developers and promotes reuse.
For example, say you want to create a horizontal ticker that uses an RSS feed as a source. Your first instinct might be to create a single class that does the XML loading, the parsing, and then the ticking. If you want to promote reuse however, you'll want to make modules for reusable things and then create a controller class to bind everything together. In our RSS ticker case, you could create a RSSReader class that takes in a URL, creates an XML object, parses it, and then gets the relevant data and places it in a data array. Then you could create a HorizontalTicker class that takes an array of titles and creates a horizontal ticker out of it. Finally a Controller class could bind the RSSReader and the HorizontalTicker together, filling the gap. That way you can use the RSSReader and the HorizontalTicker for other projects outside of the RSS ticker context.
There are several places where RSSReader and HorizontalTicker will need to notify the Controller; for example, when the RSS feed is properly loaded, the controller will need to start the ticker. The question here is how to glue everything together. There are a few ways in which this could be done, for example:
loaded variable in the RSSReader instance to see if it's true (polling) RSSReader could store a reference to the controller and call a certain function directly in it when the feed is loaded (hard links) RSSReader that will be called by RSSReader (callbacks).All of these have some inconveniences; the first method is wasteful and is just plain hacky; the second method is not very flexible because the class will not work properly when used out of context; the third method is in fact the method used by v1 components and can be quite useful but it necessarily creates one-to-one links when sometimes one-to-many relationships can be quite useful.
The method I'll discuss today is binding these classes using mx.events.EventDispatcher. Macromedia has kindly included several classes with Flash MX 2004 to help in certain redundant tasks. Among the most useful are:
This tutorial will be split in 3 sections:
If you already use EventDispatcher, you can skip right ahead to the examples, you could still learn some rather clever tricks. The tutorial zip file includes the source code to the examples, be sure to grab it to follow along.
If you've used v2 components, chances are you already have used EventDispatcher unknowingly when calling addEventListener or removeEventListener. EventDispatcher is at the core of the v2 component framework. Amazingly however, it has very little dependencies, so including mx.events.EventDispatcher in your projects adds less than 2k to a movie. First let's see a minimalist Timer class that uses EventDispatcher to see what it looks like:
[as]import mx.events.EventDispatcher;
import mx.utils.Delegate;
class Timer
{
var timeInt:Number;
var addEventListener:Function;
var removeEventListener:Function;
var dispatchEvent:Function
function Timer(len)
{
EventDispatcher.initialize(this);
timeInt = setInterval(Delegate.create(this, handleTimer), len);
}
function handleTimer()
{
dispatchEvent({type:'timeout', target:this, message:'Hey dude the timer is done', time:getTimer()});
clearInterval(timeInt);
}
}[/as]
By calling EventDispatcher.initialize(this), three methods are added to the current class instance: addEventListener, removeEventListener, and dispatchEvent. We define these functions as class variables so that the compiler will not whine about undefined functions when we test our movie. When the timer is over, we dispatch an event. dispatchEvent takes one argument, an object with two properties: target and type. 'type' is the name of the event you want to dispatch, while target is whatever dispatched the event, which 99% of the time will be 'this'. Once those required fields are filled you can add whatever else you feel would be interesting to know about the event; in this case we've added a message and a time variable to show how this can work.
Dispatching an event is all fine and dandy, but isn't very useful unless someone is listening to it (you can insert a remark about trees falling in a forest with no one around here). We set up a listener like so:
[as]import mx.utils.Delegate;
function handleConstruct(eventObj)
{
trace(eventObj.target);
trace(eventObj.type);
trace(eventObj.message);
trace(eventObj.time);
}
var myClass = new Timer(1000);
myClass.addEventListener('timeout', Delegate.create(this, handleConstruct));[/as]
Here we create an instance of the Timer class and add an event listener to it to link to the timeout event. We can see that although there isn't a direct hard link between the controller and the dispatcher, nevertheless the two are linked indirectly through EventDispatcher.
So what's so great about this method of doing things, especially when compared to simpler methods like callbacks? Well:
EventDispatcher is the only way to go, because v2 components have a special tie-in between EventDispatcher and on(event) handlers in the IDE; meaning that if you add the proper metatag to your class all that needs to be done to handle the event is writing on(eventtype) in the actions panel, meaning it's easier for beginners and designers to use your components. EventDispatcher is installed on every machine that has Flash MX 2004 on it, so that means one less piece of code to worry about including when you pass your code around.At this point, I'd really like to believe that I've convinced you of the good of EventDispatcher, but I'm afraid that you still might be skeptical about it. What follows is two advanced examples of things you can do with EventDispatcher that would be a pain in the neck to do using other methods.
Here's the problem we are going to solve. Buttons are some of the most basic things in Flash-world along with graphics and movieclips. Buttons are very easy to create with their no-frills frames based mechanism but they are also limited in that they have only four states including the hit state which is never actually seen. You would think that something as basic as a 'disabled' state would be covered by buttons but the enabled property was in fact introduced in Flash 6, while the button has been available since Flash 2 (!). A couple of other states like 'locked', 'toggled', 'dragover', etc. might also seem very useful. Another thing is that buttons don't behave like MovieClips in some odd places; for example, there is no Button.getBounds function. TextField names inside buttons are not exported, which means changing labels or translating buttons at runtime is impossible.
Of course MovieClips have none of these limitations but you'll have to write everything from scratch. Ideally we'd like to have some middle way that offers all the power of MovieClip, a lot of button states yet the ease of use buttons; ideally we'd need backwards compatibility with buttons so that the first, second and third frame of our new scheme correspond to the up, over and down states. You might think if you need those advanced functions the Button component would be the way to go; but it's heavy (a good 70k) and it's a pain in the neck to skin, so that's a no go.
Here's my solution: we make a PseudoButton class that extends MovieClip. We associate this class through the library with any movieclip we want to turn into a PseudoButton. That class has some generic functionality to make it work and feel like a button (ie. Frames 1, 2 and 3 work like they work with buttons). To do this we'll need to tap into the on* callbacks of the movieclip; we'll forward these using EventDispatcher, with the added bonus that several functions can tap into the new events at the same time. Sounds great? Here's the script then:
[as]import mx.utils.Delegate;
import mx.events.EventDispatcher;
class PseudoButton extends MovieClip
{
private var __label:String = "";
public var labeltf:String = "lbl";
public var toggable:Boolean = false;
public var lockable:Boolean = false;
public var toggled:Boolean = false;
public var locked:Boolean = false;
public var addEventListener:Function;
public var removeEventListener:Function;
public var dispatchEvent:Function;
public var over:Boolean = false;
public var down:Boolean = false;
public var currentFrame:Number = 1;
function PseudoButton()
{
EventDispatcher.initialize(this);
init();
}
function init()
{
this.stop();
}
function onRollOver()
{
over = true;
if(!locked)
{
if(!toggled)
{
gotoState(2);
}
else
{
gotoState(7);
}
dispatchEvent({target:this, type:'rollOver'});
}
}
function onRollOut()
{
over = false;
if(!locked)
{
if(!toggled)
{
gotoState(1);
}
else
{
gotoState(6);
}
dispatchEvent({target:this, type:'rollOut'});
}
}
function onPress()
{
down = true;
if(!locked)
{
if(!toggled)
{
gotoState(3);
}
else
{
gotoState(8);
}
dispatchEvent({target:this, type:'press'});
}
}
function onRelease()
{
down = false;
if(!lockable)
{
if(!toggable)
{
gotoState(2);
}
else
{
if(!toggled)
{
toggled = true;
gotoState(7);
}
else
{
toggled = false;
gotoState(2);
}
}
dispatchEvent({target:this, type:'release'});
}
else
{
if(!locked)
{
locked = true;
gotoState(6);
dispatchEvent({target:this, type:'release'});
dispatchEvent({target:this, type:'lock'});
}
}
}
function onReleaseOutside()
{
down = false;
if(!locked)
{
gotoState(1);
dispatchEvent({target:this, type:'releaseoutside'});
}
}
function unlock()
{
locked = false;
gotoState(1);
}
function lock()
{
locked = true;
gotoState(6);
}
function setEnabled(val)
{
enabled = val;
if(!enabled)
{
gotoState(5);
}
else
{
if(down)
{
gotoState(3);
}
else if(over)
{
gotoState(2);
}
else
{
gotoState(1);
}
}
}
function gotoState(num)
{
gotoAndStop(num);
currentFrame = num;
updateText();
}
function updateText()
{
if(labeltf != null)
{
this[labeltf].text = label;
}
}
public function set label(val:String)
{
this.__label = val;
updateText();
}
public function get label():String
{
return __label;
}
}[/as]
toggable), along with over and down states for the toggled state. We can tap into the release, releaseOutside, rollOver, rollOut and press events corresponding to the usual button events. In addition, we get access to new events like toggle. We've added a label setter that allows the button labels to be carried across frames if labeltf (label text field) is set properly, thus making translation dead easy. You can see this is a powerful solution that you can easily extend by modifying the class.Here's a more tricky example that shows more of the power of EventDispatcher. Say you have a form that contains conditional form elements. For example, you could have a checkbox to subscribe to a newsletter, and if checked, an email field becomes activated. These conditional form elements can cascade; that is, a form element can be activated only if it's parent is both selected and activated, and that parent can be activated only if its own parent is both selected and activated, and so on and so forth.
There are a lot of ways in which you could achieve this. For example:
These have performance, elegance, and weight tradeoffs that we want to avoid. Here's my solution: make two classes, Form and Field. Form keeps a list of the fields in the form and handles tab focus, while Field has hooks to bind it's enabled state to another element, and dispatches events depending on its own enabled state. A controller class then binds this whole form together. Here I've chosen to create the form layout in the IDE on the root timeline, although you could write code to do the layout if you prefer. Here's the code for the Field class:
[as]import mx.utils.Delegate;
import mx.events.EventDispatcher;
class Field
{
var mc:MovieClip;
var type:String = "";
var boundField:Field;
var boundIndex:Number;
var addEventListener:Function;
var removeEventListener:Function;
var dispatchEvent:Function;
function Field(mc:MovieClip, type:String)
{
EventDispatcher.initialize(this);
this.mc = mc;
this.type = type;
init();
}
function init()
{
if(type == 'combo')
{
this.mc.addEventListener('change', Delegate.create(this, handleSelect));
}
else if(type == 'radio')
{
var group = this.mc._parent[this.mc.groupName];
group.addEventListener('click', Delegate.create(this, handleSelect));
}
else
{
this.mc.addEventListener('click', Delegate.create(this, handleSelect));
}
}
function handleSelect()
{
dispatchEvent({type:'select', target:this});
}
function bindEnabledTo(what, index)
{
boundField = what;
boundIndex = index;
boundField.addEventListener('enable', Delegate.create(this, handleBoundFieldChange));
boundField.addEventListener('select', Delegate.create(this, handleBoundFieldChange));
//update immediately
handleBoundFieldChange({type:'enable', target:boundField});
}
function handleBoundFieldChange(eventObj)
{
var toEnable:Boolean = false;
if(boundField.mc.enabled)
{
if(boundField.type == 'combo')
{
toEnable = boundField.mc.selectedIndex == boundIndex;
}
else if(boundField.mc.selected)
{
toEnable = true;
}
}
if(this.mc.enabled != toEnable)
{
setEnabled(toEnable);
}
}
function setEnabled(newVal)
{
if(type != 'label')
{
//For some reason label.enabled does not work properly, so skip
this.mc.enabled = newVal;
}
dispatchEvent({type:'enable', target:this});
}
}[/as]
The Form class:
[as]class Form
{
var fields:Array = new Array();
function addField(theField:Field):Field
{
fields.push(theField);
if(theField.mc == null)
{
trace('Field ' + fields.length + ' not found');
}
theField.mc.tabIndex = fields.length;
return theField;
}
}[/as]
And the controller class:
[as]class Controller
{
var root:MovieClip;
var _form:Form;
function Controller(root:MovieClip)
{
this.root = root;
init();
}
function init()
{
_form = new Form();
_form.addField(new Field(root.lblSurvey, 'label'));
var no = _form.addField(new Field(root.rbNo, 'radio'));
var yes = _form.addField(new Field(root.rbYes, 'radio'));
var lblRef = _form.addField(new Field(root.lblRef, 'label'));
var cbRef = _form.addField(new Field(root.cbRef, 'combo'));
lblRef.bindEnabledTo(yes);
cbRef.bindEnabledTo(yes);
var lblRefOther = _form.addField(new Field(root.lblRefOther, 'label'));
var txtRefOther = _form.addField(new Field(root.txtRefOther, 'input'));
lblRefOther.bindEnabledTo(cbRef, 2);
txtRefOther.bindEnabledTo(cbRef, 2);
var cbOffers = _form.addField(new Field(root.cbOffers, 'checkbox'));
cbOffers.bindEnabledTo(yes);
var rbEmail = _form.addField(new Field(root.rbEmail, 'radio'));
rbEmail.bindEnabledTo(cbOffers);
var lblEmail = _form.addField(new Field(root.lblEmail, 'label'));
var txtEmail = _form.addField(new Field(root.txtEmail, 'input'));
lblEmail.bindEnabledTo(rbEmail);
txtEmail.bindEnabledTo(rbEmail);
var rbMail = _form.addField(new Field(root.rbMail, 'radio'));
rbMail.bindEnabledTo(cbOffers);
var lblAddress = _form.addField(new Field(root.lblAddress, 'label'));
var txtAddress = _form.addField(new Field(root.txtAddress, 'textarea'));
lblAddress.bindEnabledTo(rbMail);
txtAddress.bindEnabledTo(rbMail);
}
}[/as]
And the result:
The binding between each of the elements is done at the level of the bindEnabledTo method. At that point the field starts listening to another field's state changes. Events are bubbled through the chain of bound elements to toggle element states. In the end, we end up with a clever solution to a difficult problem.
At this point we've used addEventListener and dispatchEvent; you can also use removeEventListener(listenerRef) to remove an event listener, as you would with components. A question you may be asking at this point is what is the overhead of using EventDispatcher? In fact dispatching an event involves looping through an array of dispatch queues, which does involve some overhead. Therefore if you plan on dispatching and responding events several hundreds of times per frame you may find some significant execution time difference caused by the overhead of EventDispatcher. However for almost all normal circumstances you can rest assured that the bottleneck is elsewhere.
Once you start using EventDispatcher, it will grow on you and it will increasingly become a part of your day-to-day tools. Here's hoping that I've been able to switch a couple of you to the light side of the force.
If you need professional help with ActionScript, please visit 5etdemi.com for my portfolio and contact info. Happy flashing!