mardi 21 octobre 2008

The GDS/Tide Flex 3 framework

Since 1.1 RC3, the Tide framework has been including a not much advertised Flex framework, that is tightly integrated with the whole GDS/Tide data management / service integration functionalities and can completely remove the need for using Cairngorm or PureMVC to develop properly structured Flex applications.

The Tide Flex framework is heavily inspired from JBoss Seam, with an architecture based on stateful components. It borrows the most important concepts of Seam: bijection, events, conversations and is based on Flex 3 custom annotations. In general it shares with Seam the goal to eliminate complexity, thus allows to use simple annotated ActionScript classes with very little configuration.

A Tide application is composed of 3 kinds of components :

- Managed entities (the data model)
- UI view (annotated mxml or ActionScript Flex components)
- Tide components (annotated ActionScript classes)

The overall architecture is centered around the Tide context, which serves as an event bus between the UI view, the Tide components and the server, and as a container for managed entities.

The Tide components contain the core logic of the application. They play the same role on the client as Seam components on the server. Compared to Cairngorm for example, they share similar functionalities with commands and business delegates.
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

Let's look at a very simple example of a search list to show how this works :

Seam component (simply extracted from the Seam booking sample) :
@Stateful
@Name("hotelSearch")
@Scope(ScopeType.SESSION)
@Restrict("#{identity.loggedIn}")
public class HotelSearchingAction implements HotelSearching {

@PersistenceContext
private EntityManager em;

private String searchString;
private int pageSize = 10;
private int page;

@DataModel
private List hotels;

public void find() {
page = 0;
queryHotels();
}
...
private void queryHotels() {
hotels = em.createQuery("select h from Hotel h where lower(h.name) like #{pattern} or lower(h.city) like #{pattern} or lower(h.zip) like #{pattern} or lower(h.address) like #{pattern}")
.setMaxResults(pageSize)
.setFirstResult( page * pageSize )
.getResultList();
}
...
public List getHotels() {
return this.hotels;
}

public int getPageSize() {
return pageSize;
}

public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}

@Factory(value="pattern", scope=ScopeType.EVENT)
public String getSearchPattern() {
return searchString==null ? "%" : '%' + searchString.toLowerCase().replace('*', '%') + '%';
}

public String getSearchString() {
return searchString;
}

public void setSearchString(String searchString) {
this.searchString = searchString;
}

@Remove
public void destroy() {}
}

MXML application :

<mx:Application creationComplete="init();">
<mx:Script>
[Bindable]
(1) private var tideContext:Context = Seam.getInstance().getSeamContext();

// Components initialization in a static block
{
(2) Seam.getInstance().addComponents([HotelsCtl]);
}

[Bindable] [In]
(3) public var hotels:ArrayCollection;

private function init():void {
(4) tideContext.mainAppUI = this;
}

private function search(searchString:String):void {
(5) dispatchEvent(new TideUIEvent("searchForHotels", 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>

Tide component :
import mx.collections.ArrayCollection;

[Name("hotelsCtl")]
[Bindable]
public class HotelsCtl {

[In]
(6) public var hotels:ArrayCollection;

[In]
(7) public var hotelSearch:Object;

(8) [Observer("searchForHotels")]
public function search(searchString:String):void {
(9) hotelSearch.searchString = text;
hotelSearch.find();
}
}
  1. The main application initializes and gets the Tide context.
  2. The application registers the component class HotelsCtl in Tide. This will scan the class and detect the name of the component from the annotation [Name("hotelsCtl")]. Once registered, we will be able to use tideContext.hotelsCtl to instantiate or get a unique instance of the component.
  3. The [In] annotation defines a data binding between the variable named 'hotels' in the Tide context and the variable 'hotels' of the mxml component. It's worth noting that it's consequently also bound to the 'hotels' variable in the server Seam context because Tide synchronizes the client and server context at each remote call.
  4. The application registers itself in the Tide context. This is very important because it allows Tide to intercept the events dispatched by the application, and notify the interested components.
  5. When the user clicks on the search button, the application dispatches a TideUIEvent. This is a common event class that has a simple string type and can hold parameters. In this case, it holds the search string entered by the user.
  6. The component also get injected with the 'hotels' context variable. Note that we will always get the exact same collection instance here than in the mxml UI because the source context variable is the same.
  7. The component get injected with the 'hotelSearch' context variable. We have neither defined a type for this variable nor the name corresponds to a registered Tide component, so Tide will instantiate a common proxy object. Calling any method on this proxy object will trigger a remote call on this method on the server Seam component named 'hotelSearch'.
  8. The component declares an observer method (annotated with Observer). This method will be called when an event of the expected type (here 'searchForHotels') is dispatched on the context. In this case, it will be called when the user clicks on the search button. The method receives the event parameters as arguments.
  9. The component sets the 'searchString' property on the 'hotelSearch' proxy, then calls the 'find' method. Tide will transmit the property updates to the server, and execute them on the Seam component. On the server, Tide will set the searchString property on the Seam component 'hotelSearch', then call the 'find' method. 'hotels' is annotated with @DataModel, so Tide intercepts the outjection and sends the value back to the Flex client along the result of the remote call. On the client, Tide finally merges the 'hotels' collection with the one in the Tide context. The visible result for the user is that the DataGrid content is updated, because it is bound to this context variable.

This simple example demonstrates much of the concepts of the Tide framework. Using the context as a central repository for all elements of the application allows a correct decoupling between the UI layer and the application logic, while keeping the amount of classes and code to a minimum. The framework contains a few more features, notably support for client conversations, but keeps relatively lightweight.

Combined with Seam on the server, it does most of the tedious integration work automatically and helps developers to focus on the real application logic.

The Tide sample projects in the GDS distribution do not fully use the framework because they must support Flex 2 compatibility.
Two Flex 3-only projects graniteds_seam_fx3 and graniteds_seam_booking_fx3 have been adapted to benefit from the Tide framework capabilties and are available in the GDS subversion repository at https://granite.svn.sourceforge.net/svnroot/granite.

A more complete description is available in the documentation here.

As always, feedback is more than welcome.

3 commentaires:

Seto a dit…

(5) dispatchEvent(new TideUIEvent("search", searchString));

here should be "searchForHotels" other than "search"? Is it a mistake?

William Draï a dit…

Thanks for the remark. It's fixed.

Seto a dit…
Ce commentaire a été supprimé par l'auteur.