6. Flex 3 Client Framework
The Client Framework (Flex 3 Only)
It is, of course, perfectly possible to use GDS/Tide with any Flex framework; for example, Cairngorm or PureMVC. In general you just have to keep the Context object somewhere in the framework components or get it from the static factory.
To allow a more seamless integration with the server, and also to avoid the huge amount of boilerplate code that comes with these frameworks, Tide comes with a simple client framework which looks very much like Seam and depends heavily on custom annotations, and thus works only with Flex 3. Note that even though it is inspired by Seam, it can work with any server-side technology supported by GDS/Tide but without the benefits of Seam-specific features such as bijection.
Like Seam, it is based on stateful components and not on stateless commands. There can be three types of components:
- UI components (standard MXML or ActionScript Flex components)
- Managed entities
- Tide client components (plain AS3 classes with a few annotations)
All these kinds of components communicate through the Tide context, which serves as an event bus.
Among these three types, only Tide client components are a new concept. In general, they will have the following activities:
- observe incoming events (coming from the UI or from the server)
- execute the business logic of the client application
- control the state of the UI
- interact with the server
The next part will give more details about these components.
Before being able to play with them, we will need a small configuration in either the project compiler options or the Flex framework configuration file to tell the Flex 3 compiler to keep the Tide annotations in the compiled classes.
Flex compiler option:
-keep-as3-metadata=Bindable,Managed,ChangeEvent,NonCommittingChangeEvent,Transient, Name,In,Out,Observer,Destroy,Version
Flex Ant task compiler option (Note: This does not seem to be supported correctly in all versions of the Flex 3 Ant task.):
<mxmlc ...> ... <keep-as3-metadata name="Bindable"/> <keep-as3-metadata name="Managed"/> <keep-as3-metadata name="ChangeEvent"/> <keep-as3-metadata name="NonCommittingChangeEvent"/> <keep-as3-metadata name="Transient"/> <keep-as3-metadata name="Name"/> <keep-as3-metadata name="In"/> <keep-as3-metadata name="Out"/> <keep-as3-metadata name="Observer"/> <keep-as3-metadata name="Destroy"/> <keep-as3-metadata name="Version"/> ... </mxmlc>
Global Flex configuration in flex_sdk_3\frameworks\flex-config.xml:
<keep-as3-metadata> <name>Bindable</name> <name>Managed</name> <name>ChangeEvent</name> <name>NonCommittingChangeEvent</name> <name>Transient</name> <name>Name</name> <name>In</name> <name>Out</name> <name>Observer</name> <name>Destroy</name> <name>Version</name> </keep-as3-metadata>
Client Components
Let's start with a simple component.
import mx.collections.ArrayCollection;
[Name("hotelsCtl")]
[Bindable]
public class HotelsCtl {
[In]
public var hotels:ArrayCollection;
[In]
public var hotelSearch:Object;
[Observer("searchForHotels")]
public function search(searchString:String):void {
hotelSearch.searchString = searchString;
hotelSearch.find();
}
}
- The Name annotation defines a name for the component. This name will be used to get a component instance from the Tide context and to manage injection in other components. For our HotelsCtl component we will get an instance by getting tideContext.hotelsCtl.
- The In annotation defines an injection. It creates a binding between a context variable/component and a property of a component instance. Here the In on hotels ensures that this property always reflects the hotels variable on the Tide context.
- Similarly, the second In on hotelSearch gives a reference on a remote proxy for the Seam component hotelSearch.
- The Observer annotation is also similar to the one in Seam. It means that the annotated method will be called when an event with the specified type is dispatched on the Tide context by other components of the application; in this example, a Search button on a form could trigger this event. The annotation parameter searchForHotels is the type of the event that will be observed. See the section on Events below.
Note that this component uses some specific features of the Seam integration; here outjection of the hotels variable. The last paragraph will go into detail about how to make it work with Spring.
It is not enough to just create this class and annotate it. It needs to be registered on the Tide/Seam singleton with Seam.getInstance().addComponents([HotelsCtl]). This is similar to defining it in a Seam components.xml file or a Spring application context.
The initialization of a Tide application will be done in the root MXML:
<mx:Application preinitialize="Seam.getInstance().initApplication()"> // Context declaration [Bindable] private var tideContext:Context = Seam.getInstance().getSeamContext(); // Components initialization in a static block Seam.getInstance().addComponents([HotelsCtl]); </mx:Application>
The parameter of addComponents() is an array of classes, so you can define as many components as needed in each call. Their configuration will be read from annotations defined in the AS3 class. The use of a static block, instead of a initialization handler, ensures that the components are properly defined before the Flex framework starts initializing data bindings. This way it will correctly bind UI components to expressions like
{tideContext.myComponent}
.
Alternatively, you can define components one by one with addComponent():
Seam.getInstance().addComponent("people", PagedQuery);
This is useful for components that can exist multiple times with different names for the same implementation class; for example, the PagedQuery component.
Once registered, the components can be automatically instantiated when retrieved from the context. tideContext.hotelsCtl will get the current instance or create a new instance of the component. There will always be only one instance of each component in a particular context. This automatic instantiation behaviour can be disabled either at component level with:
[Name("myComponent", autoCreate="false")]
Or at each injection point with:
[In(create="false")]
Note: Component names that are not defined as client components are by default initialized as remote proxies. They can be used either for proxying a server component with the same name or for an untyped context variable. For example, by default, tideContext.hotelSearch will create a remote proxy for the server Seam component hotelSearch.
Due to this default behaviour, it is highly recommended to use a clear naming convention for the different kinds of components. The sample projects all use the suffix Ctl, for example myComponentCtl, for naming the client components and avoid naming conflicts with server components.
The In annotation is implemented with Flex data binding. It is also possible to define static injections on a component at definition time with:
tide.addComponentWithFactory("searchCtl", SearchCtl, {
personHome: "#{personHome}",
persons: "#{mainAppUI.persons}",
people: "#{people}",
examplePerson: "#{examplePerson}"
});
The properties map contains pairs of property name/injected value. Simple types are allowed, and values enclosed with #{...}, are considered as simplified EL expressions that support only property chains but not indexed properties or method calls. With static injection, the values will be injected only once at the creation of the component instance. This is thus mainly suitable for initialization values, references on other client components that are not supposed to change, or on MXML UI components.
Spring & EJB Limitations
If you use Spring or EJB, the integration is less deep and there will be a little learning curve because most features are borrowed and adapted from Seam. For example, here is a similar client component for use with Spring:
import mx.collections.ArrayCollection;
[Name("hotelsCtl")]
[Bindable]
public class HotelsCtl {
[In] [Out]
public var hotels:ArrayCollection;
[In]
public var hotelSearch:Object;
[Observer("searchForHotels")]
public function search(searchString:String):void {
hotelSearch.find(searchString, findResult);
}
private function findResult(event:TideResultEvent):void {
hotels = event.result as ArrayCollection;
}
}
You can notice the small difference with the Seam version. Spring components do not support bijection on the server so we have to get the hotels collection more classically as a method result. You can also notice the Out annotation on the hotels variable, which allows to publish the component variable hotels to the context. This was not needed with Seam because the hotels variable was already received in the context from server outjection. In most cases, there is a little more code but this is no big deal.
Spring MVC integration
The Tide/Spring integration can detect when Spring MVC is available on the classpath and then provide a way to use Spring controllers and automatically get the model variables in the context. Only Spring controllers with annotations are supported (or Grails controllers when using the Grails plugin).
import mx.collections.ArrayCollection;
[Name("hotelsCtl")]
[Bindable]
public class HotelsCtl {
[In]
public var hotels:ArrayCollection;
[In]
public var hotelSearch:Object;
[Observer("searchForHotels")]
public function search(searchString:String):void {
hotelSearch.find({searchString: searchString});
}
}
@Controller("hotelSearchController") @Transactional public class HotelSearchController { @RequestMapping("/hotelSearch/find") public ModelMap findHotels(@RequestParam("searchString") String searchString) { // Get hotels from somewhere return new ModelMap("hotels", hotels); }
In this case, the client method call is mapped to a request handled by the controller. When the first argument of the call is a map, its properties are mapped to request parameters for the Spring controller. All properties of the returned model are automatically bound to variables in the client Tide context.
Tide Modules & Flex Modules
If you have a great number of components to initialize, your main MXML application will be polluted with lots of Tide initializations. This can be cleaned up by implementing a Tide module class, which just requires an init method:
Seam.getInstance().addModule(MyModule);
public class MyModule implements ITideModule {
public function init(tide:Tide):void {
tide.addExceptionHandler(ValidationExceptionHandler);
...
tide.addComponents([Component1, Component2]);
tide.addComponent("comp3", Component3);
...
}
}
Using Tide modules is also useful if you need to register components that are dynamically loaded from a Flex module. In this case, Tide needs to know the Flex ApplicationDomain to which the component classes belong, and you have to pass it to the Tide.addModule method.
private var _moduleAppDomain:ApplicationDomain; public function loadModule(path:String):void { var info:IModuleInfo = ModuleManager.getModule(path); info.addEventListener(ModuleEvent.READY, moduleReadyHandler, false, 0, true); _moduleAppDomain = new ApplicationDomain(ApplicationDomain.currentDomain); info.load(appDomain); } private function moduleReadyHandler(event:ModuleEvent):void { var loadedModule:Object = event.module.factory.create(); loadedModule.init(_moduleAppDomain); }
<mx:Module> <mx:Script> public function start(appDomain:ApplicationDomain = null):void { Seam.getInstance().addModule(MyLoadedModule, appDomain); } </mx:Script> </mx:Module>
Bijection
A component can be injected with any component or variable of its context with the In annotation; once again like Seam. The In is implemented as a binding between context.variableName and component.variableName. This means that contrary to Seam that injects/outjects variables on method calls, the Tide injections are made in real-time.
| Variables annotated with the [In] or [Out] annotation must be public. |
The reason behind this limitation is that ActionScript metadata reflection capabilities do not allow access to private methods and properties.
For the previous HotelCtl component, the hotels property will then be bound to tideContext.hotels.
It is worth noting that this binding mechanism will ensure that when the hotelSearch server component outjects the hotels context variable on the server, it will be propagated transparently to the hotels property of the HotelsCtl client component and to any other component having this In annotation, even a property of a custom UI component.
It is possible to use a simplified EL expression in the In annotation to bind on a property chain.
[Bindable] [In("#{personHome.instance}")]
public var person:Person;
Simplified means that only property chains are supported, as in BindingUtils.bindProperty, but not indexed properties or complex expressions; for example, [In("#{personHome.instances[1]}")] will not work.
By default, injection of a non-existent context variable will automatically create a new instance except for UI component types. It is possible to force the instantiation of the context variable with create="true" or disable the automatic creation with create="false"; the latter does not work in 1.2 GA due to a bug:
[Bindable] [In(create="true")]
public var personUI:PersonMXML;
In this case, if there is no personUI variable in the context, it will be automatically instantiated and put in the context, then injected in the component.
Finally, injection of the current context, the context to which the component belongs, can be done with:
[In]
public var tideContext:Context;
You can also define an outjection between a component property and a context variable. It can be used to easily change the state of the UI without coupling the component and the UI too much. For example:
<mx:Panel> <mx:Script> [Bindable] [In] public var sendDisabled:Boolean; </mx:Script> <mx:Button id="sendMail" disabled="{sendDisabled}" label="Send mail" click="..."/> </mx:Panel>
[Bindable]
[Name("sendMailCtl")]
public class SendMailCtl {
[Out]
public var sendDisabled:Boolean = true;
public function disableSend():void {
sendDisabled = true;
}
public function enableSend():void {
sendDisabled = false;
}
}
By default, variables outjected from a client component will not be sent to the server. It is possible to override this behaviour with [Out(remote="true")].
Events
A client component can register Observer methods, which can listen to client events or remote Seam events with remote="true".
For the same reason as In properties, Observer methods must be public.
Observer methods can have two possible signatures:
- a single TideContextEvent argument: They get an event object and can get the event context and the event parameters from the event.params array.
- a "normal" signature: They get the params array of the event as arguments of the method. Consequently they do not get the Context, but in most cases this is not necessary.
The tideContext.raiseEvent(type, params) allows to dispatch an event to other interested components in the same context.
Note: If a component has registered an observer for an event and is not instantiated when the event is raised, it will be automatically instantiated, unless it is marked as [Name("myComponent", autoCreate="false")].
Integration with UI Components
The UI part is a little more tricky, but it is designed to enable the use of any plain MXML or ActionScript UIComponent with minimal code modification.
First the UI components will have to be bound to the Tide context. In practice, that consists of assigning a context variable with the UI component, such as tideContext.loginUI = myApplication.login.
Once bound, UI components can interact with other parts of the application by two means:
- they can dispatch a TideUIEvent which will be intercepted by the context and dispatched to interested observers.
- they can get injected with context variables as any other client component.
The most difficult part is to correctly bind the UI component to the context. For this, there are two main cases:
- Root MXML: It is the place where the context is initially declared so it is the most simple case:
<mx:Application ...> <mx:Script> private function init():void { // We just put the current mxml instance in the context under // the name 'mainAppUI' tideContext.mainAppUI = this; } </mx:Script> </mx:Application>
- Children of registered MXML components: the parent MXML has to put its children in the correct context by listening their preinitialize events :
Unable to find source-code formatter for language: mxml. Available languages are: actionscript, html, java, javascript, none, sql, xhtml, xml
<mx:Application ..> <mx:ViewStack> <Login id="loginUI" preinitialize="tideContext.loginUI = loginUI"/> <MainPanel id="mainPanelUI" preinitialize="tideContext.mainPanelUI = mainPanelUI"/> </mx:ViewStack> </mx:Application> - Other MXML/AS components: Usually they will be component dynamically created in controllers. It is up to the controller to bind the created components in the correct context. Example with a popup:
[Observer("showLoginForm")] public function showLoginForm(event:TideContextEvent):void { var loginPopUpWindow:loginPopUp = PopUpManager.createPopUp( event.context.mainAppUI, loginPopUp, true) as loginPopUp; event.context.loginUI = loginPopUpWindow; // Bind the popup in the context PopUpManager.centerPopUp(loginPopUpWindow); }
With the injection mechanism, you can even have a UI component automatically created in a controller and bound to the correct context.
[Bindable]
[Name("bookingProcessCtl", scope="conversation")]
public class BookingProcessCtl {
[In]
public var mainAppUI:UIComponent;
// The panel UI component will be instantiated at the time of
// component initialization and automatically bound to the controller context
[In(create="true")]
public var hotelSelectionUI:HotelSelectionPanel;
[Observer("selectHotel")]
public function selectHotel(hotel:Hotel):void {
hotelBooking.selectHotel(hotel, selectHotelResult);
}
private function selectHotelResult(event:TideResultEvent):void {
mainAppUI.addChild(hotelSelectionUI);
mainAppUI.selectedChild = hotelSelectionUI;
}
}
Once everything is setup and the UI components are correctly bound to the appropriate contexts, the UI can dispatch events that will be observed by controllers. For example, for the hotel search functionality, we could have a very basic MXML like this:
<mx:Application creationComplete="init();"> <mx:Script> [Bindable] private var tideContext:Context = Seam.getInstance().getSeamContext(); // Components initialization in a static block Seam.getInstance().addComponents([HotelsCtl]); [Bindable] [In] public var hotels:ArrayCollection; private function init():void { tideContext.mainAppUI = this; } private function search(searchString:String):void { dispatchEvent(new TideUIEvent("search", searchString)); } </mx:Script> <mx:Panel> <mx:TextInput id="fSearchString"/> <mx:Button label="Search" click="search(fSearchString.text);/> <mx:DataGrid id="dgHotels" dataProvider="{hotels}"> <mx:columns> <mx:DataGridColumn headerText="Name" dataField="name"/> </mx:columns> </mx:DataGrid> </mx:Panel> </mx:Application>
Using such events, you can start building the whole UI without even having to write one client component.
We have a reasonably compact code, and we keep a clear and clean separation of concerns between the various parts of the application.
Conversations
The framework provides support for client conversations. The Name annotation provides an optional attribute to define the scope of the component; for example, [Name("myComponent", scope="conversation")].
Conversation-scoped components will be created only in conversation contexts and will be destroyed at the end of the conversation. That also means that it is possible to have many instances of the component at the same time; one for each active conversation context. But, for now, it is not possible that a component with the same name is present in both conversation scope and global/session scope.
When using Seam, injections and events in the client conversation context are bound to the corresponding server conversation. A context variable outjected by a server Seam component in the conversation context will be only visible on the Flex side by client components of the context with the same id as the server conversation.
It is possible to define a client-only conversation which uses only event-scoped server components. Client conversations are a good way to manage well-defined user interactions. They allow to minimize memory usage by cleaning the whole context when the user work is finished. It is possible to model many use cases in an application with client conversations, such as wizards, editing panels, etc., and this can also be useful to handle use cases like having multiple windows on entities of the same type.
It is possible to start a client conversation from the UI by dispatching a TideUIConversationEvent. The first argument in the event constructor is the conversation id. Using an id for which there is already a context will redirect to the existing context and not create a new one. When interacting with a Seam server conversation, it is not mandatory to define an id from Flex. In this case, there are two options: use a null id and the first call to a server conversation-scoped component will retrieve the id from the server, or you can force an id, but in this case, you will have to use @Begin(join=true) because Tide itself will start a server conversation with the specified id.
Note that all entities passed as parameters to the new context are cloned. Thus all changes made on these entities have no impact on the global context until the conversation context is ended and merged.
Here is the modified hotel search that starts a booking wizard when the user clicks on one hotel:
<mx:Application creationComplete="init();"> <mx:Script> [Bindable] private var tideContext:Context = Seam.getInstance().getSeamContext(); // Components initialization in a static block Seam.getInstance().addComponents([HotelsCtl]); [Bindable] [In] public var hotels:ArrayCollection; private function init():void { tideContext.mainAppUI = this; } private function search(searchString:String):void { dispatchEvent(new TideUIEvent("search", searchString)); } </mx:Script> <mx:Panel> <mx:TextInput id="fSearchString"/> <mx:Button label="Search" click="search(fSearchString.text);/> <mx:DataGrid id="dgHotels" dataProvider="{hotels}" change="dispatchEvent(new TideUIConversationEvent(null, 'selectHotel', dgHotels.selectedItem));"> <mx:columns> <mx:DataGridColumn headerText="Name" dataField="name"/> </mx:columns> </mx:DataGrid> </mx:Panel> </mx:Application>
[Bindable]
[Name("bookingProcessCtl", scope="conversation")]
public class BookingProcessCtl {
private var finished:Boolean = false;
[In("#{mainAppUI.tabNavigator}")]
public var nav:TabNavigator;
[In]
public var hotel:Hotel;
[In(create="true")] [Out]
public var hotelSelectionUI:HotelSelection;
[In]
public var hotelBooking:Object;
[Observer("selectHotel")]
public function selectHotel(hotel:Hotel):void {
hotelBooking.selectHotel(hotel, selectHotelResult);
}
private function selectHotelResult(event:TideResultEvent):void {
nav.addChild(hotelSelectionUI);
nav.selectedChild = hotelSelectionUI;
}
...
}
The bookingProcessCtl components observes the selectHotel event. When it is triggered, it will call the server component hotelBooking to start the server conversation and get its id. When it gets the server result, it adds the hotel form which has been automatically added in the context with create="true" in the application tab navigator. Further user actions on this form could then be observed by this component.
Ending a conversation can be done by two means:
- When the server conversation ends, the client conversation is marked as finished. You will be able to process final data in the result/fault handler and then the whole context will be destroyed. It is possible though to keep the existing client context with tideContext.meta_continue(merge) if you need that the client context spans many server conversations. In this case, its id will not change and all server conversations will need to have to same id.
- You can call tideContext.meta_end(merge) from the client component. If merge=true, this will immediately merge the context entities with the global context. After this, the context will be completely destroyed.
Security
One of the main goals of the framework is to simplify the integration between the Flex application and the server application.
The Identity built-in client component manages login and logout of the application. It provides the login() and logout() methods, which are mapped to the Seam server Identity component. The isLoggedIn property indicates if the client is authenticated.
Two events, login and logout, are dispatched by the framework. Any component can observe these events and do initialization or cleaning job:
[Observer("logout")]
public function clean(event:TideLoginEvent):void {
event.context.hotels = null;
}
Finally, you can use [Name("myComponent", restrict="true")] on components which cannot be used when the user is not logged in. Restricted components will also be cleared of all data when the user logs out.
