Sunday, March 21, 2010

Removing mxml/inline event listeners

If you are thinking that why does this topic deserve a blog than I highly encourage you to read further and I promise you a little bit of Flex enlightenment.

How do you remove the click event listeners of myButton below...

<mx:Button id="myButton" click="clickHandler(event)">

if your answer is...

myButton.removeEventListener(MouseEvent.CLICK, clickHandler, false);

than you are in for a bit of surprise because this will not remove the event listener.

UIComponentDescriptors:
The Flex compiler generates actionscript code from MXML. It generates UIComponentDescriptors for mxml components which are considered documents. In one line, a document is a class that ends in ".mxml". The compiler generates AS3 code for ".mxml" files. The descriptors are bunch of nested objects of type Function, UIComponentDescriptor, Object and Class. A UIComponentDescriptor is used by actionscript at runtime to create the mxml component and its children and grand children. They also contain information regarding the various mxml/inline event listeners e.g. the click event listener for the myButton above. In this blog, we will focus on as3-generated event listeners.

Here is what an mxml event listener converted to as3-generated looks like. The code below is partial code copied from an as3-generated file.
Code 1:

new mx.core.UIComponentDescriptor({
type: mx.controls.Button
,
id: "myButton"
,
events: {
click: "__myButton_click"
}
,
propertiesFactory: function():Object { return {
label: "My Button"
}}
})

Explanation:
I have a button called "myButton" and on click of that button I want to invoke a method called "__myButton_click". Hold it, my event listener was "clickHandler" and not "__myButton_click". The compiler created a "PUBLIC" wrapper function and calls "clickHandler" from that wrapper function. This is what the wrapper function looks like.
Code 2:

public function __myButton_click(event:flash.events.MouseEvent):void
{
clickHandler()
}

The question is, why does the compiler do this. My educated guess: Many times, developers write actionscript code in the mxml/inline event e.g...

<mx:Button id="myButton" click="myIntVar = 0; myBooleanVar = false">

To handle this situation, the Flex compiler wraps all event listener code in a generated function. This design pattern takes care of many different types of event listeners (inline actionscript code, anonyomous function as an event listener, a function that already exists in the document, a listener function that is private etc).

Now you see why the removeEventListener() will not work. So the question is, how do I stop listening to an inline event listener. Let's take a look at this demo.

Click here for a demo
View source: Right click demo, or download from here.


Some details to know about this application...

  • In the above example, Application, EmployeeAddress.mxml, EmployeeBasicDetails.mxml and EmployeeDetails.mxml are documents and their isDocument property will be true.
  • The EmployeeXXX.mxml contain TextInput controls which have event listeners for valueCommit. In the eventListeners we invoke a static method in the Util class to dispatch a custom event and also to update the log.
  • EmployeeDetails contains EmployeeAddress and EmployeeBasicDetails and some other mx controls. It also declares an event "eventFromComponent" which the application listens to and shows in the log TextArea.


How to use the demo ...
1. Tab through all the TextInput's and you will see 2 logs for every tabbing. One log from the Util class when dispatching the event and one from the Application event listener after listening to the event. Both will have the same counter number.
2. Click on the button to remove all inline event listeners.
3. Tab through all the TextInput's again. No logs will be created except for Address textinput (which I will explain later).
4. Check the logs below. Upto log #6 were generated in step 1. Log #7 was generated in step 3.
This is what the logs look like...

#7:dispatching 'eventFromComponent'
#6:listened 'eventFromComponent'
#6:dispatching 'eventFromComponent'
#5:listened 'eventFromComponent'
#5:dispatching 'eventFromComponent'
#4:listened 'eventFromComponent'
#4:dispatching 'eventFromComponent'
#3:listened 'eventFromComponent'
#3:dispatching 'eventFromComponent'
#2:listened 'eventFromComponent'
#2:dispatching 'eventFromComponent'
#1:listened 'eventFromComponent'
#1:dispatching 'eventFromComponent'

Removing event listener code is in the Util class. The entry method to remove the inline event listeners is...
"disposeInlineEventListeners(disposeUIComponent:UIComponent, disposeChildDocumentEventListeners:Boolean):void {"

I will skip a detailed explanation of the component descriptors but here are some bullet points.

  • A component descriptor can contain child component descriptors.
  • It contains the inline event listener related information and that is what my functions use to remove the event listeners.
  • An UIComponent has a property called "descriptor". Container's also have a property called "childDescriptors" and "mx_internal::_documentDescriptor".
  • A Container has a method called "createComponentsFromDescriptors" which utilizes the "mx_internal::_documentDescriptor" to create its children.
  • The _documentDescriptor is mutable and during processing and children creation, the _documentDescriptor is modified hence it is not very valuable post children creation.
  • The inline event listener information is scattered in descriptor/childDescriptors of the document and its children.
  • Hence we use the "descriptor/childDescriptors" of the document and of the children to remove the inline event listeners.
  • A UIComponent has a property called "isDocument" which determines if a component is a document or not.


In the above demo, click in the Address TextInput and than tab out. An event is dispatched and the logs show that. Why are we left with this event listener?
Answer, Look at "Code 1:" above. The componentDescriptor contains an "id" property. My remove inline event listener code uses this property to get a handle to the object to remove the event listener. If an "id" is not provided in the mxml control declaration than the descriptor.id will be null and the remove event listener will fail. The Address TextInput does not have an id hence the remove event listener fails.
Note: In some cases, I have noticed that the compiler provided a generated id. I am not sure under what circumstances.

If you would like to examine the as3 generated files than you can get it from here.

I will be writing soon on as3-generated files for mxml components. Do not hesitate to ask a question if you have any doubts or questions.

Labels: , , ,

8 Comments:

At March 26, 2010 at 12:20 AM , Blogger Ike Blogs said...

Hey Pj

Really nice stuff, and very eager to know a solution on this.

working hard to get one.

But found a work around adding a

button.addEventlistener(mouseEvent.click, buttonOnClick);

on the code will not create the inline functions

Thanks

IKE

 
At March 26, 2010 at 4:37 PM , Blogger sunild said...

Hi Prashant, good to see that you're blogging!

I don't think there is a way to remove inline event listeners. I haven't dug into the generated code that deeply.

A similar problem to this is to remove a listener when the listener is an anonymous function. The solution here is to use the arguments object that is accessible inside every function. You can use arguments.callee to get a reference to the listener function and remove it:

this.addEventListener(MouseEvent.CLICK,
function {
this.removeEventListener(MouseEvent.CLICK, arguments.callee);
// normal listener code here
} );

But if you try to use arguments.callee to remove an inline event listener, it doesn't work :(

Please do share if you find a solution to this!

 
At March 26, 2010 at 4:40 PM , Blogger sunild said...

oops, got the syntax slightly wrong above, but you get the idea :P

 
At March 26, 2010 at 7:07 PM , Blogger Prashant Jain said...

Just haven't had time from my busy schedule to post the solution and the source code. But I promise you will have a solution by eod tomorrow.

Prashant

 
At March 29, 2010 at 7:33 AM , Blogger Prashant Jain said...

SudnilD/Ike,
As promised, the source code and explanation is all here. Let me know if you have any questions.

Prashant

 
At June 17, 2010 at 6:21 PM , Blogger ven512 said...

Hi Prashant,

Can you provide some sample code for displaying the custom component in the actionscript repeater.. bcoz.. I facing some issue in displaying the custom component in a VBox container through the Repeater.

Thanks in advance.

-Ven.

 
At September 9, 2010 at 8:18 AM , Anonymous Anonymous said...

Simple workaround:

If you know ahead of time that you'll have to kill an event, and you have control of the mxml, then make a smart listener that checks for a global flag (such as a custom settings object on the Application singleton). If the flag is off, then the listener won't do its intended work when fired.

var appSettings:ApplicationSettings; //*ALWAYS* cast your custom objects to a defined class type.
...
<mx:Button click="if (Application.application.appSettings.buttonEventAllowed) onClick(event)"/>

 
At December 2, 2010 at 3:46 PM , Anonymous schjlatah said...

Another alternative is to create a function that adds all of your event listeners programmatically on init of the mxml, so it would look something like this

<mx:Button initialize="addEventListeners(event)"/>

private function addEventListeners(event:FlexEvent):void
{
    event.currentTarget.addEventListener(MouseEvent.CLICK, onMouseClick);
    event.currentTarget.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
    // ... etc.
}

 

Post a Comment

Subscribe to Post Comments [Atom]

<< Home