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 | |
---|---|
-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 |
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")] </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"); private var logTarget:StringBufferTarget = new StringBufferTarget(); private var logWindow:LogWindow; /** * Initialize component. */ private function initializeHandler():void { 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"> <mx:columns> <mx:DataGridColumn headerText="{resourceManager.getString('messages', 'person.form.firstName')}" dataField="firstName"/> <mx:DataGridColumn headerText="{resourceManager.getString('messages', 'person.form.lastName')}" dataField="lastName"/> <mx:DataGridColumn width="150" editable="false"> <mx:itemRenderer> <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>
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(); } }