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:

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});
        }
}

The Form class:

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;
        }
}

And the controller class:

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);
        }
}

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!