5. Flex Code Example

This section will go over the code for the Flex search page. It will cover remoting, the UI, internationlization (i18n), and logging. Parts of the application use the Flex MVC framework from Adobe called Cairngorm. The model, view, and controller are all in the ActionScript code and helps separate business logic from the UI components.

[Note]Note
                -locale=en_US,es_ES -source-path=../locales/{locale}
                -context-root simple-flex
                -compiler.services ${user.dir}/../webapp/WEB-INF/flex/services-config.xml
            

These additional compiler arguments are necessary when building the Flex part of the example. The 'locale' option specifies that both the 'en_US' and 'es_ES' locales should be compiled into the binary. The 'source-path' option indicates where the different locales properties files should be found. To use locales other than english (en_US) a command must be run for the Flex SDK to copy the default locale of 'en_US' to create the new locale.

$ /Applications/Adobe\ Flex\ Builder\ 3\ Plug-in/sdks/3.2.0/bin/copylocale en_US es_ES

The 'context-root' is the web applications context path and is used as a variable in the 'services-config.xml' when defining the remoting channel's URL. The 'compiler.services' option points to the location of the BlazeDS configuration. The different channels defined are compiled into the binary, so when a RemoteObject is defined in the code below it isn't necessary to specify it's endpoint.

The 'search.mxml' is the entry point for the Flex application. So it's main enclosing element is mx:Application. In Flex components can either be made in mxml files or ActionScript files. It configures the mx namespace (Flex components) and controller namespace (applicaton specific classes). The layout is set to 'horizontal', but isn't important since there is just one component displayed. There are multiple events during the components initialization that can have callbacks registered with them. The 'initialize' and 'addedToStage' events are used here.

The mx:Metadata element loads the 'messages' resource bundles. The controller namespace is used to instantiate the application's two Cairngorm controllers.

The mx:Script element contains ActionScript. At the beginning of it are imports just like in Java. Below that fields for the logging are defined. Underneath that are the two methods for handling initialization events and a key down handler for the logging window. During initialization logging is setup with the application's log target and the logging window is initialized. The Cairngorm events for initializing the locale from the server and the initial data for search are dispatched as well as setting the model for the search results to the DataGrid. The added to stage event sets up a key down handler to hide and show the logging window when 'ctrl + shift + up' is pressed.

The mx:DataGrid is the display component for search. If columns weren't explicitly defined, the data would still be shown but the property name would be used for the header. By using the mx:DataGridColumn element the columns being shown and the i18n column name is used. Also the third column is a custom renderer that creates an edit and delete button for each row. The edit button redirects to the edit page using the navigateToURL method, and delete sends a request to the person service.

Example 2. search.mxml

                
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
                xmlns:controller="org.springbyexample.web.flex.controller.*" 
                layout="horizontal"
                initialize="initializeHandler()"
                addedToStage="addedToStageHandler()">

    <!--
        To use other locales besides en_US, the en_US locale must be copied in the sdk.
    
            ex: /Applications/Adobe\ Flex\ Builder\ 3\ Plug-in/sdks/3.2.0/bin/copylocale en_US es_ES
    -->
    <mx:Metadata>
        [ResourceBundle("messages")] 1
    </mx:Metadata>
    
    <controller:ResourceController/>
    <controller:PersonController/>

    <mx:Script>
        <![CDATA[ 
            import org.springbyexample.web.flex.log.LogWindow;
            import mx.logging.Log;
            import mx.logging.ILogger;

            import org.springbyexample.web.flex.event.LocaleChangeEvent;
            import org.springbyexample.web.flex.event.PersonSearchEvent;
            import org.springbyexample.web.flex.log.StringBufferTarget;
            import org.springbyexample.web.flex.model.PersonSearchModelLocator;

            private const logger:ILogger = Log.getLogger("search.mxml");  2
            private var logTarget:StringBufferTarget = new StringBufferTarget();
            private var logWindow:LogWindow;
            
            /**
             * Initialize component.
             */
            private function initializeHandler():void { 3
                Log.addTarget(logTarget);
                
                logWindow = new LogWindow()
                logWindow.logTarget = logTarget;
                
                var lce:LocaleChangeEvent = new LocaleChangeEvent(resourceManager);
                lce.dispatch();

                var psml:PersonSearchModelLocator = PersonSearchModelLocator.getInstance();
                searchDataGrid.dataProvider = psml.personData;
                
                var pse:PersonSearchEvent = new PersonSearchEvent();
                pse.dispatch();
            }

            /**
             * Handles the 'addedToStage' event.
             */
            private function addedToStageHandler():void {
                stage.addEventListener(KeyboardEvent.KEY_DOWN, keydownHandler);
            }
            
            /**
             * Handles key down event.  Toggles showing the log text area 
             * if 'ctrl + shift + up' is pressed.
             */
            private function keydownHandler(event:KeyboardEvent):void {
                if (event.ctrlKey && event.shiftKey && event.keyCode == Keyboard.UP) {
                    if (logWindow.active) {
                        logWindow.hide();
                     } else {
                        logWindow.open(this);
                     }
                }
            }
        ]]>
    </mx:Script>

    <mx:DataGrid id="searchDataGrid"> 4
        <mx:columns>
            <mx:DataGridColumn headerText="{resourceManager.getString('messages', 'person.form.firstName')}" dataField="firstName"/>  5
            <mx:DataGridColumn headerText="{resourceManager.getString('messages', 'person.form.lastName')}" dataField="lastName"/> 6
            <mx:DataGridColumn width="150" editable="false">
                <mx:itemRenderer> 7
                    <mx:Component>
                        <mx:HBox>
                            <mx:Script>
                                <![CDATA[ 
                                    import org.springbyexample.web.flex.event.PersonDeleteEvent;
                                    import org.springbyexample.web.jpa.bean.Person;
                                ]]>
                            </mx:Script>
                            <mx:Button label="{resourceManager.getString('messages', 'button.edit')}" 
                                       click="navigateToURL(new URLRequest('../person/form.html?id=' + data.id), '_self');"/>
                            <mx:Button label="{resourceManager.getString('messages', 'button.delete')}" 
                                       click="new PersonDeleteEvent((data as Person).id).dispatch();"/>
                        </mx:HBox>
                    </mx:Component>
                </mx:itemRenderer>
            </mx:DataGridColumn>    
        </mx:columns>
    </mx:DataGrid>
	
</mx:Application>
                
            

1 Loads all locale resource bundles that start with 'messages' (all 'messages.properties' resource bundles).
2 Configures a logger.
3 Initialization callback handler that configures logging with the application's log target, initializes the log window, dispatch the LocaleChangeEvent to get the locale from the server and set the matching one in the client, sets the Person model to the search DataGrid, and dispatch the PersonSearchEvent to get the search data from the server and initialize the search DataGrid
4 The search DataGrid that displays the search results.
5 Defines the column for the 'firstName' field and retrieves the header from the resource manager's 'messages' bundle using the 'person.form.firstName' key.
6 Defines the column for the 'lastName' field and retrieves the header from the resource manager's 'messages' bundle using the 'person.form.lastName' key.
7 Creates a custom renderer for the third column that has an edit and delete button for the current row. The edit button redirects to the HTML edit page. The delete button sends a request to the server and removes the row from the UI.

The Person ActionScript class is very similar to a Java class. A package is defined, imports, and a class that can contain variables and functions. The class has RemoteClass metadata set on it indicating that if the org.springbyexample.web.jpa.bean.Person Java class is serialized by either a remoting or a messaging request, Flex will bind the incoming data to the matching ActionScript class. More can be read about the mapping between ActionScript and Java at Explicitly mapping ActionScript and Java objects.

Example 3. Person

                
package org.springbyexample.web.jpa.bean {

import mx.collections.ArrayCollection;
	

/**
 * <p>Person information which binds to the Java 
 * remote class <code>org.springbyexample.web.jpa.bean.Person</code>.</p>
 * 
 * @author David Winterfeldt
 */
[RemoteClass(alias="org.springbyexample.web.jpa.bean.Person")]
public class Person {
	
    public var id:int;
    public var firstName:String;
    public var lastName:String;
    public var addresses:ArrayCollection;
    public var created:Date;

}

}
                
            

This is the Cairngorm model. The PersonSearchModelLocator has the metadata value [Bindable] set on it. This indicates that any changes to values in this class will fire events to anything a value is bound to. In this case the search DataGrid is bound to the personData ArrayCollection so when the controller updates the data, the DataGrid is automatically updated.

The PersonSearchModelLocator is using the standard singleton pattern, but ideally a Dependency Injection (DI) framework would be used instead to inject the model where it's needed. This wasn't done to keep this example simpler, and this is actually the suggested way to create a model in Cairngorm. There are multiple DI frameworks are available. Two are Parsley and Spring ActionScript. Spring ActionScript is a SpringSource sponsored project.

Example 4. PersonSearchModelLocator

                
[Bindable]
public class PersonSearchModelLocator implements ModelLocator {

    public var personData:ArrayCollection = new ArrayCollection();
    
    private static var _instance:PersonSearchModelLocator = null; 
    
    /**
     * Implementation of <code>ModelLocator</code>.
     */
    public static function getInstance():PersonSearchModelLocator { 
    	if (_instance == null)  { 
    		_instance = new PersonSearchModelLocator(); 
        } 
            
        return _instance; 
    } 

}
                
            

This is the custom Cairngorm event for retrieving person search data. As was seen in 'search.mxml' an instance of the event can be created and dispatch can then be called on it.

Example 5. PersonSearchEvent

                
public class PersonSearchEvent extends CairngormEvent {

    public static var EVENT_ID:String = "org.springbyexample.web.flex.event.PersonSearchEvent"; 
    
    /**
     * Constructor
     */
    public function PersonSearchEvent() {
        super(EVENT_ID); 
    }

}
                
            

The PersonController is a front controller and allows mapping of custom events to command implementations for the events.

Example 6. PersonController

                
public class PersonController extends FrontController {

    /**
     * Constructor
     */
    public function PersonController() {
        super();
        
        addCommand(PersonSearchEvent.EVENT_ID, PersonSearchCommand);
        addCommand(PersonDeleteEvent.EVENT_ID, PersonDeleteCommand);
    }

}
                
            

The PersonSearchCommand was associated with the PersonSearchEvent in the PersonController. In execute(event:CairngormEvent), which is the implementation of ICommand, a remoting request to the 'personDao' service is made. A RemoteObject is created passing in the name of the service, and the method matching the Java class on the server is called. It's very simple and straightforward. An event listener is attached to the RemoteObject to listen for a result. An event listener could also be registered to listen for a failure. Flex has an excellent event model that is easy to leverage for custom events.

Example 7. PersonSearchCommand

                
public class PersonSearchCommand implements ICommand {
        
    /**
     * Implementation of <code>ICommand</code>.
     */
    public function execute(event:CairngormEvent):void { 
        var ro:RemoteObject = new RemoteObject("personDao");      	
        ro.findPersons();
        
        ro.addEventListener(ResultEvent.RESULT, updateSearch);
    }
    
    /**
     * Updates search.
     */ 
    private function updateSearch(event:ResultEvent):void {
        var psml:PersonSearchModelLocator = PersonSearchModelLocator.getInstance();
        
        psml.personData.source = (event.result as ArrayCollection).source;
    }
    	
}
                
            

The PersonDeleteCommand was associated with the PersonDeleteEvent in the PersonController. It retrieves the person id from the event and makes a delete request to the server. Upon success a PersonSearchEvent is fired to display the latest data in the search results. It would be more efficient to just remove the row from the ArrayCollection, but this was done to illustrate how easy it is to perform different tasks in the application once everything is cleanly decoupled using MVC.

Example 8. PersonDeleteCommand

                
public class PersonDeleteCommand implements ICommand {

    private const logger:ILogger = Log.getLogger("org.springbyexample.web.flex.controller.command.PersonDeleteCommand");
            
    /**
     * Implementation of <code>ICommand</code>.
     */
    public function execute(event:CairngormEvent):void { 
        var pde:PersonDeleteEvent = event as PersonDeleteEvent;
        var id:int = pde.id;
        
        logger.info("Delete person.  id=" + id);
        
        var ro:RemoteObject = new RemoteObject("personService");  
        ro.remove(id);
        
        ro.addEventListener(ResultEvent.RESULT, updateSearch);
    }
    
    /**
     * Updates search.
     */ 
    private function updateSearch(event:ResultEvent):void {
        var pse:PersonSearchEvent = new PersonSearchEvent();
        pse.dispatch();
    }
    	
}