Note: The following chapter is a preview excerpt
from O'Reilly's Essential ActionScript 2.0 by Colin Moock. You can
purchase the book on the O'Reilly website.
Ironically, Flash users who are new to object-oriented programming (OOP) are often familiar with many object-oriented concepts without knowing their formal names. This chapter demystifies some of the terminology and brings newer programmers up to speed on key OOP concepts. It also serves as a high-level overview of OOP in Flash for experienced programmers who are making their first foray into Flash development.
While Flash MX Professional 2004 is not required for this article, it is recommended:
In this chapter, we handled component events in two different ways:
With a generic listener object (in the case of the TextInput component):
var enterHandler:Object = new Object();
enterHandler.enter = function (e:Object):Void {
thisConverter.convert();
}
input.addEventListener("enter", enterHandler);
With an event handler function (in the case of the Button component):
convertButton.clickHandler = function (e:Object):Void {
thisConverter.convert();
};
Handling component events with generic listener objects in ActionScript 2.0
is somewhat analogous to handling Java Swing component events with anonymous
inner classes. In Swing, an anonymous instance of an anonymous inner class is
created simply to define a method that responds to a component event. In
ActionScript 2.0, an instance of the generic Object class is
created for the same reason (to define a component-event-handling method). But
in ActionScript 2.0, the anonymous class is not required because new methods can
legally be added dynamically to instances of the Object class at
runtime.
In general, the listener object approach is favored over the event handler
function approach, primarily because multiple listener objects can receive
events from the same component, whereas only one event handler function can be
defined for a component at a time. This makes listener objects more flexible and
scalable than event handler functions. Hence, Macromedia formally discourages
use of event handler functions. However, you’ll definitely see both approaches
thriving in the wild. The older v1 components that shipped with Flash MX did not
support listener objects, so all older v1 code uses event handler functions. The
v2 components support event handler functions for backward compatibility. Moving
forward, you should use listener objects rather than event handler functions.
That said, even if you’re not working with components, you’ll still encounter
event handler functions when working with the Flash Player’s built-in library of
classes. Many of the built-in classes, including MovieClip, Sound, XML, and XMLSocket use event
handler functions as their only means of broadcasting events.
As an alternative to defining an event handler function on a component
instance, you can also use a so-called listener function, which
logically lies somewhere between an event handler function and a listener
object. A listener function is a standalone function (i.e., a function not
defined on any object) registered to handle a component event. For example, our
earlier event handler function for the Convert Button component looked like
this:
convertButton.clickHandler = function (e:Object):Void {
thisConverter.convert();
}
The analogous listener function would be:
function convertClickHandler (e:Object):Void {
thisConverter.convert();
};
convertButton.addEventListener("click", convertClickHandler);
Listener functions are preferred over event handler functions because
multiple listener functions can be registered to handle events for the same
component. However, when using listener functions, you should be careful to
delete the function once it is no longer in use. Or, to avoid cleanup work, you
might simply pass a function literal to the addEventListener( ) method of the component in question, as follows:
convertButton.addEventListener("click", function (e:Object):Void {
thisConverter.convert();
});
However, using this function literal approach prevents you from ever removing the listener function from the component’s listener list. Hence, when registering for an event that you may later want to stop receiving, you should not use the preceding function literal approach.
Whether you’re using a listener object, an event handler function, or a
listener function, the fundamental goal is the same: to map an event from a
component to a method call on an object. For example, in our Convert button
example, we want to map the button’s click event to our CurrencyConverter object’s convert( ) method. Yet
another way to make that mapping would be to define a click( ) method on the CurrencyConverter class and register the CurrencyConverter instance to handle button click events. Here’s
the click( ) method definition:
public function click (e:Object):Void {
convert();
}
And here’s the code that would register the CurrencyConverter instance to receive click events from the Convert button:
convertButton.addEventListener("click", this);
In the preceding approach, because the click( ) method is called
on the CurrencyConverter instance, the click( ) method
can invoke convert( ) directly, without the need for the thisConverter local variable that was required earlier. However,
problems arise when the CurrencyConverter instance needs to respond
to more than one Button component’s click event. To differentiate between our
Convert button and, say, a Reset button, we’d have to add cumbersome if or switch statements to our click( ) method, as shown in the following code. For this example, assume that the
instance properties convertButton and resetButton have
been added to the CurrencyConverter class.
public function click (e:Object):Void {
if (e.target == convertButton) {
convert();
} else if (e.target == resetButton) {
reset();
}
}
Rather than forcing our CurrencyConverter class to handle
multiple like-named events from various components, we’re better off reverting
to our earlier generic listener object system, in which each generic object
could happily forward events to the appropriate methods on CurrencyConverter. For example:
// Convert button handler
var convertClickHandler:Object = new Object();
convertClickHandler.click = function (e:Object):Void {
thisConverter.convert();
}
convertButton.addEventListener("click", convertClickHandler);
// Reset button handler
var resetClickHandler:Object = new Object();
resetClickHandler.click = function (e:Object):Void {
thisConverter.reset();
}
resetButton.addEventListener("click", resetClickHandler);
To reduce the labor required to create generic listener objects that map
component events to object method calls, Mike Chambers from Macromedia created a
utility class, EventProxy. Using Mike’s EventProxy class, the preceding code could be reduced to:
convertButton.addEventListener("click", new EventProxy(this, "convert"));
resetButton.addEventListener("click", new EventProxy(this, "reset"));
The EventProxy class, shown in Example 12 2, does a good, clean
job of mapping a component event to an object method call. However, the
convenience of EventProxy comes at a price: reduced type checking.
For example, in the following line, even if the current object
(this) does not define the method convert( ), the
compiler does not generate a type error:
convertButton.addEventListener("click", new EventProxy(this, "convert"));
Hence, wherever you use the EventProxy class, remember to
carefully check your code for potential datatype errors. For more information on EventProxy, see: http://www.markme.com/mesh/archives/004286.cfm.
Example 12-2. The EventProxy class (continued)
class EventProxy {
private var receiverObj:Object;
private var funcName:String;
/**
* receiverObj The object on which funcName will be called.
* funcName The function name to be called in response to the event.
*/
function EventProxy(receiverObj:Object, funcName:String) {
this.receiverObj = receiverObj;
this.funcName = funcName;
}
/**
* Invoked before the registered event is broadcast by the component.
* Proxies the event call out to the receiverObj object's method.
*/
private function handleEvent(eventObj:Object):Void {
// If no function name has been defined...
if (funcName == undefined) {
// ...pass the call to the event name method
receiverObj[eventObj.type](eventObj);
} else {
// ...otherwise, pass the call to the specified method name
receiverObj[funcName](eventObj);
}
}
}
To avoid the type checking problem presented by the EventProxy class, you can use the rewritten version of Mike Chambers’ original class, shown
in Example 12 3. The rewritten version uses a function reference instead of a
string to access the method to which an event is mapped. Hence, to use the
rewritten EventProxy class, we pass a method instead of a string as
the second constructor argument, like this:
// No quotation marks around convert! It's a reference, not a string!
convertButton.addEventListener("click", new EventProxy(this, convert));
Because the convert( ) method is accessed by reference, the
compiler generates a helpful error if the method doesn’t exist.
Example 12-3. The Rewritten EventProxy class (continued)
class EventProxy {
private var receiverObj:Object;
private var funcRef:Function;
/**
* receiverObj The object on which funcRef will be called.
* funcName A reference to the function to call in response
* to the event.
*/
function EventProxy(receiverObj:Object, funcRef:Function) {
this.receiverObj = receiverObj;
this.funcRef = funcRef;
}
/**
* Invoked before the registered event is broadcast by the component.
* Proxies the event call out to the receiverObj object's method.
*/
private function handleEvent(eventObj:Object):Void {
// If no function name has been defined...
if (funcRef == undefined) {
// ...pass the call to the event name method
receiverObj[eventObj.type](eventObj);
} else {
// ...otherwise, pass the call to the specified method using
// Function.call().
funcRef.call(receiverObj, eventObj);
}
}
}
As evidenced by the sheer number of event-handling techniques just discussed, the v2 component-event-handling architecture is very flexible. But it also suffers from a general weakness: it allows type errors to go undetected in two specific ways.
First, any component’s events can be handled by any object of any class. The
compiler does not (indeed cannot) check whether an event-consuming object
defines the method(s) required to handle the event(s) for which it has
registered. In the following code, if the convertClickHandler object does not define the required click( ) method, no error
occurs at compile time:
var convertClickHandler:Object = new Object();
// Oops! Forgot the second "c" in "click," but no compiler error occurs!
convertClickHandler.clik = function (e:Object):Void {
thisConverter.convert();
}
convertButton.addEventListener("click", convertClickHandler);
In other words, in the v2 component architecture there’s no well-known manifest of the events a component broadcasts and no contract between the event source and the event consumer to guarantee that the consumer actually defines the events broadcast by the source.
Second, event objects themselves are not represented by individual classes. All event objects are instances of the generic Object class. Hence, if you misuse an event object within an event-handling method, the compiler, again, does not generate type errors. For example, in the following code (which, so far, contains no type errors), we disable a clicked button by setting the button’s enabled property to false via an event object. We access the button through the event object’s target property, which always stores a reference to the event source:
convertClickHandler.click = function (e:Object):Void {
thisConverter.convert();
e.target.enabled = false;
}
But if the programmer specifies the wrong property name for target (perhaps due to a typographical error or a mistaken
assumption), the compiler does not generate a type error:
convertClickHandler.click = function (e:Object):Void {
thisConverter.convert();
e.source.enabled = false; // Wrong property name! But no compiler error!
e.trget.enabled = false; // Oops! A typo, but no compiler error!
}
In addition to suppressing potential compiler errors, the lack of typed event objects in the v2 component architecture effectively hides the information those objects contain. If the architecture used formal event classes, such as, say, Event or ButtonEvent, the programmer could quickly determine what information is available for an event simply by examining the v2 component class library. As things stand, such information can be found only in the documentation (which may be incomplete) or in an event-broadcasting component’s raw source code (which is laborious to read).
One way to help make an application’s handling of v2 component events more
obvious is to define specific classes for event-consumer objects rather than
using generic objects. For example, to handle events for the Convert button, an
instance of the Button component, in our CurrencyConverter application, we might create a custom ConvertButtonHandler class as
follows:
import org.moock.tools.CurrencyConverter;
class org.moock.tools.ConvertButtonHandler {
private var converter:CurrencyConverter;
public function ConvertButtonHandler (converter:CurrencyConverter) {
this.converter = converter;
}
public function click (e:Object):Void {
converter.convert();
}
}
Then, to handle the Button component events for the Convert button, we’d use:
convertButton.addEventListener("click", new ConvertButtonHandler(this));
By encapsulating the button-event-handling code in a separate class, we make the overall structure of the application more outwardly apparent. We also isolate the button-handling code, making it easier to change and maintain. However, in simple applications, using a separate class can require more work than it’s worth. And it doesn’t alleviate the other event type checking problems discussed earlier (namely, the compiler’s inability to type check event-consumer objects and event objects).
In Java, every aspect of the Swing component event architecture includes type checking. Event consumers in Java must implement the appropriate event listener interface, and event objects belong to custom event classes. In simple Flash applications, Java’s additional component event architecture would be cumbersome and hinder rapid, lightweight development. However, for more complex situations, Java’s strictness would be welcome. Therefore, we’ll see how to implement Java-style events in ActionScript 2.0 classes in Chapter 19. And we’ll learn more about generating and handling custom user interface events in Chapter 18.
For reference, Table 12 1 summarizes the various component-event-handling techniques discussed in this chapter.
Generally, the preferred means of handling component events
var convertClickHandler:Object = new Object();
convertClickHandler.click = function (e:Object):Void {
thisConverter.convert();
}
convertButton.addEventListener("click", convertClickHandler);
Same as generic listener object, but exposes event-handling code more explicitly
convertButton.addEventListener("click", new ConvertButtonHandler(this));
Functionally the same as generic listener object, but more convenient and easier to read
convertButton.addEventListener("click", new EventProxy(this, "convert"));
or
convertButton.addEventListener("click", new EventProxy(this, convert));
The lesser evil of the two function-only event-handling mechanisms
convertButton.addEventListener("click", function
(e:Object):Void {
thisConverter.convert();
});
The least-desirable means of handling component events; discouraged by Macromedia
convertButton.clickHandler = function (e:Object):Void {
thisConverter.convert();
}
We’ve come to the end of our look at a component-based ActionScript 2.0 application. If you want to see the currency converter in action, you can download all the files discussed in this chapter from http://moock.org/eas2/examples/. For a lot more information on both using and authoring components, see Flash’s online Help (Help › Using Components) and the components section of Macromedia’s Flash Developer Center at http://www.macromedia.com/devnet/flash/components.html.
In the next chapter, we’ll continue our exploration of controlling and
creating visual assets by studying MovieClip subclasses.