ActionScript.org Flash, Flex and ActionScript Resources - http://www.actionscript.org/resources
Using EventDispatcher
http://www.actionscript.org/resources/articles/204/1/Using-EventDispatcher/Page1.html
Patrick Mineault
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. 
By Patrick Mineault
Published on June 3, 2006
 
Written by: Patrick Mineault, [email:[email protected]], http://www.5etdemi.com
Difficulty Level: Expert
Requirements: MX 2004
Topics Covered: EventDispatcher, Classes, MVC
Assumed Knowledge: Working knowledge of AS2 classes 

Using EventDispatcher
Written by: Patrick Mineault, [email:[email protected]], http://www.5etdemi.com
Difficulty Level: Expert
Requirements: MX 2004
Topics Covered: EventDispatcher, Classes, MVC
Assumed Knowledge: Working knowledge of AS2 classes
Download FLAs

Using EventDispatcher

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:

  • the controller could continually check for a loaded variable in the RSSReader instance to see if it's true (polling)
  • the RSSReader could store a reference to the controller and call a certain function directly in it when the feed is loaded (hard links)
  • the controller could overwrite a function inside of the 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:

  • mx.transitions.Tween
  • mx.utils.Delegate
  • Today's subject mx.events.EventDispatcher

This tutorial will be split in 3 sections:

  • Introduction to using EventDispatcher
  • Example 1: a pseudo-button class
  • Example 2: conditional form elements

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:

  • Several objects can listen to the same events. That means that dispatching one event can trigger calls to several methods seamlessly. In the third part of this tutorial I will show you how you can use the power of one-to-many event relationships to solve complicated problems elegantly.
  • You don't need to define a new method in your class for every new event you create, so you can dispatch as much events as you see fit without polluting your class with empty callbacks.
  • If you create components, 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.
  • The event object you receive always includes a reference to the caller. This may sound a bit esoteric, if you don't understand don't lose sleep over it, but the scope in which the callback is called can be forced using a Delegate, yet you still know the original piece of code that dispatched the event. That gives you a lot of flexibility, since classes can handle events within their scope while keeping track of the real caller and all of this without ugly hacks, compiler fooling and other things that make babies cry.
  • Don't forget that mx.events.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.


Example 1: Creating a Pseudo-Button

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]

Now not only do we have a movieclip that follows the conventions of the three first button frames, it also includes a disabled state on frame 5 (triggered using setEnabled(false)) and a toggled state on frame 6 (activated through 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.

Example 2: Conditional form elements

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:

  • There could be a loop set up that checks for states and if they are changed, then refresh everything and insure the render is consistent
  • You could set up a bunch of handlers to respond to each individual state change in an appropriate fashion
  • You could use Macromedia's data binding implementation.

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.

Closing remarks

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!