Why events suck in ActionScript 3

I actually took this from production code!

I’ve had it with events in ActionScript 3! They are annoying to structure, annoying to extend, annoying to dispatch but most importantly annoying to consume – and I consume events a lot more than write or dispatch my own.

But! The idea of listening for stuff I really do like. I have some ideas about how this could be done a lot easier. Oh, and please stick around until the very end of this post.

Background

For those who can’t remember exactly, events in AS3 are consumed by listening for certain events and passing a function, that will be called when this event occurs. The function will always be invoked with the event type, that you listen for – and only that argument will be given to the callback. You cannot define extra arguments in any way or not accept the event argument. Let’s for example listen for when the use clicks a button – which is the MouseEvent.CLICK event (of type MouseEvent of course):

public function init():void {
  var myButton:Sprite = new Sprite();
  ...
  myButton.addEventListener(MouseEvent.CLICK, buttonPressed);
  ...
}
private function buttonPressed(me:MouseEvent):void {
  trace("the button has been pressed, hurray");
}

We abidingly accept the me:MouseEvent argument even though we don’t intend to use it.

The event object has information about the event, about who sent it, information related to the bubbling nature of events (that very few people ever use in ActionScript 3) and additional specific attributes only relevant for specific events. For instance, if we have several buttons representing different related actions, we can examine the event.target

private var selectRed:ColorButton;
private var selectGreen:ColorButton ;
private var selectBlue:ColorButton;
public function init():void {
  ...
  selectRed.addEventListener(MouseEvent.CLICK, selectColor);
  selectGreen.addEventListener(MouseEvent.CLICK, selectColor);
  selectBlue.addEventListener(MouseEvent.CLICK, selectColor);
  ...
}
private function selectColor(me:MouseEvent):void {
  var color:ColorButton = me.target as ColorButton;
  if (!color) {
    throw new Error("color selected was not actually a color");
  }
  switch (color) {
    case selectRed: trace("you chose a red Papa Smurf"); break;
    case selectGreen: trace("you chose a green Papa Smurf"); break;
    case selectBlue: trace("you chose a blue Papa Smurf"); break;
  }
}

Here, we do use the MouseEvent-object, but we cast the target property to the type we expect it to have and then we need to check, if the cast went well. Because, technically we could have used the same function as a listener for something else somewhere else (even though we know we didn't) and thus target could have been something entirely different.

Personal experiences

I've just checked a recent, larger project with 185 (home-made) classes. In general I try to minimize my event usage, but a lot of things I have to use events for event though I don't really need them, so I ran a simple script counting how many times I had a function that took an event as an argument, and how many of these actually used the event object. The result was 121 functions accepting an event as argument (10 of those had default null values), and in only 4 of those I actually used the event argument (one of them a KeyboardEvent, the others were all the same home-made PageEvent). 83 of the events accepted were MouseEvents (which is no surprise), and never was the me:MouseEvent variable used in the function body.

Then, I tested an even earlier project from before I started minimizing event usage (almost maximized it back then). It consisted of 50 (home-made) classes. It had 113 functions (a lot more per class than the above) accepting event arguments (16 of those defaulting to null) and 74 of those were MouseEvents. And this time around, I used the event argument 25 times, but 18 of these usages were accessing event.target or event.data and casting it to the right class. Thus, yes I did use the event argument in some cases (22% of the time I accepted an argument), but in most of them I still had to do manual casting of the event.target (or event.data) attribute to get the type I knew I was accessing. This leaves only 7 "true" usages or about 6%. Please see the background section about why using the event object for casting only just moves the problem around and makes the code even more verbose.

Dream scenario

So, what would I like instead? Something intelligent, that allows me to accept the arguments I need when I need them. For instance, take this snippet here:

private var close:CloseButton;
public function init():void {
  close.addEventListener(MouseEvent.CLICK, closeDialog);
}
private function closeDialog():void {
  someDialog.fadeToOblivion();
}

It is simple, I do not need any argument, so I do not expect one.

A often used situation is when you have several instances of the same type and attach the same listener to all of these, then you use the event argument's target attribute for switching on which instance was invoked and handling the result correspondingly:

private var downloadLowRes:DownloadButton;
private var downloadMediumRes:DownloadButton;
private var downloadHighRes:DownloadButton;
public function init():void {
  downloadLowRes.addEventListener(MouseEvent.CLICK, download);
  downloadMediumRes.addEventListener(MouseEvent.CLICK, download);
  downloadHighRes.addEventListener(MouseEvent.CLICK, download);
}
private function download(db:DownloadButton):void {
  switch (db) {
    case downloadLowRes: open("file_360p.avi"); break;
    case downloadMediumRes: open("file_720p.avi"); break;
    case downloadHighRes: open("file_1080p.avi"); break;
  }
}

The function expects a reference to the button clicked - not to an event, who references the button clicked, because only the actual button reference is necessary.

Finally, a more elaborate example of different other component listeners expecting just what they need and nothing else:

public function init():void {
  // listen for selections in a dropdown
  countryDropdown.addEventListener(Event.SELECT, selectCountry);
 
  // listen for checkbox changes
  acceptTermsCheckbox.addEventListener(Event.CHANGE, acceptTerms);
 
  // listen for focus changes between password input fields
  passwordInput.addEventListener(FocusEvent.FOCUS_OUT, checkPasswords);
  passwordConfirmInput.addEventListener(FocusEvent.FOCUS_OUT, checkPasswords);
 
  // listen for three different radio buttons changing
  languageDanish.addEventListener(Event.SELECT, selectLanguage);
  languageSwedish.addEventListener(Event.SELECT, selectLanguage);
  languageNorwegian.addEventListener(Event.SELECT, selectLanguage);
 
  // listen for submit button presses
  submitButton.addEventListener(MouseEvent.CLICK, submit):
}
private function selectCountry(country:String):void {
  selectedCountry = country;
}
private function acceptTerms(accepted:Boolean):void {
  termsAccepted = accepted;
}
private function checkPassword(fe:FocusEvent):void {
  if (!(fe.relatedObject in [passwordInput, passwordConfirmInput])) {
    // focus moved away from either input field
    // show mismatch alert if texts are not the same
    passwordMismatchAlert.visible = passwordInput.text !== passwordConfirmInput.text;
  }
}
private function selectLanguage(language:LanguageRadioButton):void {
  switch (language) {
    case languageDanish: load("texts_da.xml"); break;
    case languageSwedish: load("texts_se.xml"); break;
    case languageNorwegian: load("texts_no.xml"); break;
  }
}
private function submit():void {
  sendForm(withParameters);
}

One notable example among the above is, that if an event is needed, you can always have it. I only dislike events, when I don't need them but have to accept them anyway. When I actually need it, they do come quite handy - like the FocusEvent.relatedObject used in the above example.

Announcement

Actually, I have made the above work! It is a dirty, dirty triple-hack, but it feels quite sweet to use on both ends. I need to clean it up a lot, need to come up with a good name for it and need to benchmark it to see, that it doesn't kill performance (too much).

So please, stay tuned. But do not fear commenting on the above ideas anyway.

No related posts.

Category: API, AS3, Programming, Trends 11 comments »

11 Responses to “Why events suck in ActionScript 3”

  1. fh

    http://github.com/robertpenner/as3-signals

    Use AS3 Signals API by R. Penner.

  2. Barklund

    @fh: I have checked out signals, yes. It has a lot of cool features, but my main feature is that you don’t have to have a 1-1 match between sent and received arguments. Signals still require that.

    But I’ll definitely borrow some ideas from signals once finishing up…

  3. WarPug

    It’s not clear to me why the event parameter is a problem, besides aesthetics. Is there a performance cost?

  4. Barklund

    Let’s call it purity. Code quality. API consistency. Call it what you want, I write the functions I need taking the arguments I need. See e.g. the topmost screen shot: if I have a public reset() method that other classes might use, I cannot use it directly from an event, because I will of course not contaminate my API with silly, unused arguments.

    Besides that, yes events are performance-wise expensive, but only a 10th of my projects are performance heavy, so that’s not the main issue.

  5. Frank

    Why do you say that casting is necessary for the event target?

    I found that using event.currentTarget instead of just the regular event.target fixes 90% of my problems. Without ever casting.

    I enjoyed this post, as I often use events simply to control the flow of an animation or game, without ever needing the event itself.

    Please post those benchmarks, as I’m very interested in seeing the results!

    Thanks

  6. Barklund

    Well, you can *not* cast – and then *not* have compile-time validation. But I prefer my code clean, i.e. as much compile-time validation as possible. But with events, there is no compile-time validation anyway, so if you do not have compile-time validation of whether or not your function accepts the correct event argument, you can just as well not have compile-time validation as to whether or not your functions accepts the correct runtime-decided arguments.

  7. Thomas James

    Perhaps you are using the Decorator pattern and you have a base class that you are extending with abstract decorator, and they in return extend by concrete decorators. The jungle grows and you are fiddling with some subclass when you look up at the method and wonder what the event object was in the first place. Having those unused arguments there would clarify rather than obfuscate that?

    But perhaps there could be a compelling benchmark argument for making the arguments optional. I really rely a lot on events. But perhaps there are ways of approaching designing things that rely less. That would be beyond my grasp which stretches from my sandwich to my coffe-brewer; but would be interesting to see a suggestion of how to escape an (over?) use of events.

  8. Eric Coker

    Here are a couple of potential solutions that come to mind.

    Option 1:
    Set the default value of the event parameter to null…
    public function reset(e:MouseEvent=null) {}
    Or to take it a step further, make the first parameter as generic as possible…
    public function reset(e:Object=null) {}

    Option 2:
    If you’re really pining for the old AS2 way of things you can always extend MovieClip and create your own properties for onClick, onMouseOver, onEnterFrame, etc. Set up all your listeners once in that class, and then use that class for all your clips.

  9. Barklund

    Hi Eric,

    Well, about the null or even the generic parameter, it does not solve anything – just makes everything uglier.

    And I don’t like the AS2 (or in general AVM1) way of doing events – it’s even worse than AS3.

  10. kontur

    I think it is good practice to keep events and methods seperated. Examples like your first, where an event triggers a method you might also want to invoke independent of the event should be as such seperated from the event handling code – like in your example.

    The obligatory event argument only helps you to keep in mind that actions and functionality should not be mingled, even though you might never refer to the event itself. Reusing the same handler function for different event triggering objects comes with differentiating the event causing object via the event target.
    Skipping the event, imo, is like trying to make sentences without verbs, you lose meaning at the expense of shortness.

    If you still insist on shortening your writing style, you could use the “… arguments” notation, which will make your function work called both, as event handler or without context. Just ignore handling the arguments passed, or if you like, include conditional checking for arguments passed, if you want to use the event, in case such is passed. Like:

    public function Test() {
    something.addEventListener(MouseEvent.CLICK, doSomethingSpecial);
    doSomethingSpecial();
    }

    private function doSomethingSpecial(…a):void {
    trace(“do stuff”);
    if (a[0] is MouseEvent) {
    trace(“invoked by clicking somewhere, do more stuff”);
    }
    }

    Maybe this helps – I have no clue about the performance implications of “…arguments”, but I’d wager they are minimal.

    k.

  11. Geoff

    Ever checked out Signals?

    https://github.com/robertpenner/as3-signals


Leave a Reply



Back to top

     

Get Adobe Flash playerPlugin by wpburn.com wordpress themes