21 January 2008

How to Extend "Event", Once and For All

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:
  1. 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.
  2. It allows events to be documented with ASDoc, so that documentation can be automatically generated.
  3. In some code editors (Flex and [I think] FlashDevelop), it enables autocomplete, which is pretty nice.
  4. 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));

17 comments:

  1. I admire all those who have the patience to learn programming.

    ReplyDelete
  2. Great example!

    I'm trying to create a custom ResultEvent. I need to pass it additional parameters like you did in the example.

    So instead of extending Event, I extended ResultEvent.

    If I manually dispatch the event, i only get the passed parameter and not the result. Any idea what I'm forgetting?

    ReplyDelete
  3. I'd have to see your code to be sure, but make sure you're passing the result object to the super constructor (i.e., the ResultEvent constructor) . That's the only way you can set ResultEvent.result.

    ReplyDelete
  4. DxEvent.as
    package flex.events
    {
    import flash.events.Event;

    import mx.rpc.events.ResultEvent;

    public class DxEvent extends ResultEvent
    {
    public function DxEvent(type:String, data:String, result:Object = null, bubbles:Boolean = false, cancelable:Boolean = false)
    {
    _data = data;
    super(type, result, bubbles, cancelable);
    }

    public static const RES:String = "res";
    private var _data:String;

    public function get data():String
    {
    return _data;
    }
    override public function clone():Event
    {
    return new DxEvent(type, data, result, bubbles, cancelable);
    }
    }
    }

    DxController.as
    package flex.events
    {
    import flash.events.EventDispatcher;

    [Event(name="res", type="flex.events.DxEvent")]

    public class DxController extends EventDispatcher
    {
    public function DxController()
    {
    super();
    }

    private var _data:String;

    [Bindable(event="res")]
    public function get data():String
    {
    return _data;
    }
    public function set data(value:String):void
    {
    _data = value;
    dispatchEvent(new DxEvent(DxEvent.RES, value));
    }


    }
    }

    I then have a function that uses a webservice
    CursorManager.setBusyCursor();
    WS = new WebService();
    WS.wsdl = "http://www.webservicex.net/uszip.asmx?wsdl"
    WS.GetInfoByCity.resultFormat = "e4x";
    WS.GetInfoByCity.addEventListener("fault", faultHandler);
    WS.GetInfoByCity.addEventListener("res", customlistener);
    WS.addEventListener(LoadEvent.LOAD, loadHandler);
    WS.loadWSDL();

    I'm not sure how or where to dispatch the DxEvent. Do I dispatch it in the loadHandler?

    thanks for your help!
    Neal

    ReplyDelete
  5. I'm surprised it compiles with the superclass constructor call as the second line of the constructor (it should be the first line), but it looks fine otherwise. I think your problem is in knowing where to substitute the ResultEvent created by the web service with your own custom event. You could try something like this in your DxController class:

    public function addWebService(ws:WebService):void {
    ws.GetInfoByCity.addEventListener( ResultEvent.RESULT, onWebServiceResult);
    }

    private function onWebServiceResult(event:ResultEvent):void {
    _data = event.result as String;
    dispatchEvent(new DxEvent(DxEvent.RES, data, event.result, event.bubbles, event.cancelable));
    }


    You'll notice, though, that that's completely redundant. The data and result fields will be exactly the same. Which makes me want to ask--why are you extending ResultEvent at all? What difference is there between its result and your data?

    ReplyDelete
  6. Oh, also:

    package flex.events

    That's Adobe's namespace. You should make your own, or it might conflict with Adobe code. (So far Adobe only has Java code in its flex family of packages, I think, but that could change.) Most people just take their web address and invert it, which is a good ploy. I work with com.exopolis, org.marchofman, org.namesonnexus, com.dinosauricon, etc. and I can be pretty confident my packages won't ever conflict with anybody else's.

    ReplyDelete
  7. Hi thanks for responding. The webservice that I'm calling has a count method that returns the total number of rows for a particular object.

    WS.count(whatObj, "query");

    a real example whould look like

    WS.count("Patient", "lastname like '%smith%'");

    the count returns 694 rows. I then on that result want to use that same query to call another method that gives me the rows.

    WS.find("Patient", "lastname like '%smith%'");

    So I need to pass the query to the resultEvent as an additional param.

    addEventListener("result", resultHander(query));

    will not work. So I tried to subClass the resultEvent.

    I hope this makes sense. I actually thought that the webservice should provide more information (the query used) about the the returned data, other than just the data itself (694 rows) or combine the methods into one and give me a count and the results.

    ReplyDelete
  8. That approach isn't going to work unless you extend WebService to dispatch your type of event rather than (or along with) ResultEvent objects.

    A better approach would involve using the AsyncToken object generated by the request. AsyncToken is a dynamic class, so you can add custom data to it:

    In the invoking method:
    var query:String
    = "lastname like %smith%";
    var token:AsyncToken
    = WS.count("Person", query);
    token.query = query;
    token.addEventListener(
    ResultEvent.RESULT,
    onResult);


    And the handler:
    private function onResult(event:ResultEvent):void {
    var data:Object
    = event.result;
    var query:String
    = AsyncToken(event.target).query;
    // Perform another call with same query.
    }

    ReplyDelete
  9. very cool, I will try that. Thank you for taking the time to answer the question.

    ReplyDelete
  10. No problem. (I've never actually tried it, so let me now if it works!)

    ReplyDelete
  11. OMG, you have shed the light on my problem :).

    public function count():void
    {
    var query:String = "lastname like '%prac%'";
    var token:AsyncToken = WS.count("Patient", "", query);
    token.query = query;
    var responder:mx.rpc.Responder = new mx.rpc.Responder( onResult, faultHandler );
    token.addResponder(responder);
    }

    private function onResult(event:ResultEvent):void
    {
    var data:Object = event.result;
    var query:String = event.token.query;
    // Perform another call with same query.
    }

    Thank you very much!

    ReplyDelete
  12. Whoops -- I forgot that AsyncToken doesn't actually dispatchResultEvent objects, but just passes them (and FaultEvent objects) on to Responder objects. Well, glad it's working now!

    ReplyDelete
  13. Great summary, Mike. I wonder whether you also see a bug in Flex Builder 3 for your tip #5: Code completion does not show ProcessController.EVENT_START but Event.START . If it's really a bug, it makes metadata in this scenario somewhat useless.

    ReplyDelete
  14. Actually, yes, I had just noticed that and was going to update the post at some point. It's a bug in Flex Builder, and it's in their public bug database. Hopefully it'll be fixed in the next version.

    ReplyDelete
  15. thanks so much. flash is a lumbering hulking arse of a program designed by a team of sadistic monkeys with a depressingly effective marketing department, and what's more the manual is so crap (and online, so unsearchable) that random forum posts show up before pages on adobe.com when you google search.

    posts like this help to make us poor developers lives just that little bit less painful..

    ReplyDelete
  16. Heh, no problem.

    I've been with Flasha long time. The current version is ... well, let's just say it's going through growing pains. (Which is why I mostly use Flex now.)

    ReplyDelete