Check out our Angular Book Series.

Moving your Flex Components from MXML to ActionScript 3

I wrote two articles for InsideRIA when the site was still active. I'm purging my personal digital archives and came across them. I decided to repost them here for posterity.

This article is about educating folks on how to move from MXML to ActionScript. I wrote the article in February of 2010; but it didn't get posted until later.

You can find the original on this DevelopRIA site. I remember it being slightly controversial, but I'm not sure why. Sadly the comments do not seem to be archived. Here is the article.

I often hear it said that all the cool kids write their Flex components using ActionScript without MXML. I'm not sure that I agree. MXML is great for layout purposes. It is great for building simple components quickly. The declarative syntax makes many development tasks easier, such as setting styles and adding events listeners. But, if you look closely at the Flex Framework source code or commercial grade components, such as what I build at Flextras, you'll notice they do not use MXML. Everything is built using ActionScript. Why is that?

ActionScript gives you granular control over your code. MXML is an ActionScript generation language. The Flex Framework takes your MXML and turns it into ActionScript. That means the code you write, isn't the code that is running. If coding were cooking, using MXML would be like buying a cake mix from the store. You just add water and you're ready to bake. ActionScript is akin to starting with flour and choosing your other ingredients carefully. It takes longer, it requires more thought, but the results are often worth it.

This article will show you how to move your component development from MXML to ActionScript. Along the way we'll touch on various aspects of the Flex Component Lifecycle. Often when you are building your own applications, in controlled environment, it will make more sense to build with MXML; and that is fine. But, understanding how to build from scratch using ActionScript will give you a deeper knowledge of how Flex works and help you build all your components better.

Today's Application

Today, I want you to pretend that your boss asked you to build a survey application. As with most surveys, this application includes a bunch of questions, and needs some way to collect answers. Many of those questions can be answered with a simple yes or no. In a normal situation, you would collect yes and no answers using radio buttons.

Unfortunately, and try to stretch your imagination with me on this, your boss is a bit irrational. He hates radio buttons. You're never quite sure why, but it is what it is. Instead of a radio button, he insists that you use one of those dropdown select box thingies. Fine! We can work with that. Knowing that your survey is bound to have a lot of yes and no questions, you decide to make a component out of it.

YesNoQuestion Component Version 1

Let's just jump in and create the first rendition of our component. You can use a Text component for the question, and a ComboBox for the 'dropdown select thingy'. Throw this all in an HBox and the code would look something like this:


<?xml version="1.0" encoding="utf-8"?>
<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" width="100%" height="100%">
    <mx:Script>
        <![CDATA[
            import mx.collections.ArrayCollection;
            [Bindable]
            public var dp : ArrayCollection = new ArrayCollection([
                {label:'Yes'},
                {label:'No'}
            ]);
        ]]>

    </mx:Script>
    <mx:Text id="question" />
    
    <mx:ComboBox id="answer" dataProvider="{dp}" />
        
</mx:HBox>

We are already making our first use of ActionSCript in this otherwise MXML component. The dataProvider of the ComboBox is coded in script. It contains two objects, one for yes and one for no.

Unfortunately, this component is still lacking in functionality. When using this component, how do we specify the question text? Your developer could access "question.text", but it would be nicer if we gave them a simpler way. How do we know which answer the survey taker has chosen? We'll need to add a property to expose that value too.

Add these two variables to your ActionScript block:


[Bindable]
public var questionText : String;
            
[Bindable]
public var selectedAnswer : String;

Since the variables are public, they are easily accessible by people using our component. I call these variable properties, although I don't think that is a formal name for them. Next you'll want to tie the variables to the two components. Modify the MXML, like this:


<mx:Text id="question" text="{questionText}" />

<mx:ComboBox id="answer" dataProvider="{dp}" change="selectedAnswer = answer.selectedItem.label" />

Data Binding ties the questionText to the text display. You can use the change event to update the selectedAnswer each time you the ComboBox value changes. For all intents and purposes, this component would work for what we need it to do. It's time to test it.

To test the component, you'll need to build a simple application. I decided to build an AIR app to test, so I don't have to muck around with web server settings. From a code point of view, a web based app is almost no different; just change the WindowedApplication to an Application. This is my main application file:


<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:MXMLToAS3="com.flextras.InsideRIA.MXMLToAS3.*">
    
<mx:VBox>
    <MXMLToAS3:YesNoQuestionV1 id="q1" questionText="Do you want to take a Survey?" />
    <mx:Text text="{q1.selectedAnswer}" />        
</mx:VBox>
    
</mx:WindowedApplication>
The code imports the package that contains the component. It creates an instance of the component, q1, and specifies the question text as a string. The code contains an additional text component instance that is bound to q1's selectedAnswer property. As we change answers to the question, we can see that the selectedAnswer property changes too. Here is the running app:

Version 2: Implementation Hiding

One of the key reasons to separate your code out into a component is so that you can hide the implementation. With a hidden implementation, you can change that implementation without changing the API and all code using the component should have no issues. The current component does not hide the implementation details, as you see here:

The question and answer field are both exposed. By creating them in MXML, they are treated as public properties. What would happen to our component if someone changed the dataProvider on the ComboBox? You want to prevent this type of meddling as much as possible.

To address this we are going to turn our MXML components for Text and YesNoQuestion into protected ActionScript variables and make use of the Flex Framework's component lifecycle createChildren() to create the components and add them to the stage.

The createChildren() method is run during the component's initial setup, and is intended to be used to create children and add it to the parent container. When dealing with ActionScript States, I'll often initialize any AddChild or RemoveChild state elements in createChildren(). I do this in the Flextras Calendar component to implement day, week, and month views. To keep the selectedAnswer variable in sync with what is going on, we were responding to the change event and setting the value. Our new approach will be the same, but we'll set up the event listener and the event handler in ActionScript, not using in-line MXML.

First, create the component variables:


protected var question : Text;
protected var answer : ComboBox;

Because these variables are protected, it means that any components that extend this component can access them, but if the new component does not extend this file, it cannot. This is our createChildren() method:


override protected function createChildren():void{
    super.createChildren();
            
    this.question = new Text();
    this.question.text = questionText;
    this.addChild(this.question);
                
    this.answer = new ComboBox();
    this.answer.dataProvider = this.dp;
    this.addChild(this.answer);
    this.answer.addEventListener(ListEvent.CHANGE, onChange);
}

createChildren() is initially defined in the UIComponent class. All Flex User Interface Components extend UIComponent, and ours is no exception, even though we are down the chain a bit. To implement it in our YesNoQuestion component, we override the method signature and call the super method. When overriding methods, it is important to call the super method. You never know what code magic may be executing higher up in the chain. Then the component creates the question Text instance and answer ComboBox instance. It sets the default value and adds it to the container.

In our MXML version, you kept the selectedAnswer in sync using the change event in MXML. In ActionScript, you'll use the same approach, but need to set up the event listener with the addEventListener() method. It specifies the type of event you are listening for, change in this case, and the function to run when that event is dispatched. Instead of using the event name, 'change', I'm referring to the event constant from the event class. Either approach should work, but referencing that constant is a bit more flexible. If the event name changes, our code does not have to. This is the listener function


protected function onChange(e:ListEvent):void{
this.selectedAnswer = this.answer.selectedItem.label;
}

The listener function accepts an event argument. The single line of code inside the method is the same that we used in-line in our MXML version.

Version 3: commitProperties()

What happens if the questionText is still an empty string when the createChildren() method is called? Our question will have nothing to display. There is nothing in our current code to update the question text if the questionText property changes. There is a solution. The Flex Framework provides the commitProperties() method to run code after all the component's properties have been set. We will make use of this method to set and update the question text whenever the value changes.

First, add the commitProperties() method into your code. We can copy and paste the line to set the question text into the body:


override protected function commitProperties():void{
    super.commitProperties();
    this.question.text = questionText;
}

The method, of course, calls its' super just as we did with createChildren(). The Flex Component Lifecycle provides us with an invalidation method named invalidateProperties(). We can call this at any time on our component and it will force the commitProperties() to run during the next render event. This is a difference between createChildren() and commitProperties(). createChildren() only runs once; while commitProperties() runs during initial setup, and then again as needed. In order to trigger the commitProperties() invalidation, we're going to replace the questionText variable property with a get/set property. Flash Builder 4 contains some code generation to do this, and the resultant code will look something like this:


private var _questionText : String;
[Bindable]
public function get questionText(): String{
    return this._questionText;
}
public function set questionText(value:String):void{
    this._questionText = value;
}

The commitProperties() method will most likely execute during the life of our application more often than when we change the questionText. To let commitProperties() know what it actually needs to do, we'll add a propertyChanged flag, like this:


private var questionTextChanged : Boolean = false;

The set method will be modified to set the flag to true and to call invalidateProperties():


public function set questionText(value:String):void{
    this._questionText = value;
    this.questionTextChanged = true;
    this.invalidateProperties()
}

The commitProperties() method will need to be revisited to check for that flag:


override protected function commitProperties():void{
    super.commitProperties();
    if(this.questionTextChanged == true){
        this.question.text = questionText;
        this.questionTextChanged = false;
    }
}

By setting up the selectedAnswer method using a variable property, users of the component can change it at will; which could leave to undesirable effects. We can replace this property with a get method. Leaving out the set method will make the value read only from the outside. This is the updated set method:


private var _selectedAnswer : String
[Bindable(event='selectedAnswerChanged')]
public function get selectedAnswer (): String{
    return this._selectedAnswer;
}

Notice that I changed the Bindable metadata tag. Instead of using its default state, I added an event. The Flex Framework knows to make properties Bindable when the set method exists, but will cause a warning if no set method exists. The solution is to specify the bindable event, and dispatch it on your own when the property changes. We can add a method for the property change:


protected function setSelectedAnswer(value:String):void{
this._selectedAnswer = value;
    this.dispatchEvent(new Event('selectedAnswerChanged'));
}

The property was being set in the onChange event handler. We have toChange that so it accesses the set method instead of the variable property directly:


protected function onChange(e:ListEvent):void{
    setSelectedAnswer(this.answer.selectedItem.label);
}

I want to point out that there is nothing code related that will prevent you from having a get method and set method with different access modifiers. But,the ASDoc tool does have a problem with it, which is why I just remove the space between the set and property name. If you don't use ASdocs, feel free to make public getters and protected setters.

To test the setting of the questionTxt, we can make some modifications to our main application file. Add in a TextInput and a button to modify the questionText on q1:


<mx:TextInput id="questionText" />
<mx:Button click="q1.questionText = questionText.text" />

Run and test the code and you'll find that we can change the question text without any issues; and the read only selectedAnswer property is still changing the main application's Text component when it changes. Things are good.

Version 4: Moving to All ActionScript

If you look at your component code, you realize most of it is ActionScript already. It is not a big leap to turn the MXML component into an all ActionScript component. The component first starts with a package definition:


package com.flextras.InsideRIA.MXMLToAS3 {

The package definition is the folder structure where the component is located. So, in my application the YesNoQuestionV4.as file is located in the MXMLToAS3 directory of the InsideRIA directory of the Flextras directory of the com directory. The com directory is located off the main source root. This piece was masked from us in the MXML alternate.

Next we put the class imports:


import mx.containers.HBox;
import mx.controls.ComboBox;
import mx.controls.Text;
import mx.collections.ArrayCollection;
import mx.events.ListEvent;

The only difference between these and the previous MXML Version is that we are importing the HBox, which our component is based off of. The imports were also in a script tag of the MXML component. In the ActionScript version, there is no script tag; in fact there are no tags at all.

Next up, comes the class definition:


public class YesNoQuestionV4 extends HBox{

Classes can use the same access modifiers that properties and methods use. Access modifiers cannot be specified when developing in MXML. Next is the class constructor:


public function YesNoQuestionV4(){
    super();
}

In this sample, the constructor does nothing other than calling its' parent's constructor. But, I'll often use it for defining default styles or performing setup of the component's states. Any code that you commonly write in response to the creationComplete event most likely belongs in the constructor; however MXML components do not support constructors.

Next comes all the ActionScript code that was in the code block from our previous MXML version. I won't replicate it for you here. Finally, the open brackets for the class and package definition close:


}
}

Even though your component is now 100% ActionScript, our main application does not have to change. In a true nod to implementation hiding, the application, or other component, that uses your components do not care whether it was implemented in ActionScript or MXML or some mix of the two.

Version 5: Extending UIComponent

In the previous versions, we were extending the HBox class. This makes our lives a bit easier, because we were able to use the HBox's inherent ability to position and layout our question Text and ComboBox components. However, the layout algorithms in some of the classes may be too complicated for your needs. Sometimes something simpler will offer better performance. For the final rendition of our YesNoQuestion, we're going to extend the UIComponent. The first line to modify is the class definition. Previously it extended HBox, now it extends the UIComponent, like this:


public class YesNoQuestionV5 extends UIComponent

That is the only line of code that needs to change, but we do need to make some additions. There are two Flex component LifeCycle methods we haven't implemented yet, measure() and updateDisplayList(). Implementing these two methods will finish our component.

The purpose of the measure() method is to decide in the ideal height and width that your component needs without showing scroll bars. A component's parent is ultimately responsible for its' size, so the measure() method is really just setting suggestions, via the measuredHeight and measuredWidth property.

This is the method:


override protected function measure():void{
super.measure();
this.measuredHeight = question.measuredHeight + answer.measuredHeight;
this.measuredWidth = question.measuredWidth + answer.measuredWidth;
}

The method overrides the parent, and calls the super version of its method. Then it calculates the measuredHeight by adding the measuredHeight and measuredWdth of each child, respectively. In most cases, the measure method just loops over the children and calculates the values similar to what we've done here.

The measure() method can, optionally, set the measuredMinWidth and measuredMinHeight properties. These properties specify how small the component can go before it stops sizing down. I did not specify those values here, but often default them to 100 just so that they have a value. I've run into odd issues when using percentage heights on components that do not specify the minimums.

The last method to implement is updateDisplayList(). updateDisplayList() is used primarily to position and size the children. However, you can also use it for other display items, such as setting styles or drawing with the graphics API. This is our updateDisplayList() method:


override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void{
    this.question.setActualSize(
this.question.getExplicitOrMeasuredWidth(),
this.question.getExplicitOrMeasuredHeight());

    this.question.move(0,0);

this.answer.setActualSize(
this.answer.getExplicitOrMeasuredWidth(),
this.answer.getExplicitOrMeasuredHeight());

    this.answer.move(this.question.width, 0);
            
}

updateDisplayList() contains to arguments, the unscaledWidth and the unscaledHeight of the component. These values are, essentially, the height and width that you want to use to size your component. To position the components, use the move method. The first one, question, is positioned at the top left corner, with an x position of 0 and a y position of 0. The answer component is positioned next to the first one, with a x position equal to the width of the question instance. The answer's y value still remains 0.

setActualSize() is used to size the components. In this case, we use the two methods: ,getExplicitoOrMeasuredWidth() and getExplicitOrMeasuredHeight(). Since we never set an explicit height or width, this sets the components to their measured height and width.

Final Code

Before you start to build your components, think for a second. Are these components that you want to optimize for reuse and use in a lot of different places, and a lot of different ways? Or are these onetime components that you want to build for your application? Even if you can build everything in ActionScript, it may not be worth your extra time. But, even with MXML components, you can still make use of the ActionScript techniques and Flex component lifecycle methods to make robust components.

For the same of completeness, the final code follows the end of this article.


package com.flextras.InsideRIA.MXMLToAS3
{
    import mx.containers.HBox;
    import mx.controls.ComboBox;
    import mx.controls.Text;
    import mx.collections.ArrayCollection;
    import mx.events.ListEvent;
    import mx.core.UIComponent;
            
    public class YesNoQuestionV5 extends UIComponent
    {
        public function YesNoQuestionV5()
        {
            super();
        }

        [Bindable]
        public var dp : ArrayCollection = new ArrayCollection([
            {label:'Yes'},
            {label:'No'}
        ]);
        
        private var _questionText : String;
        private var questionTextChanged : Boolean = false;
        [Bindable]
        public function get questionText(): String{
            return this._questionText;
        }
        public function set questionText(value:String):void{
            this._questionText = value;
            this.questionTextChanged = true;
            this.invalidateProperties()
        }
        
        private var _selectedAnswer : String
        [Bindable(event='selectedAnswerChanged')]
        public function get selectedAnswer (): String{
            return this._selectedAnswer;
        }
        
        protected function setSelectedAnswer(value:String):void{
            this._selectedAnswer = value;
            this.dispatchEvent(new Event('selectedAnswerChanged'));
        }
        
        protected var question : Text;
        protected var answer : ComboBox;
        
        override protected function commitProperties():void{
            super.commitProperties();
            if(this.questionTextChanged == true){
                this.question.text = questionText;
                this.questionTextChanged = false;
            }
        }
        
        override protected function createChildren():void{
            super.createChildren();
            
            this.question = new Text();
            this.addChild(this.question);
            
            this.answer = new ComboBox();
            this.answer.dataProvider = this.dp;
            this.addChild(this.answer);
            this.answer.addEventListener(ListEvent.CHANGE, onChange);
        }
        
        override protected function measure():void{
            super.measure();
            
            this.measuredHeight = this.question.measuredHeight + this.answer.measuredHeight;
            this.measuredWidth = this.question.measuredWidth + this.answer.measuredWidth;
        }
        
        override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void{
            this.question.setActualSize( this.question.getExplicitOrMeasuredWidth(), this.question.getExplicitOrMeasuredHeight());
            this.question.move(0,0);
            this.answer.setActualSize(this.answer.getExplicitOrMeasuredWidth(), this.answer.getExplicitOrMeasuredHeight());
            this.answer.move(this.question.width, 0);
            
        }
        
        
        protected function onChange(e:ListEvent):void{
            setSelectedAnswer(this.answer.selectedItem.label);
        }

        
    }
}

Comments (Comment Moderation is enabled. Your comment will not appear until approved.)
Comments are not allowed for this entry.
All Content Copyright 2005, 2006, 2007, 2008, 2009 Jeffry Houser. May not be reused without permission
BlogCFC was created by Raymond Camden. This blog is running version 5.9.2.002.