Thursday, November 12, 2009

Flash / ActionScript 3: Event Bubbling Example

Previously I wrote an article that showed an example of Event Bubbling with Flex. I wanted to write a similar example for ActionScript 3. The principle is the same, but it works just a little different.

Any class involved in bubbling must extend DisplayObjectContainer. Sprite is the most basic DisplayObjectContainer class and most examples I see use it, so that's what I will use. I gave a brief overview of how event bubbling works in the Flex example that I won't repeat here.

I'm creating three classes that include each other. The primary class includes Level1. Level1 includes Level2. Level2 includes Level3. Level3 triggers an event when it's added to the stage. The event from Level3 will bubble through Level2 and Level1 to the primary class without re-dispatching it.

event_bubbling_as.as
package {
    import flash.display.Sprite;
    import flash.events.Event;
    
    import obj.Level1;
    import obj.Level3;

    public class event_bubbling_as extends Sprite
    {
        private var _level1:Level1
        
        public function event_bubbling_as()
        {
            _level1 = new Level1;
            _level1.addEventListener(Level3.EVENT, onEvent);
            
            addChild(_level1);
        }
        
        private function onEvent(event:Event) : void {
                trace('caught event from level 3');
        }
    }
}

Level1.as
package obj
{
    import flash.display.Sprite;
    
    public class Level1 extends Sprite
    {
        private var _level2:Level2;
        
        public function Level1()
        {
            trace("in level 1");
            _level2 = new Level2;
            
            addChild(_level2);
        }
    }
}
Level2.as
package obj
{
    import flash.display.Sprite;
    
    public class Level2 extends Sprite
    {
        private var _level3:Level3;
        
        public function Level2()
        {
            trace("in level 2");
            _level3 = new Level3;
            
            addChild(_level3);
        }
    }
}
Level3.as
package obj
{
    import flash.display.Sprite;
    import flash.events.Event;
    
    public class Level3 extends Sprite
    {
        public static const EVENT:String = "level3";
        public function Level3(){
            trace("in level 3");
            
            //trigger event when the object is added to the stage
            addEventListener(Event.ADDED_TO_STAGE, launchEvent);
        }
        
        public function launchEvent(event:Event) : void {
         var newEvent:Event = new Event(EVENT, true, true);
         dispatchEvent(newEvent);
        }
    }
}
You can download the Flex Project Archive example here. This can be directly imported into Flex. Run it in debug mode to see how each level is triggered.

Labels: , , , , , , , , , , , , , , , , ,

Sunday, November 1, 2009

Flex: Event Bubbling Example

A refactor it gave me a chance to update the way I handled events. This page on Adobe's site explains the event bubbling model. To give a brief overview there are three phases: Capture, Targeting and Bubbling. The Capture phase will pass through each branch of a DisplayObject tree until it reaches the last node. The Targeting phase will look for objects that have event listeners bound to them. The Bubbling phase ascends the DisplayObject tree from the last node to the first node and activates the event listeners. Only DisplayObjects have a Capture and Bubbling phase.

Here is some example code I made in MXML that shows how event bubbling works. There are four files. The Application file includes Layer1. Layer1 includes Layer2. Layer2 includes Layer3. When Layer3 is created it will throw an event. The first property of the event is the string that will trigger event listeners, the second allows the event to bubble through the DisplayObject tree. The third allows the event to be canceled at any of the DisplayObjects it passes through.  The event will bubble through each DisplayObject and back to the Application without manually passing it forward.

UPDATE
If you're looking for an example of event bubbling in ActionScript please view this article.

index.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" initialize="init()" styleName="plain">
    <mx:Script>
        <![CDATA[
            import obj.Level3;
            import obj.Level1;
            
            private var _level1:Level1;
            
            private function init() : void {
                trace("in the base");
                _level1 = new Level1;
                
                //add eventListener
                _level1.addEventListener(Level3.EVENT,onEvent);
                
                addChild(_level1);
            }
            
            private function onEvent(event:Event) : void {
                trace("picked up event from Level3 ");
            }
        ]]>
    </mx:Script>
</mx:Application>


Layer1.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml" width="400" height="300" initialize="init()">
    <mx:Script>
        <![CDATA[
            private var _level2:Level2;
            private function init() : void {
                trace("in level 1");
                _level2 = new Level2;
                addChild(_level2);
            }
        ]]>
    </mx:Script>
</mx:Canvas>

Layer2.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml" width="400" height="300" initialize="init()">
    <mx:Script>
        <![CDATA[
            private var _level3:Level3;
            private function init() : void {
                trace("in level 2");
                _level3 = new Level3;
                addChild(_level3);
            }
        ]]>
    </mx:Script>
</mx:Canvas>

Layer3.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml" width="400" height="300" initialize="init()">
    <mx:Script>
        <![CDATA[
            public static const EVENT:String = "level3";
            private function init(): void {
                trace("in level 3");
                var event:Event = new Event(EVENT,true,true);
                dispatchEvent(event);
            }
        ]]>
    </mx:Script>
</mx:Canvas>

You can download the Flex Project Archive example here. This can be directly imported into Flex. Run it in debug mode to see how each level is triggered.

Labels: , , , , , , , , , , ,

Thursday, October 29, 2009

Flex: Problems reusing the HTTPService class

UPDATE: This problem occurred because of a bug in the 3.4 SDK. This bug does not exist in 3.2 or lower. I've opened a bug with adobe on the issue.

UPDATE 2: The issue has been resolved in a new nightly build of the SDK. It should be fixed when 3.6 is release.

I ran into an issue using the HTTPService object that involved removing event listeners that were added inside MXML.

Here is a snippet of code in the project. This will open an xml file and load some data into a List.

//this will fetch all our content.
private var _httpService:HTTPService = new HTTPService; 

private function init() : void {
    _httpService.url = "stub/data/albums.xml";
    _httpService.resultFormat = "e4x";
    
    //set up http call
    _httpService.addEventListener(ResultEvent.RESULT, populateList);
    _httpService.send();
}

private function populateList(event:ResultEvent) : void {
    //set the data provider
    albumsComboBox.dataProvider = event.result.album;
    
    //remove listener
    _httpService.removeEventListener(ResultEvent.RESULT, populateList);
}

This will open another xml file and populate the TileList with images.

private function loadAlbum(event:ListEvent) : void {
    _httpService.url = "stub/data/" + event.currentTarget.selectedItem.@images;
    _httpService.resultFormat = "e4x";
    
    //set up http call
    _httpService.addEventListener(ResultEvent.RESULT, populateTilelist);
    _httpService.send();
}

private function populateTilelist(event:ResultEvent) : void {
    //set the data provider
    imageGrid.dataProvider = event.result.photo;
    
    //remove listener
    _httpService.removeEventListener(ResultEvent.RESULT, populateTilelist);
}

When loadAlbum calls send() it will call populateList first and then goto populateTilelist. removeEventListener didn't work. I posted a question to StackOverflow with the problem and got some great answers.


To get around this problem I created a HTTPService wrapper that can be reused with no issues because it's an ActionScript Class. It's very light weight, doesn't include everything under the sun, but it gets the job done in a pinch. This is the 3rd revision of the class. It's been cleaned up and there are fewer methods now.

package util
{
    import mx.controls.Alert;
    import mx.rpc.AsyncResponder;
    import mx.rpc.AsyncToken;
    import mx.rpc.IResponder;
    import mx.rpc.events.FaultEvent;
    import mx.rpc.events.ResultEvent;
    import mx.rpc.http.HTTPService;
    
    public class HTTPServiceWrapper {
        //reference for connections
        private var _httpService:HTTPService;
        
        private var _alertTitle:String = "HTTPService: An error occured";
        
        // Holds all the tokens and callbacks
        private var _processingQueue : Object = {};
        
        /**
         * Create the HTTPService 
         * 
         */
        public function HTTPServiceWrapper() : void {
            //create the service
            _httpService = new HTTPService;
        }
        
        /**
         * Get content from URL 
         * @param url
         * @param resultFormat
         * @param callBack
         * @param request
         * @param returnErrorEvent
         * 
         */
        public function getContent(url:String,
                                    resultFormat:String,
                                    callBack:Function,
                                    request:Object = null, 
                                    returnErrorEvent:Boolean = false) : void {
            var asyncToken:AsyncToken;
            var internalIResponder:IResponder;
            
            //set the url and result format
            _httpService.url = url;
            _httpService.resultFormat = resultFormat;
            
            //check for key/value pairs to be sent in the url
            if(request) {
                _httpService.request = request;
            }
            
            //send the request
            asyncToken = _httpService.send();
            internalIResponder = new AsyncResponder(onGetContentHandler,onFault, asyncToken);
            asyncToken.addResponder(internalIResponder);
            
            //give token unique ID
            asyncToken = tokenID(asyncToken);
            
            _processingQueue[asyncToken.ID] = new QueueObject(callBack,returnErrorEvent);
        }
        
        /**
         * This is the handler event for getContent. The callBack should accept a ResultEvent
         *  
         * @param event
         * @param token
         * 
         */
        private function onGetContentHandler(event:ResultEvent, token:AsyncToken) : void {
            var queueObject:QueueObject = _processingQueue[token.ID];
            
            queueObject.callBack(event);
        }
        
        
        /**
         * This is the fault event for the class. The callBack should accept a null ArrayCollection and a FaultEvent if
         * you want to handle the error.
         *  
         * @param event
         * @param token
         * 
         */
        private function onFault(event:FaultEvent, token:AsyncToken) : void {
            var queueObject:QueueObject = _processingQueue[token.ID];
            
            //handle the fault
            if(queueObject.returnErrorEvent){
                queueObject.callBack(null, event);
            }
            else {
                var displayMessage:String = event.fault.faultString;
                displayError(displayMessage);
                queueObject.callBack(null);
            }
        }
        
        
        /**
         * This will add a unique identifier to a token 
         * @param token
         * @return 
         * 
         */
        private function tokenID(token : AsyncToken) : AsyncToken {
            token.ID = Math.random();
            return token;
        }
        
        /**
         * Displays the error on a fault event in the content service 
         * @param event
         * 
         */
        private function displayError(displayMessage : String) : void {
            Alert.show(displayMessage, _alertTitle, Alert.OK);
        }
    }
}

class QueueObject{
    public var callBack:Function;
    public var returnErrorEvent:Boolean;
    
    public function QueueObject(callBack:Function, returnErrorEvent:Boolean){
        this.callBack = callBack;
        this.returnErrorEvent = returnErrorEvent;
    }
}


Using the class is really simple. Use getContent and pass it the file you want to load, the format it should be loaded in and the call back you want it to call after the file is retrieved. There are two extra properties I'm not using right now, but they will be helpful in the future. The fourth parameter accepts an Object of key/value pairs to be sent with the URL.The fifth parameter lets you capture the result of an error if one occurs. The call back should accept a FaultEvent object if you want to capture the error. If you let HTTPServiceWrapper handle the error an Alert window will pop up showing the error.
//http wrapper
private var _httpServiceWrapper:HTTPServiceWrapper = new HTTPServiceWrapper;

/**
* Runs when the application loads
* */
private function init() : void {
    //get the list of albums
    _httpServiceWrapper.getContent("stub/data/albums.xml", "e4x", populateList);
    
    //set event listeners for app
    albumsComboBox.addEventListener(ListEvent.CHANGE, loadAlbum);
}

/**
* This will populate the album list
* */
private function populateList(event:ResultEvent) : void {
 //if the result is null don't set it
 if(!event) {
  return;
 }
 
 //set the data provider
 albumsComboBox.dataProvider = event.result.album;
}

/**
* This will fetch the selected album
* */
private function loadAlbum(event:ListEvent) : void {
    _httpServiceWrapper.getContent("stub/data/" + event.currentTarget.selectedItem.@images, "e4x", populateTilelist);
}

This cut quite a bit of bloat out of my app and is also very reusable which saves time in the long run. The final version of this code is hosted here. You can view source on it by right-clicking in the app and selecting "View Source". Please let me know if you have any questions about it.

Labels: , , , , , , , , , ,

Thursday, August 6, 2009

Flex: Passing a reference from a mxml component to a method or function

I ran into a situation where I needed to pass a reference from a mxml component to a function. It's easy if you have an ID assigned to the component, but if it's in an itemRenderer that makes it tougher because you don't want duplicate ID's.

You can pass a reference from a component to a function using the createComplete event.







You can also see another example at this blog post.

Labels: , , , , , , ,

Monday, August 3, 2009

Flex: Event Listeners and Array Collections

While working with some code I ran into a situation where I needed to know when an ArrayCollection was updated. Normally an event is triggered when using addItem, but I needed to know when it was set to another ArrayCollection. It's not very obvious, but it is possible.

ArrayCollection is really a wrapper for an Array. You can access the "real" array in an ArrayCollection by using ".source". When you set the source property an event called "listChanged" is triggered. You can watch for this event and then do whatever you need.

So this

var newAC1:ArrayCollection = new ArrayCollection;
var newAC2:ArrayCollection = new ArrayCollection;
newAC1 = newAC2;


Is the same as this, but with an event trigger.

var newAC1:ArrayCollection = new ArrayCollection;
var newAC2:ArrayCollection = new ArrayCollection;

newAC1.addEventListener('listChanged', function(event:Event){trace('changed');});

newAC1.source = newAC2.source;

Labels: , , , , , ,