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 are 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, you 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,Event

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"/>
  <keep-as3-metadata name="Event"/>
  ...
</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>
  <name>Event</name>
</keep-as3-metadata>

 

Client Components

Let's start with a simple component.

Tide 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(searchResult);
    }

    private function searchResult(event:TideResultEvent):void {
        hotels = event.result as ArrayCollection;
    }
}

 

  1. The Name annotation defines a name for the component. This name will be used to get a component instance from the Tide context. For our HotelsCtl component we will get an instance by getting tideContext.hotelsCtl.
     
  2. The In annotation 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.
     
  3. Similarly, the second In on hotelSearch gives a reference on a remote proxy for the Seam component hotelSearch.
     
  4. The Observer annotation is also similar to the one in Seam. It observes events that are 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.

You then have to manually register the class as a component on the Tide singleton with Seam.getInstance().addComponents([HotelsCtl]). This is similar to the definition of Seam components in the components.xml file or Spring applicationContext.xml.

Application initialization

The classic initialization of a Tide application consists in 3 parts:

<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>

Note: the example here uses the Seam singleton but the explanation is exactly the same with Spring or Ejb.

(1) Seam.getInstance().initApplication() must be called in the application preinitialize handler. It will register the application with the Tide runtime.

(2) Seam.getInstance().getSeamContext() retrieves the global Tide context. It is optional and can be omitted when the context is not necessary.

(3) Seam.getInstance().addComponents(...) registers client components.

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 our components are properly defined before the Flex framework starts initializing data bindings and other internal data. 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.

Using client components

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 context.
It is also possible to get a reference to a component by injection with:

[In]
public var hotelsCtl:HotelsCtl

Note: Component names that are not defined as client components and whose type is Object 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.

It is also possible to define static injection 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 supporting only property chains but not indexed properties or method calls. Note that 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, or on UI components.

This component framework is very similar to Seam and is very closely integrated with Seam server-side bijection and events. When using Spring or Ejb, there are small differences but for example, [In] can be compared to the @Autowired annotation of Spring beans and most concepts are similar (all these frameworks ultimately use some kind of dependency injection mechanism).

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 initialization 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.

Here is an example on how to handle dynamic loading of Flex modules :

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 and resets them to null after, Tide injections are made in real-time and are never nulled. This behaviour is necessary to have a clean integration with Flex data binding.

Variables annotated with the [In] or [Out] annotation must be public or use a Flex namespace.

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 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". Automatic creation can also be disabled at the component level with [Name("someComponent", autoCreate="false")].

[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:

MXML
<mx:Panel>
    <mx:Script>
        [Bindable] [In]
        public var sendDisabled:Boolean;
    </mx:Script>
    
    <mx:Button id="sendMail" disabled="{sendDisabled}" label="Send mail"
        click="..."/>
</mx:Panel>
Component
[Bindable]
[Name("sendMailCtl")]
public class SendMailCtl {
    
    [Out]
    public var sendDisabled:Boolean = true;
    

    public function disableSend():void {
        sendDisabled = true;
    }

    public function enableSend():void {
        sendDisabled = false;
    }
}

 

When using Seam, variables outjected from a client component can be sent to the server as context variables with [Out(remote="true")]. Context variables that have been received from the server are automatically marked as remote.

Integration with server-side injection/outjection

Tide is fully integrated with the Seam server-side bijection mechanism. The Tide client bindings will ensure that if the hotelSearch server component outjects the hotels context variable on the server, it will be propagated transparently to the hotels context variable and then to the hotels property of all injected client component properties (here HotelsCtl.hotels.

We could simplify our client component with :

Tide component with Seam
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();      
    }
}

 

We assume here that the Seam server component outjects the 'hotels' variable:

Seam component
@Name("hotelSearch")
public class HotelSearchAction implements HotelSearch {
    
    @Out
    private List<Hotel> hotels;

    public void find(String searchString) {
        ...
        hotels = someQuery.getResultList();
    }
}

This can also work with Spring MVC/Grails controllers with a small change on the Flex side (Tide only supports annotated Spring MVC controllers). Integration with Spring controllers requires to send a parameter map instead of using injection.

Tide component with Spring/Grails
import mx.collections.ArrayCollection;

[Name("hotelsCtl")]
[Bindable]
public class HotelsCtl {
     
    [In]
    public var hotels:ArrayCollection;

    [In]
    public var hotelSearchController:Object;

    [Observer("searchForHotels")]
    public function search(searchString:String):void {
        hotelSearchController.find({searchString: searchString});      
    }
}

 

Spring MVC controller
@Controller("hotelSearchController")
public class HotelSearchController implements HotelSearch {
    
    @RequestMapping("/hotelSearch/find")
    public ModelAndView find(@RequestParam("searchString") String searchString) {
        List<Hotel> hotels = someQuery.getResultList();
        return new ModelMap("hotels", hotels);
    }
}

 

Grails controller
@TideEnabled
class HotelSearchController {

    List hotels	
	
    def find = {
	hotels = Hotel.list();
    }
}

 

Untyped (String-based) 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 cannot be private and must be public or defined in a Flex namespace.

Observer methods can have 2 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.
    [Observer("myEvent")]
    public function eventHandler(event:TideContextEvent):void {
       // do something
    }
    
  • any other "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.
    [Observer("myEvent")]
    public function eventHandler(arg1:String, arg2:Object):void {
       // do something
    }
    

The tideContext.raiseEvent(type, params) dispatches an event to other interested components in the same context.

   tideContext.raiseEvent("myEvent", "arg1", { arg2: "value" });

Alternatively it is possible to dispatch a TideUIEvent from any Tide managed component :

   dispatchEvent(new TideUIEvent("myEvent", "arg1", { arg2: "value" });

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")]. It is also possible to disable this automatic instantiation for a particular observer with [Observer("someEvent", create="false")].

Typed events

Untyped events are handy to deal with remote events coming from Seam or when dynamic event types have to be used. In other cases, String-based event types can easily lead to typing errors or require a lot of constants (that unfortunately cannot be used in the Observer annotation).

It is thus possible to define an observer for a particular typed Flex event :

   [Observer]
   public function eventHandler(event:MyEvent):void {
      // do something
   }

This observer will be called whenever an event of this exact class is dispatched by a Tide managed component.

   dispatchEvent(new MyEvent({test: "test"}));

For this mechanism to work, Tide needs to be aware or the event. If the event class extends org.granite.tide.events.AbstractTideEvent, there is nothing else to do.

    public class MyEvent extends AbstractTideEvent {
        
        public var data:Object;
        
        public function MyEvent(data:Object):void {
       	    super();
            this.data = data;
        }
    }

AbstractTideEvent is a convenient abstract class, but any event class that is bubbling, cancelable and with the event type org.granite.tide.events.TideUIEvent.TIDE_EVENT can be used.

Otherwise, it is possible to use any valid Flex event, but in this case it is required to add an additional Event annotation in the source components for each dispatched event, and add the Event annotation in the list of annotations kept by the Flex compiler.

    public class MyEvent extends Event {
        
        public var data:Object;
        
        public function MyEvent(data:Object):void {
       	    super("myEvent", true, true);
            this.data = data ;
        }
    }
[Event(name="myEvent")]

public class MyComponent {
    private function test():void {
        dispatchEvent(new MyEvent({test: "test"});
    }
}

Integration with UI Components

The UI part is designed to enable the use of any plain MXML or ActionScript UIComponent with minimal code modification. In most cases, it should be possible to completely decouple the views from their controllers by just adding a few annotations.

First the UI components will have to be bound to the Tide context. Tide is able to listen to component additions in the current display tree. UI components that are annotated with [Name("myComponentUI")] will be automatically registered in the global context under the specified name. When the name is omitted in the annotation (simply [Name], the Flex component id will be used, that can be useful when using the same MXML multiple times.

With the injection mechanism, you can also have a UI component automatically created in a controller and bound to the correct context. This is useful mainly with conversation scoped components that dynamically create their UI (editing tabs for example).

[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 bound, UI components can interact with other parts of the application by two means:

  • they can dispatch an untyped TideUIEvent or a typed event which will be intercepted by the context and dispatched to interested observers (other UI or controller components).
  • they can get injected with context variables.
Using TideUIEvent
<mx:Panel>
    <mx:Metadata>
        [Name("myPanelUI")]
    </mx:Metadata>

    <mx:Script>
        import org.granite.tide.events.TideUIEvent;

        [In]
        public var user:User;
    </mx:Script>

    <mx:Label text="{user.name}"/>
    
    <mx:Button label="Click" click="dispatchEvent(new TideUIEvent('clickEvent'));"/>
</mx:Panel>
Using typed Tide events
<mx:Panel>
    <mx:Metadata>
        [Name("myPanelUI")]
    </mx:Metadata>

    <mx:Script>
        [In]
        public var user:User;
    </mx:Script>

    <mx:Label text="{person.lastName}"/>

    <mx:Button label="Click" click="dispatchEvent(new ClickEvent());"/>
</mx:Panel>
import org.granite.tide.events.AbstractTideEvent;

public class ClickEvent extends AbstractTideEvent {
        
    public function ClickEvent():void {
        super();
    }
}
Using standard Flex events
<mx:Panel>
    <mx:Metadata>
        [Name("myPanelUI")]

        [Event(name="clickEvent")]
    </mx:Metadata>

    <mx:Script>
        [In]
        public var user:User;
    </mx:Script>

    <mx:Label text="{person.lastName}"/>

    <mx:Button label="Click" click="dispatchEvent(new ClickEvent());"/>
</mx:Panel>
public class ClickEvent extends Event {
        
    public function ClickEvent():void {
        super("clickEvent", true, true);
    }
}

When received by an observer, the TideUIEvent will be translated to a TideContextEvent with the same parameters, and argument matching can be used for the handler method :

[Observer("someEvent)"]
public function someEventHandler(event:TideContextEvent):void {
    ...
}
[Observer("someEvent)"]
public function someEventHandler(param1:String, param2:Object):void {
    ...
}

Other kinds of events will be transmitted as is to the observer:

[Observer("someEvent)"]
public function someEventHandler(event:SomeEvent):void {
    ...
}

Using events makes your UI completely independent of the rest of the application, and minimally dependent on the Tide framework, except for the annotations.

Conversations

There are various use cases where it is useful to have many instances of the same component. For example tabbed panels that display different entities of the same type, or wizard interfaces that span many pages and then need to be destroyed. To handle this, 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 means that it is possible to have many instances of the component at the same time; one for each active conversation context. But it is not possible that a component is present in both conversation scope and global scope with the same name.

When using Seam, injections and events in the client conversation context are bound to the corresponding server conversation (conversation with the same id). 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 perfectly possible to define client-only conversations that will use only stateless 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.

A client conversation can be start from the UI by dispatching a TideUIConversationEvent or any event implementing IConversationEvent. The first argument in the TideUIConversationEvent 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.

All entities passed as parameters to the new context are cloned and merged in the target context. Thus all changes made on these entities have no impact on the global context until the conversation context is ended and merged back.

Here is the modified hotel search that starts a booking wizard when the user clicks on one hotel:

MXML list
<mx:Application preinitialize="Seam.getInstance().initApplication()">
  <mx:Script>
    // Components initialization in a static block
    Seam.getInstance().addComponents([HotelsCtl]);

    [Bindable] [In]
    public var hotels:ArrayCollection;

    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:TabNavigator id="tabNavigator">
       ...
    </mx:TabNavigator>
  </mx:Panel>
</mx:Application>
Conversation-scoped component
[Bindable]
[Name("bookingProcessCtl", scope="conversation")]
public class BookingProcessCtl {
        
    private var finished:Boolean = false;
        
    [In("#{application.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 remaining 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, org.granite.tide.login and org.granite.tide.logout, are dispatched by the framework. Any component can observe these events and do initialization or cleaning job:

[Observer("org.granite.tide.logout")]
public function clean(event:TideContextEvent):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.
 


Browse Space

- Pages
- Blog
- Labels
- Attachments
- Bookmarks
- Mail
- Advanced

Explore Confluence

- Popular Labels
- Notation Guide

Your Account

Log In

Other Features

Add Content


Copyright © 2011 Granite Data Services S.A.S. All Rights Reserved.