I've seen this information spread out in a bunch of different places online, but not in one place. So here you go:
The event-dispatching functionality in ActionScript 3.0 is wonderful. It creates a new paradigm for programming, one focused less on branching flowcharts and more on organic interactivity.
I'm assuming anyone reading this is familiar with how to use native events (e.g.,
addEventListener(Event.ENTER_FRAME, update);
and all that good stuff). But there are a few "gotchas" when it comes to making custom events. There are also some good practices that are a bit obscure.
Say you want to make a special type of event that indicates an index number, in addition to the normal information (
type
,
target
, etc.). You might think that all you had to do is this:
package tld.myproject.events {
import flash.events.Event;
public class IndexEvent extends Event {
public function IndexEvent(type:String, index:uint, bubbles:Boolean = false, cancelable:Boolean = false) {
super(type, bubbles, cancelable);
_index = index;
}
private var _index:uint;
public function get index():uint {
return _index;
}
}
}
Tip #1. You must override flash.events.Event.clone()
.
This is the big one. If you don't override
clone
, then there is no guarantee that listeners will receive the right kind of event. This method is used during dispatching to ensure that all listeners receive the same (i.e., unchanged) information. By default,
Event.clone()
simply returns a basic
Event
object, so if you don't override
clone
to return your custom event type, listeners may not be able to access your custom event data, and you will probably get type errors and null pointer exceptions.
Here's how to override
clone
for
IndexEvent
:
package tld.myproject.events {
import flash.events.Event;
public class IndexEvent extends Event {
public function IndexEvent(type:String, index:uint, bubbles:Boolean = false, cancelable:Boolean = false) {
super(type, bubbles, cancelable);
_index = index;
}
private var _index:uint;
public function get index():uint {
return _index;
}
override public function clone():Event {
return new IndexEvent(type, index, bubbles, cancelable);
}
}
}
Tip #2: You should override flash.events.Event.toString()
This one is not as important, since
toString
is only used for debugging. (Or
should only be used for debugging, anyway.) But the default method will not report the type of the event, and will also not report custom data. It'll just show something like
[Event type="select" bubbles=true cancelable=false eventPhase=2]
.
For this reason, it's a good idea to override the default method. Fortunately, there is a method called
formatToString
that makes it much easier to generate a standardized event string. Al arguments passed are strings. The first one should be the name of the custom event class, and the rest should be the names of all fields that you want reported in the string. Here's an example:
package tld.myproject.events {
import flash.events.Event;
public class IndexEvent extends Event {
public function IndexEvent(type:String, index:uint, bubbles:Boolean = false, cancelable:Boolean = false) {
super(type, bubbles, cancelable);
_index = index;
}
private var _index:uint;
public function get index():uint {
return _index;
}
override public function clone():Event {
return new IndexEvent(type, index, bubbles, cancelable);
}
override public function toString():String {
return formatToString("IndexEvent", "type", "index", "bubbles", "cancelable", "eventPhase");
}
}
}
This will produce a much more informative string, something like
[IndexEvent type="select" index=12 bubbles=true cancelable=false eventPhase=2]
. (Sometimes I even omit some of the base
Event
fields. For example, if my custom event's constructor doesn't allow for cancelable events, I might leave
"cancelable"
out of the
formatToString
call.)
Tip #3. You should include static constants for event types.
The base
Event
class and all of Adobe's
Event
subclasses do this, e.g.,
Event.ENTER_FRAME
,
TextEvent.LINK
, etc. The main reason for this is so that programmers can catch typos at compile-time rather than run-time. Typing
addEventListener("enerFrame", update)
won't generate a compile-time error, or even a run-time error -- your code will just fail silently. But
addEventListener(Event.ENER_FRAME, update)
will flag a compile-time error. For this reason, it's a good idea to follow in Adobe's footsteps and make your own constants:
package tld.myproject.events {
import flash.events.Event;
public class IndexEvent extends Event {
public function IndexEvent(type:String, index:uint, bubbles:Boolean = false, cancelable:Boolean = false) {
super(type, bubbles, cancelable);
_index = index;
}
public static const SELECT:String = "select";
public static const UNSELECT:String = "unselect";
private var _index:uint;
public function get index():uint {
return _index;
}
override public function clone():Event {
return new IndexEvent(type, index, bubbles, cancelable);
}
override public function toString():String {
return formatToString("IndexEvent", "type", "index", "bubbles", "cancelable", "eventPhase");
}
}
}
Ideally, these should be commented with ASDoc like so:
/**
* The <code>IndexEvent.SELECT</code> constant defines the value of the <code>type</code> property of the event object for a <code>select</code> event.
* <p>
* The properties of the event object have the following values:
* </p>
* <table class="innertable">
* <tr><th>Property</th> <th>Value</th></tr>
* <tr><td><code>type</code></td> <td><code>"select"</code></td></tr>
* <tr><td><code>bubbles</code></td> <td>A Boolean value.</td></tr>
* <tr><td><code>cancelable</code></td> <td>A Boolean value.</td></tr>
* <tr><td><code>index</code></td> <td>An unsigned integer.</td></tr>
* </table>
*
* @eventType select
*/
public static const SELECT:String = "select";
(Admittedly, I usually don't bother to do this, but....)
Tip #4. You should add metadata to your dispatchers.
Although metadata is not, strictly speaking, necessary, it's good practice for the following reasons:
- It allows other programmers (or you yourself, for that matter) to see at a glance which event types the dispatcher class is supposed to dispatch.
- It allows events to be documented with ASDoc, so that documentation can be automatically generated.
- In some code editors (Flex and [I think] FlashDevelop), it enables autocomplete, which is pretty nice.
- In Flex, it can allow binding.
Here's an example of a class that dispatches
IndexEvent
objects:
package tld.myproject.control {
import flash.events.EventDispatcher;
import tld.myproject.events.IndexEvent;
[Event(name="select", type="tld.myproject.events.IndexEvent")]
[Event(name="unselect", type="tld.myproject.events.IndexEvent")]
public class IndexController extends EventDispatcher {
public function IndexController() {
super();
}
private var _index:uint;
[Bindable(event="select")]
public function get index():uint {
return _index;
}
public function set index(value:uint):void {
if (_index != value) {
var oldValue:uint = _index;
_index = value;
dispatchEvent(new IndexEvent(IndexEvent.UNSELECT, oldValue));
// This test ensures that the event is not dispatched if some listener to "unselect" has changed the value.
if (_index == value) {
dispatchEvent(new IndexEvent(IndexEvent.SELECT, value));
}
}
}
}
}
(Incidentally, if you were programming this class in MXML, the
{Event(...)]
metadata would go inside an
<mx:Metadata/>
section.)
With this metadata in place, we can now document the events:
/**
* Dispatched when the <code>index</code> field changes.
* <p>
* The <code>index</code> field of the event object indicates the new value.
* </p>
*
* @eventType tld.myproject.events.IndexEvent.SELECT
* @see #index
*/
[Event(name="select", type="tld.myproject.events.IndexEvent")]
/**
* Dispatched when the <code>index</code> field changes.
* <p>
* The <code>index</code> field of the event object indicates the old value.
* </p>
*
* @eventType tld.myproject.events.IndexEvent.UNSELECT
* @see #index
*/
[Event(name="unselect", type="tld.myproject.events.IndexEvent")]
Note that the
@eventType
ASDoc attribute indicates the constant that specifies the event type. (It can also indicate a plain string, but, per Tip #3, you should use constants anyway.)
We can also use autocomplete in Flex. if
ctrl
is an instance of
IndexController
, then typing
ctrl.a
will bring up a list of options including:
ctrl.addEventListener(IndexEvent.SELECT,
ctrl.addEventListener(IndexEvent.UNSELECT,
... keeping you from having to laboriously type out either one.
And we can bind to the value of
index
in MXML:
<control:IndexController id="ctrl"/>
<mx:ViewStack selectedIndex="{ctrl.index}">
<!-- child objects -->
</mx:ViewStack>
The
ViewStack
object's
selectedIndex
field will now auto-update whenever
ctrl
dispatches a
select
event (per the
Bindable
metadata element -- note that multiple such elements may precede a property, if there are different trypes of event that signal the value changing).
Tip #5. You don't always have to extend flash.events.Event
.
Sometimes it's fine to use a plain old
Event
instance! There are plenty of generic event types (e.g.,
Event.CANCEL
) associated with it, and often that's all you really need. Even if you want a type that doesn't have a constant, you don't have to create a custom event type for it. The constant can be housed elsewhere. For example:
package tld.myproject.control {
import flash.events.Event;
import flash.events.EventDispatcher;
/**
* Dispatched when the process this object controls begins.
*
* @eventType tld.myproject.control.ProcessController.EVENT_START
*/
[Event(name="start",type="flash.events.Event")]
public class ProcessController extends EventDispatcher {
/**
* The <code>ProcessController.EVENT_START</code> constant defines the value of the <code>type</code> property of the event object for a <code>start</code> event.
* <p>
* The properties of the event object have the following values:
* </p>
* <table class="innertable">
* <tr><th>Property</th> <th>Value</th></tr>
* <tr><td><code>type</code></td> <td><code>"start"</code></td></tr>
* <tr><td><code>bubbles</code></td> <td><code>false</code></td></tr>
* <tr><td><code>cancelable</code></td> <td><code>false</code></td></tr>
* </table>
*
* @eventType start
*/
public static const EVENT_START:String = "start";
public function startProcess():void {
dispatchEvent(new Event(EVENT_START));
}
}
}
As long as the ASDoc comment with the
@eventType
attribute is included, Flex will autocomplete for the
start
event as well.
As a general rule of thumb, if you don't need the event object to carry extra data, you don't need to create a custom event class. Even if you do, there may be another subclass already in existence that suits your needs (e.g.,
TextEvent
,
ErrorEvent
, etc.). On the other hand, though, if you do create a custom class with no added fields, it may come in handy down the road if you realize that you do need added fields after all. And it might also be handy if you want to restrict how default fields like
type
,
bubbles
, and
cancelable
are set.
Well, I think that's about it.
dispatchEvent(new Event(Event.COMPLETE));