Archive for November, 2008

Creating a Loosely-Coupled Custom Component in Flex

Thursday, November 20th, 2008

Note: This is a tutorial that I wrote for a contest at SitePoint earlier this year.  I had titled it Creating a Reusable Custom Component in Flex. After watching this session from 360Flex, I now know about there being two “levels” of reusability: loosely-coupled components and polished, fit for cataloging/distribution components. This tutorial is for the former.


Creating a Reusable Custom Component in Flex

by Jamie McDaniel

In this tutorial, I will guide you through the process of creating a custom Flex component. You will also learn how to pass data between the main Flex application and the custom component using best practices.

If you do not have Flex Builder, you can download a 60-day trial.  If you are a student, faculty, or staff of an eligible education institution, you can get the education version of Flex Builder 3 free by going to http://flexregistration.com.

The component we will be creating is a slideshow navigation bar.  The navigation bar will include buttons for previous, next, play/pause, full screen on/off, and sound on/off.  We will start by using the default buttons in Flex, and then proceed to style them using more recognizable graphics.

Open Flex Builder and click File -> New -> Flex Project. I am giving this project the name of SitePointTutorial.

When you click Finish, Flex Builder will create folders for your project. In the src folder you will find the main application file called SitePointTutorial.mxml. Go ahead and create three additional folders inside the src folder and name them assets, components, and events.

Click File -> New -> MXML Component. Select the components folder as the parent folder and give this component a filename of SlideshowNavigationBar.  Base it on HBox and set the width to 400 and the height to 60.

In the SlideShowNavigationBar.mxml file, enter the following code:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" width="400" height="60" initialize="onInitialize()" horizontalAlign="center" verticalAlign="middle">
  3.     <mx:Script>
  4.         <![CDATA[
  5.             private function onInitialize():void
  6.             {
  7.                 this.drawRoundRect(0, 0, 400, 60,
  8.                 {tl: 7, tr:7, bl:7, br: 7},
  9.                 0×3A3A3A, 0.75);
  10.             }
  11.         ]]>
  12.     </mx:Script>
  13.     <mx:Button id="btPrev" label="Previous"/>
  14.     <mx:Button id="btNext" label="Next"/>
  15.     <mx:Button id="btPlay" label="Play"/>
  16.     <mx:Button id="btFullScreen" label="Full Screen"/>
  17.     <mx:Button id="btSound" label="Sound"/>
  18. </mx:HBox>

This will create the five buttons and center them within an HBox with a rounded rectangle background. To include our new component in the main application, open up SitePointTutorial.mxml and update it so your code matches the following:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:components="components.*" layout="absolute">
  3.     <mx:HBox width="100%" horizontalAlign="center" bottom="60">
  4.         <components:SlideshowNavigationBar id="mySlideshowNavigationBar"/>
  5.     </mx:HBox>
  6. </mx:Application>

Note the xmlns:components="components.*" in the Application tag. This is because our custom component is in a folder that we named components.  Therefore we need to define a namespace that tells the compiler where to find our custom component.  All the standard components that ship with the Flex framework begin with a namespace of mx.  The mx namespace is also defined in the Application tag and it points to www.adobe.com/2006/mxml.

Our component is added to the application with the simple mxml tag:

  1. <components:SlideshowNavigationBar id="mySlideshowNavigationBar"/>

The id property is needed to access the component with ActionScript.

To test the application, click the Debug button.

You should see the application running in your browser.

Styling the Slideshow Navigation Bar

In SlideshowNavigationBar.mxml add the following code inside the <mx:Script> tag just above the onInitialize function:

  1. [Embed(source="../assets/prev.png")]
  2. private var _prevIcon:Class;
  3. [Embed(source="../assets/next.png")]
  4. private var _nextIcon:Class;
  5. [Embed(source="../assets/play.png")]
  6. private var _playIcon:Class;
  7. [Embed(source="../assets/pause.png")]
  8. private var _pauseIcon:Class;
  9. [Embed(source="../assets/fullscreen.png")]
  10. private var _fullscreenIcon:Class;
  11. [Embed(source="../assets/smallscreen.png")]
  12. private var _smallscreenIcon:Class;
  13. [Embed(source="../assets/sound_on.png")]
  14. private var _soundOnIcon:Class;
  15. [Embed(source="../assets/sound_off.png")]
  16. private var _soundOffIcon:Class;

You will need to copy the graphic files from this tutorial into the assets folder of your project.

It is worth noting that we could do without the above code and set the upSkin, overSkin, and downSkin properties of each of our buttons like so:

  1. <mx:Button id="btPrev" upSkin="@Embed(source='../assets/prev.png')"
  2. overSkin="@Embed(source='../assets/prev.png')
  3. downSkin="@Embed(source='../assets/prev.png')/>

However by associating each graphic with a class, we can swap out the icons at runtime using ActionScript. We will need to do that, for example, when the play button should show a pause graphic.

Also note that we have designated the scope of our variables as private and have prefixed their names with an underscore.  Setting variables inside a custom component as private is not required, but it is recommended as a best practice.  You can always make the variable more accessible later if needed. Prefixing private variables with an underscore is a coding practice used by many developers for readability.

We now need to update the onInitialize function so that it contains the following code:

  1. private function onInitialize():void
  2. {
  3.     btPrev.setStyle("upSkin", _prevIcon);
  4.     btPrev.setStyle("overSkin", _prevIcon);
  5.     btPrev.setStyle("downSkin", _prevIcon);
  6.     btNext.setStyle("upSkin", _nextIcon);
  7.     btNext.setStyle("overSkin", _nextIcon);
  8.     btNext.setStyle("downSkin", _nextIcon);
  9.     btPlay.setStyle("upSkin", _pauseIcon);
  10.     btPlay.setStyle("overSkin", _pauseIcon);
  11.     btPlay.setStyle("downSkin", _pauseIcon);
  12.     btFullScreen.setStyle("upSkin", _fullscreenIcon);
  13.     btFullScreen.setStyle("overSkin", _fullscreenIcon);
  14.     btFullScreen.setStyle("downSkin", _fullscreenIcon);
  15.     btSound.setStyle("upSkin", _soundOnIcon);
  16.     btSound.setStyle("overSkin", _soundOnIcon);
  17.     btSound.setStyle("downSkin", _soundOnIcon);
  18.     this.drawRoundRect(0, 0, 400, 60,
  19.     {tl: 7, tr:7, bl:7, br: 7},
  20.     0×3A3A3A, 0.75);
  21. }

Remove the label property on each of the five buttons so that they look like the following:

  1. <mx:Button id="btPrev"/>
  2. <mx:Button id="btNext"/>
  3. <mx:Button id="btPlay"/>
  4. <mx:Button id="btFullScreen"/>
  5. <mx:Button id="btVolume"/>

Run the application and you should see the styled navigation bar below:

Changing a Button’s Appearance on Mouseover

When the user hovers over the buttons, we would like to provide visual feedback.  One way to do that is by adjusting the alpha value.  In the onInitialize function, add the following code:

  1. btPrev.alpha = 0.75;
  2. btNext.alpha = 0.75;
  3. btPlay.alpha = 0.75;
  4. btFullScreen.alpha = 0.75;
  5. btSound.alpha = 0.75;

To make the buttons respond to a mouseover event, we will first need to register event handler functions for each of the five buttons.  To do so, add the following code to the onInitialize function:

  1. btPrev.addEventListener(MouseEvent.MOUSE_OVER, onButtonMouseOver);
  2. btPrev.addEventListener(MouseEvent.MOUSE_OUT, onButtonMouseOut);
  3. btNext.addEventListener(MouseEvent.MOUSE_OVER, onButtonMouseOver);
  4. btNext.addEventListener(MouseEvent.MOUSE_OUT, onButtonMouseOut);
  5. btPlay.addEventListener(MouseEvent.MOUSE_OVER, onButtonMouseOver);
  6. btPlay.addEventListener(MouseEvent.MOUSE_OUT, onButtonMouseOut);
  7. btFullScreen.addEventListener(MouseEvent.MOUSE_OVER, onButtonMouseOver);
  8. btFullScreen.addEventListener(MouseEvent.MOUSE_OUT, onButtonMouseOut);
  9. btSound.addEventListener(MouseEvent.MOUSE_OVER, onButtonMouseOver);
  10. btSound.addEventListener(MouseEvent.MOUSE_OUT, onButtonMouseOut);

Next, add these two event handler functions. The functions can appear anywhere, but I would suggest placing them after the onInitialize function.

  1. private function onButtonMouseOver(event:MouseEvent):void
  2. {
  3.     event.currentTarget.alpha = 1.0;
  4. }
  5. private function onButtonMouseOut(event:MouseEvent):void
  6. {
  7.     event.currentTarget.alpha = 0.75;
  8. }

When an event occurs, an event object gets passed to the event handler function. The event object has properties that contain information about the event. Here we have used the currentTarget property of the event object to point to the button that triggered the event.

Before we run the application, let’s set the background color in our SitePointTutorial.mxml file.

  1. <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:components="components.*" layout="absolute" backgroundColor="#000000">

Now when you hover the mouse over the buttons, you should get a subtle visual clue.

With our slideshow navigation bar looking good, let’s move to the next step — programming our custom component to dispatch an event that contains information on which button was clicked.  Events are how components notify the application (or the larger component that they are part of) that an action has taken place.  Just as each of the five button components dispatched mouseover events that our SlideshowNavigationBar component listened for, the SlideshowNavigationBar component can be programmed to dispatch events that its parent (the application SitePointTutorial) listens for.

“Loosely Coupled” Versus “Tightly Coupled” Components

Before programming the SlideshowNavigationBar to dispatch its own custom events, let’s talk about how you could accomplish the same result without doing so. You could update the SitePointTutorial.mxml file as follows:

  1. <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:components="components.*" layout="absolute" backgroundColor="#000000" initialize="onInitialize()">
  2.     <mx:Script>
  3.         <![CDATA[
  4.             private function onInitialize():void
  5.  
  6.             {
  7.                 mySlideshowNavigationBar.btPrev.addEventListener(
  8.                 MouseEvent.CLICK, onPrevClick);
  9.             }
  10.             private function onPrevClick(event:MouseEvent):void
  11.             {
  12.                 trace("Previous Button Clicked");
  13.             }
  14.         ]]>
  15.     </mx:Script>

Here we have registered an event listener on the component btPrev inside our component mySlideshowNavigationBar.  This works, however it is not a best practice.   By doing so, you are creating what is referred to as a “tightly coupled” component.  If you were to use the slideshow navigation bar in several applications and then changed the id of the button to something else (perhaps btPrevSlide), it would break all the applications that relied on the component the next time they were compiled.

A “loosely coupled” component, on the other hand, has a well-defined interface. The internal workings of the component can change, but the interface does not. Information is passed down to the component through the setting of properties. The component passes information back up with event objects (or, in some cases, the parent reads a property of the component at a specific point in time.)

To develop a “loosely coupled” (and thus highly reusable component) you should avoid accessing the component’s own internal components.  So instead of “reaching inside” mySlideshowNavigationBar and registering an event listener on its button, we should register an event listener directly on mySlideshowNavigationBar.  But first we need to program our slideshow navigation bar component to dispatch events.

Creating a Custom Event

The base class for all event objects is the flash.events.Event class.  Subclasses such as the MouseEvent class extend the Event class to contain additional information.  You can see all the standard subclasses listed in the Flex documentation.  You can create your own subclasses of the Event class as well, which is what we will be doing now.

Click File -> New -> ActionScript Class.  Browse and select the events folder as the package. Give it a name of SlideshowNavigationBarEvent. Set the Superclass as flash.events.Event (if you click Browse, it is under Event – flash.events).

Update the code for SlideshowNavigationBarEvent.as to match the following:

  1. package events
  2. {
  3.     import flash.events.Event;
  4.     public class SlideshowNavigationBarEvent extends Event
  5.     {
  6.         public static const PLAY_CLICK:String = "playClick";
  7.         public static const PREVIOUS_CLICK:String = "previousClick";
  8.         public static const NEXT_CLICK:String = "nextClick";
  9.         public static const FULLSCREEN_CLICK:String = "fullscreenClick";
  10.         public static const SOUND_CLICK:String = "soundClick";
  11.  
  12.         public function SlideshowNavigationBarEvent(type:String,
  13.         bubbles:Boolean=false, cancelable:Boolean=false)
  14.         {
  15.             super(type, bubbles, cancelable);
  16.         }
  17.  
  18.         override public function clone():Event
  19.         {
  20.             return new SlideshowNavigationBarEvent(type, bubbles,
  21.             cancelable);
  22.         }
  23.     }
  24. }

In our custom event class we have defined five constants that will be used as the value for the event’s type property. We have defined the constructor to require the type property be set. The constructor also accepts two optional arguments.  All three properties are passed with the super method.  (The super method calls the constructor of the superclass, which is the Event class.)  We also overrode the clone method which is required when creating a subclass of the Event class.

To use our new custom event, add this line to SlideshowNavigationBar.mxml inside the <mx:Script> tag at the top:

  1. import events.SlideshowNavigationBarEvent;

Before the <mx:Script> tag, add the following:

  1. <mx:Metadata>
  2.     [Event(name="playClick", type="events.SlideshowNavigationBarEvent")]
  3.     [Event(name="previousClick", type="events.SlideshowNavigationBarEvent")]
  4.     [Event(name="nextClick", type="events.SlideshowNavigationBarEvent")]
  5.     [Event(name="fullscreenClick", type="events.SlideshowNavigationBarEvent")]
  6.     [Event(name="soundClick", type="events.SlideshowNavigationBarEvent")]
  7. </mx:Metadata>

Now in the onInitialize function in SlideshowNavigatonBar.mxml add the following:

  1. btPrev.addEventListener(MouseEvent.CLICK, onButtonClick);
  2. btNext.addEventListener(MouseEvent.CLICK, onButtonClick);
  3. btPlay.addEventListener(MouseEvent.CLICK, onButtonClick);
  4. btFullScreen.addEventListener(MouseEvent.CLICK, onButtonClick);
  5. btSound.addEventListener(MouseEvent.CLICK, onButtonClick);

Add this event handler function to SlideshowNavigationBar.mxml after the other functions:

  1. private function onButtonClick(event:MouseEvent):void
  2. {
  3.     var eventObj:SlideshowNavigationBarEvent;
  4.     switch (event.currentTarget.id)
  5.     {
  6.         case 'btPrev' :
  7.             eventObj = new SlideshowNavigationBarEvent(
  8.             SlideshowNavigationBarEvent.PREVIOUS_CLICK);
  9.             dispatchEvent(eventObj);
  10.             break;
  11.         case 'btNext' :
  12.             eventObj = new SlideshowNavigationBarEvent(
  13.             SlideshowNavigationBarEvent.NEXT_CLICK);
  14.             dispatchEvent(eventObj);
  15.             break;
  16.         case 'btPlay' :
  17.             eventObj = new SlideshowNavigationBarEvent(
  18.             SlideshowNavigationBarEvent.PLAY_CLICK);
  19.             dispatchEvent(eventObj);
  20.             break;
  21.         case 'btFullScreen' :
  22.             eventObj = new SlideshowNavigationBarEvent(
  23.             SlideshowNavigationBarEvent.FULLSCREEN_CLICK);
  24.             dispatchEvent(eventObj);
  25.             break;
  26.         case 'btSound' :
  27.             eventObj = new SlideshowNavigationBarEvent(
  28.             SlideshowNavigationBarEvent.SOUND_CLICK);
  29.             dispatchEvent(eventObj);
  30.             break;
  31.         default :
  32.             break;
  33.     }
  34. }

Our slideshow navigation bar component will now dispatch events that its parent component can listen for.  Update the SitePointTutorial.mxml as so:

  1. <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:components="components.*" layout="absolute" backgroundColor="#000000" initialize="onInitialize()">
  2.     <mx:Script>
  3.         <![CDATA[
  4.             import events.SlideshowNavigationBarEvent;
  5.             private function onInitialize():void
  6.             {
  7.                 mySlideshowNavigationBar.addEventListener(
  8.                 SlideshowNavigationBarEvent.PREVIOUS_CLICK, onPrevClick);
  9.                 mySlideshowNavigationBar.addEventListener(
  10.                 SlideshowNavigationBarEvent.NEXT_CLICK, onNextClick);
  11.                 mySlideshowNavigationBar.addEventListener(
  12.                 SlideshowNavigationBarEvent.PLAY_CLICK, onPlayClick);
  13.                 mySlideshowNavigationBar.addEventListener(
  14.                 SlideshowNavigationBarEvent.FULLSCREEN_CLICK, onFullScreenClick);
  15.                 mySlideshowNavigationBar.addEventListener(
  16.                 SlideshowNavigationBarEvent.SOUND_CLICK, onSoundClick);
  17.             }
  18.             private function onPrevClick(event:SlideshowNavigationBarEvent):void
  19.             {
  20.                 myTextArea.htmlText += "Go to previous slide.<br>";
  21.             }
  22.             private function onNextClick(event:SlideshowNavigationBarEvent):void
  23.             {
  24.                 myTextArea.htmlText += "Go to next slide.<br>";
  25.             }
  26.             private function onPlayClick(event:SlideshowNavigationBarEvent):void
  27.             {
  28.                 myTextArea.htmlText += "Pause slideshow.<br>";
  29.             }
  30.             private function onFullScreenClick(
  31.             event:SlideshowNavigationBarEvent):void
  32.             {
  33.                 myTextArea.htmlText += "Go to full screen.<br>";
  34.             }
  35.             private function onSoundClick(event:SlideshowNavigationBarEvent):void
  36.             {
  37.                 myTextArea.htmlText += "Turn sound off.<br>";
  38.             }
  39.         ]]>
  40.     </mx:Script>
  41.     <mx:HBox width="100%" horizontalAlign="center">
  42.         <mx:TextArea id="myTextArea" width="500" height="400"/>
  43.     </mx:HBox>
  44.     <mx:HBox width="100%" horizontalAlign="center" bottom="60">
  45.         <components:SlideshowNavigationBar id="mySlideshowNavigationBar"/>
  46.     </mx:HBox>
  47. </mx:Application>

When you run the application, take a moment to mentally step through the sequence that occurs when you click the Previous button:

  1. The btPrev component in the mySlideshowNavigationBar component dispatches a click event.
  2. As a result of the click event, the onButtonClick event handler function gets called and creates a new event object that is an instance of SlideshowNavigationBarEvent — a custom class that we created which extends the Event class. This event object has its type property set to the string “previousClick” (defined by the constant PREVIOUS_CLICK). The event is then dispatched with the dispatchEvent method.
  3. In the main application, the function onPrevClick gets called because we registered the appropriate event listener. The application then sends information to the myTextArea component by setting its htmlText property.

This pattern is one that you will use repeatedly in the development of larger Flex applications.  The main application will contain several child components that dispatch events up to the parent application, which then sets properties on other child components.

It is also important to know that custom events can contain much more information than we used in our SlideshowNavigationBarEvent. We only used the type property, which is a standard property of the Event class. We could have extended the Event class to include new variables, including complex objects.

Creating Properties for our Custom Component

There are a few things left to do to make our slideshow navigation bar complete.  A slideshow would probably be playing automatically and when the user clicks the pause button, the application would stop at the current slide and change the pause button to a play button.  To allow for that, let’s add a property to the slideshow navigation bar component called isPlaying.  Add the following code to SlideshowNavigationBar.mxml above the onInitialize function:

  1. private var _isPlaying:Boolean;
  2. public function get isPlaying():Boolean
  3. {
  4.     return _isPlaying;
  5. }
  6. public function set isPlaying(isPlaying:Boolean):void
  7. {
  8.     _isPlaying = isPlaying
  9.     if (_isPlaying == true)
  10.     {
  11.         btPlay.setStyle("upSkin", _pauseIcon);
  12.         btPlay.setStyle("overSkin", _pauseIcon);
  13.         btPlay.setStyle("downSkin", _pauseIcon);
  14.     }
  15.     else
  16.     {
  17.         btPlay.setStyle("upSkin", _playIcon);
  18.         btPlay.setStyle("overSkin", _playIcon);
  19.         btPlay.setStyle("downSkin", _playIcon);
  20.     }
  21. }

We are defining the public property isPlaying by using a setter method.  Using getter and setter methods are not required, but they are a best practice when creating custom components.  Here we have a private variable named _isPlaying.  When this variable changes, the icon for the btPlay button needs to change as well.  Using a setter method allows us to always run code that is associated with the property whenever the property changes.

To simulate a slideshow automatically playing, add the following line to the onInitialize function in SitePointTutorial.mxml:

  1. mySlideshowNavigationBar.isPlaying = true;

Update the onPlayClick function in SitePointTutorial.mxml as so:

  1. private function onPlayClick(event:SlideshowNavigationBarEvent):void
  2. {
  3.     if (mySlideshowNavigationBar.isPlaying == true)
  4.     {
  5.         mySlideshowNavigationBar.isPlaying = false;
  6.         myTextArea.htmlText += "Pause slideshow.<br>";
  7.     }
  8.     else
  9.  
  10.     {
  11.         mySlideshowNavigationBar.isPlaying = true;
  12.         myTextArea.htmlText += "Play slideshow.<br>";
  13.     }
  14. }

Now when you run the application, the dual function of the Play/Pause button will work.

I’ll leave it to you to implement the dual function of the Full Screen and Sound buttons.  You can run the finished application and view the full code.

You can learn more about creating custom components in Adobe Flex by consulting the official documentation.