DekGenius.com
[ Team LiB ] Previous Section Next Section

Recipe 12.11 Adding Listeners to Custom Classes

12.11.1 Problem

You want your objects of a custom class to generate listener events and support listener objects.

12.11.2 Solution

Write an addListener( ) method for your custom class. Use the watch( ) method to watch a property. When the property is modified, call all listeners.

12.11.3 Discussion

Listeners are a convenient way to automatically trigger methods on one or more objects when a property is modified within the object being listened to. Listeners are available for some classes of objects such as Key, Mouse, and TextField (see Table 12-1). Although ActionScript does not provide listeners for all objects, you can use the watch( ) method to generate listener events in response to property changes.

The watch( ) method requires at least two parameters: a string value that is the name of the property to watch and a reference to a callback function to call when the property is changed. (A callback function is simply a function that is called in response to a particular trigger.) The callback function is always automatically passed at least three values: the property name, the old value, and the new value. If the callback function returns true, the new value is assigned to the property, and if it returns false, the old value is assigned to the property. The watch( ) method can also be used for other purposes, such as implementing logic in the callback function to determine which value (old or new) should be assigned to the property. For the purposes of creating listener functionality, your callback function should always return true because you don't want to interfere with the setting of the property value (you only want to detect the change so that your code can trigger a method on the listener objects).

To add listener functionality to a class, you also need to create methods for adding and removing listeners. The addListener( ) method takes a reference to an object and adds it to an array property. The removeListener( ) method also takes a reference to an object, which it removes from the array, if found. Here is an example of how to create listener functionality for a custom class and how to use it:

// Create a constructor for a class, MyClass.
_global.MyClass = function (val) {

  // Store the value passed to the constructor in a property (myProperty). 
  this.myProperty = val;

  // Tell the object to watch for any changes to myProperty. When a change occurs,
  // call the object's propSet(  ) method.
  this.watch("myProperty", this.propSet);
};

// The propSet(  ) method is the callback function invoked automatically when
// myProperty's value changes. The callback is passed the property name 
// ('myProperty' in this case) plus the old value and the proposed new value.
MyClass.prototype.propSet = function (prop, oldVal, newVal) {

  // Call onMyPropertyChange(  ), if it exists, for each listener object. Pass that
  // method the old value and new value.
  for (var i = 0; i < this.listeners.length; i++) {
    this.listeners[i].onMyPropertyChange(oldVal, newVal);
  }

  // Return the new value so that it is assigned to the property. This is very
  // important. Without this line, myProperty becomes undefined.
  return newVal;
};

// The addListener(  ) method adds a listener object to an array of listeners.
MyClass.prototype.addListener = function (listener) {

  // If the listeners array property does not exist, create it.
  if (this.listeners == undefined) {
    this.listeners = new Array(  );
  }

  // Add the new listener to the existing array of listeners.
  this.listeners.push(listener);
};

// The removeListener(  ) method removes a listener object from the listeners array.
MyClass.prototype.removeListener = function (listener) {

  // Remove the listener from the listeners array, if it is found.
  for (var i = 0; i < this.listeners.length; i++) {
    if (this.listeners[i] == listener) {
      this.listeners.splice(i, 1);
      break;
    }
  }
};

// Create a listener object. It must have a method named onMyPropertyChange(  ) to
// listen effectively (because that's what the propSet(  ) method tries to call).
obj = new Object(  );
obj.onMyPropertyChange = function (oldVal, newVal) {
  trace(oldVal + " " + newVal);
};

// Create a new instance of MyClass and add obj as a listener on the instance.
myClassInstance = new MyClass(6);
myClassInstance.addListener(obj);

// Each time myProperty is set, the callback function (propSet(  )) invokes the
// onMyPropertyChange(  ) method of each listener object.
myClassInstance.myProperty = 24;
myClassInstance.myProperty = 42;
myClassInstance.myProperty = 36;

The preceding code gives you an idea of the process involved in adding listeners to objects. However, it is not as versatile as it could be. The primary issue is that it requires you to define the listener code for every class to which you want to add listeners. There is a better way.

All ActionScript classes inherit from the Object class. Therefore, if you add the listener code to the Object class, all classes automatically inherit it. The only real difference when you add the listener code to the Object class is that you cannot rely on having access to each class's constructor as you did in the preceding example. Therefore, you should create another method, startListeners( ), that initiates the watch for a watched property. The startListeners( ) method should accept the name of the property to watch as a parameter. For future use, add the following code to an external Object.as file and include it in any project for which you want to implement custom listener functionality:

// The startListeners(  ) method calls watch(  ) to start watching 
// a property. If you want an object to listen for multiple 
// properties, call startListeners(  ) for each property.
Object.prototype.startListeners = function (propName) {
  this.watch(propName, this.propSet);
};

// The addListener(  ) and removeListener(  ) methods are the same as the preceding
// example, except that they are applied to the Object class.
Object.prototype.addListener = function (listener) {
  if (this.listeners == undefined) {
    this.listeners = new Array(  );
  }
  this.listeners.push(listener);
};

Object.prototype.removeListener = function (listener) {
  for (var i = 0; i < this.listeners.length; i++) {
    if (this.listeners[i] == listener) {
      this.listeners.splice(i, 1);
      break;
    }
  }
};

// The propSet(  ) method is the callback function for the watched properties. This
// method is slightly more generic than in the preceding example, insofar as it
// allows for any property to be watched.
Object.prototype.propSet = function (prop, oldVal, newVal) {

  // Loop through all the listeners.
  for (var i = 0; i < this.listeners.length; i++) {

    // This calls the onPropertyNameChange(  ) method on each listener, where
    // PropertyName is given by the prop parameter.
    this.listeners[i]["on" + prop + "Change"](oldVal, newVal);
  }

  // Return the new value so it is properly assigned to the property.
  return newVal;
};

Here is an example that uses our new listener code:

#include "Object.as"

// Define a custom class. This time, the class does not need any of the listener code
// because it automatically inherits it from the Object class.
_global.MyClass = function (val0, val1) {
  this.myProperty0 = val0;
  this.myProperty1 = val1;
};

// Create a listener object. Define methods named onMyProperty0Change(  ) and
// onMyProperty1Change(  ) so the object can listen for changes to the watched
// properties named myProperty0 and myProperty1.
obj = new Object(  );
obj.onMyProperty0Change = function (oldVal, newVal) {
  trace("myProperty0: " + oldVal + " " + newVal);
};
obj.onMyProperty1Change = function (oldVal, newVal) {
  trace("myProperty1: " + oldVal + " " + newVal);
};

// Create a new instance of MyClass.
myClassInstance = new MyClass(6, 9);

// Add obj as a listener. Start listeners for myProperty0 and myProperty1.
myClassInstance.addListener(obj);
myClassInstance.startListeners("myProperty0");
myClassInstance.startListeners("myProperty1");

// The following changes to the properties invoke the listener methods
// onMyProperty0Change(  ) and onMyProperty1Change(  ).
myClassInstance.myProperty0 = 24;
myClassInstance.myProperty0 = 42;
myClassInstance.myProperty1 = 36;

/* The results displayed in the Output window are as follows:
   myProperty0: 6 24
   myProperty0: 24 42
   myProperty1: 9 36
*/

Be aware that Flash cannot watch some properties, so you cannot add listeners in the following cases:

Legacy Flash 4 properties

Properties available in Flash 4, such as _x, _y, _width, _height, etc., cannot be watched. These properties are listed in the Actions Toolbox under the Properties folder.

Getter/setter properties

You cannot create custom listeners for any custom getter/setter properties you have defined (see Recipe 12.6). You cannot add listeners for any of the built-in getter/setter properties shown in Table 12-2.

Properties inherited from the object's prototype chain

A property must be defined on the object itself before you can watch it. For example, if you add a custom property, myCustomProperty, to the Object class, then that property cannot be watched for an instance of the Object class. In practice, this is generally a moot point. As soon as you assign a new value to the instance's property, you define the property for the instance, and it can be watched. If you have previously called the watch( ) method to watch the property, once you define the property for the actual instance, the callback function is invoked. For example:

// Add a custom property to the Object class.
Object.prototype.myCustomProperty = "This cannot be watched.";

// Create a new object.
myObject = new Object(  );

// Define a watch callback function.
myObject.myCustomPropertyCallback = function (prop, oldVal, newVal, useData) {
  trace(prop + " changed from " + oldVal + " to " + newVal);
};

// At first, Flash cannot watch myCustomProperty because it is inherited.
myObject.watch("myCustomProperty", this.myCustomPropertyCallback);

// Now, myCustomProperty can be watched, because it has been assigned on the
// object instance directly.
myObject.myCustomProperty = "This will be watched.";

Table 12-2 lists the getter/setter properties that cannot be watched, but it does not list all unwatchable properties. Note that Flash 4 legacy properties such as the _x, _y, _width, and _height movie clip properties also cannot be watched.

Table 12-2. Getter/setter properties, which cannot be watched

Object

Getter/setter properties

Button

tabIndex

Camera

All properties

Microphone

All properties

MovieClip

tabIndex

NetStream

All properties

Sound

duration

position

Stage

All properties

TextField

All properties

TextFormat

All properties

Video

All properties

XML

contentType

docTypeDecl

ignoreWhite

loaded

status

xmlDecl

XMLnode

attributes

childNodes

firstChild

lastChild

nextSibling

nodeName

nodeType

nodeValue

parentNode

previousSibling

12.11.4 See Also

Recipe 12.10. Also refer to the Object.watch( ) entry in the "Language Reference" section of ActionScript for Flash MX: The Definitive Guide, from which Table 12-2 is reproduced.

    [ Team LiB ] Previous Section Next Section