Monday, September 12, 2011

Introspective Event Handling For Flex SkinnableComponents

So the Flex Spark architecture promotes the separation of a component model/controller from its view, or in Flex lingo, its skin. Here is the PITA process that I go throughout when creating a skinnable component manually:
  1. I create an ActionScript class (the host component) that subclasses SkinnableComponent.
  2. I define and annotate the skin parts. I try to define the skin part types to be as high as possible in the class hierarchy. What I mean by that is instead of defining a part to be of type Button, I make it of type ButtonBase.
  3. I override the partAdded function and add all the event listeners for each part, as event handling should be done in the host component not in the skin.
  4. I override the partRemoved function and remove all the added event listeners, as I want to be a “good citizen”.
  5. I create a subclass of Skin and associate it with the host component.
  6. I add the skin parts and any graphic elements to make it “pretty”.
  7. I “ClassReference” the skin to its host component as the default skin in the main application stylesheet.
  8. I Implement the content of the event listeners.
  9. Done, to the next skinnable component.
Told you was PITA! So here is what a very simple skinnable component looks like:
package com.esri.views {
import flash.events.MouseEvent;
import mx.controls.Alert;
import spark.components.supportClasses.ButtonBase;

public class MySkinnableComponent extends SkinnableComponent{
    [SkinPart]
    public var myPart:ButtonBase;

    public function MySkinnableComponent(){
    }

    override protected function partAdded(partName:String, instance:Object):void {
      super.partAdded(partName, instance);
      if( instance === myPart) {
        myPart.addEventListener(MouseEvent.CLICK,myPart_clickHandler);
      }
    }

    override protected function partRemove(partName:String, instance:Object):void {
      super.partAdded(partName, instance);
      if( instance === myPart) {
        myPart.removeEventListener(MouseEvent.CLICK,myPart_clickHandler);
      }
    }

    public function myPart_clickHandler(event:MouseEvent):void {
      Alert.show('myPart_clickHandler');
    }
}
}
Pretty Eh ? When programming, I do believe in DRY and if something is “boilerplate”, then it should be "templated". In the above, what is really the PITA, is the monkey-coding of adding and removing event listeners for each added and removed part. Talk about repeating yourself ! What if we could automate that process with convention and very minimal configuration. This will enable me to focus on the fun part which is the skinning and styling, and on the money making part which is the logic. Now, please note my event handlers:
	public function myPart_clickHandler(event:MouseEvent):void
This naming convention says a lot; this is an event handler for a part named “myPart” and it is handling the “click” event whenever it is dispatched. Cool eh ? See, using this convention, a colleague can look at this “self-documented” function and figure out what is going on at that line of code. So how to make this set of functions with this convention be automagically “hooked” and discovered by the running application ? Enter metadata! So with minimal configuration, I can now have:
	[SkinPartEventHandler]
	public function myPart_clickHandler(event:MouseEvent):void
The discovery and handling of these functions can now be done for any skinnable component in a templated way by overriding the partAdded function using the amazing as3-commons-reflect reflection library:
override protected function partAdded(
  partName:String,
  instance:Object
  ):void
{
  super.partAdded(partName, instance);
  for each (var method:Method in m_type.methods){
    const metadataArray:Array = method.getMetadat("SkinPartEventHandler");
    if (metadataArray && metadataArray.length){
      const metadata:Metadata = metadataArray[0];
      const tokens:Array = method.name.split("_");
      const localName:String = tokens[0];
      if (localName === partName){
        const eventHandler:String = tokens[1];
        const eventType:String = eventHandler.substr(0,
             eventHandler.indexOf("Handler"));
        instance.addEventListener(eventType, this[method.name]);
      }
    }
  }
}
So what is happening here ? As each skin part is added, we look for all the method in this class that are annotated with SkinPartEventHandler. Based on the agreed convention, the name of each matching method can be split into two tokens using the underscore character as a separator. If first split token matches the added part name, then we can get the event type from the second token which is the string that is prefixing the ‘Handler’ text. So now, we can add the matching method as a listener to that added instance for that event type. Cool ? I think so too ! Come to think about it, All event handling in Flash/Flex should be done with metadata and convention. Oh well! Here is a FlashBuilder project that you can download and see how this is implement and for you to DRY.

I am leaving the MOST important part for last, make sure to add "-keep-as3-metadata+=SkinPartEventHandler" to your "Additional compiler arguments" in the "Flex Compiler" under your project properties, or else this special metadata will not be compiled into the class definition by default.

2 comments:

zach said...

I love it..I think it is very simple and make sense to use...will try to implement but still waiting for signals...

PythonFan said...

The component you created is great. However I need it to be able to do the same type of skinning and event handling for a panel. Any hints or clues would be extremely helpful