Introduction to the Tide framework for Flex 3
Join the DZone community and get the full member experience.
Join For FreeTide is an open source application framework for Adobe Flex that has been part of the Granite Data Services project for more than one year. It was developed at first to build complex applications with a clean architecture while being easy to use and avoid useless boilerplate code when possible. Later it has been improved with more advanced features such as dependency injection, a client data persistence context and a strong integration with other GraniteDS features. There have been a lot of other Flex frameworks since then but we still think that Tide is a valuable addition to the list, even outside of a pure GraniteDS environment.
So let's have a look at a very simple Flex login form built with Tide to demonstrate its main characteristics.
Flex application:<?xml version="1.0" encoding="utf-8"?>
<mx:Application
layout="absolute"
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*"
preinitialize="Tide.getInstance().initApplication()">
<mx:Script>
<![CDATA[
import org.granite.tide.Tide;
import com.myapp.controllers.LoginController;
Tide.getInstance().addComponents([LoginController]);
]]>
</mx:Script>
<MainApp id="mainApp"/>
</mx:Application>
Main view: MainApp.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:ViewStack
xmlns:mx="http://www.adobe.com/2006/mxml"
selectedIndex="{loggedIn ? 1 : 0}" width="100%" height="100%">
<mx:Metadata>[Name]</mx:Metadata>
<mx:Script>
<![CDATA[
import com.myapp.events.*;
[Bindable] [In]
public var loggedIn:Boolean;
[Bindable] [In]
public var loginMessage:String;
]]>
</mx:Script>
<mx:Panel id="loginPanel">
<mx:Form>
<mx:FormItem label="Username">
<mx:TextInput id="username"/>
</mx:FormItem>
<mx:FormItem label="Password">
<mx:TextInput id="password" displayAsPassword="true"
enter="dispatchEvent(new LoginEvent(username.text, password.text));"/>
</mx:FormItem>
</mx:Form>
<mx:Label text="{loginMessage}" textAlign="center"/>
<mx:Button label="Login"
click="dispatchEvent(new LoginEvent(username.text, password.text));"/>
</mx:Panel>
<mx:Panel id="appPanel">
<mx:Label text="You're logged in !"/>
<mx:Button label="Logout"
click="dispatchEvent(new LogoutEvent());"/>
</mx:Panel>
</mx:ViewStack>
The main mxml application initializes the Tide framework in a preinitialize handler, and registers a controller component class. This is where the dependency injection takes place: Tide will scan the component class and prepare various listeners and bindings. The component is not instantiated at this time though, Tide will create an instance only when the application needs it.
The other component is a simple mxml view that simply dispatches custom events on user actions. The only things specific to Tide are a few annotations: the [Name] annotation instructs Tide to manage the component, and will enable two things :
- events dispatched by the component will be intercepted by Tide and redispatched to interested observers.
- properties marked with the [In] annotation will be injected from variables or components of the same name in the Tide context.
Controller:
package com.myapp.controllers
{
import com.myapp.events.LoginEvent;
[Name("loginController")]
[Bindable]
public class LoginController {
[Out]
public var loggedIn:Boolean = false;
[Out]
public var loginMessage:String = "";
[Observer]
public function loginHandler(event:LoginEvent):void {
if (event.username == "demo" && event.password == "demo") {
loggedIn = true;
loginMessage = "";
}
else
loginMessage = "Username or password incorrect";
}
[Observer]
public function logoutHandler(event:LogoutEvent):void {
loggedIn = false;
}
}
}
Once again the [Name] annotation on the controller indicates a Tide managed component. The important thing is that this is a stateful component, that will be instantiated only once, and maintains the state of the login part of the application (loggedIn and loginMessage). Its observer methods are called when instances of LoginEvent and LogoutEvent are dispatched by a managed component, and can change the component state.
The [In]/[Out] bijection annotations allow to build data bindings between properties of different component instances. With the Tide bijection an update on the controller properties will be outjected to the Tide context and then injected to the view properties. The property matching is made by default on the property name but can be overriden with [In("someVariable")].
package com.myapp.events
{
import org.granite.tide.events.AbstractTideEvent;
public class LoginEvent extends AbstractTideEvent {
public var username:String;
public var password:String;
public function LoginEvent(username:String, password:String):void {
super();
this.username = username;
this.password = password;
}
}
}
package com.myapp.events
{
import org.granite.tide.events.AbstractTideEvent;
public class LogoutEvent extends AbstractTideEvent {
public function LogoutEvent():void {
super();
}
}
}
This simple example shows how Tide allows decoupling of the view and the controller parts with the event observer and bijection mechanisms. While having a clean architecture, the different components have minimal dependency on the Tide framework itself, and the overall quantity of plumbing code is very limited. More important, the typed event observer pattern avoids usual errors with string based event models: an observer method for a non existing event will just not compile.
The bijection part of the interaction still relies on strings to match the property names. We could have avoided this by using another event but it's a little heavier and not necessarily useful in all cases :
public function loginHandler(event:LoginEvent):void {
if (event.username == "demo" && event.password == "demo")
dispatchEvent(new LoginResultEvent(true, ""));
else
dispatchEvent(new LoginResultEvent(false, "Username or password incorrect"));
}
And observe this event in the mxml :
[Bindable]
public var loggedIn:Boolean;
[Bindable]
public var loginMessage:String;
[Observer]
public function loginResult(event:LoginResultEvent):void {
loggedIn = event.loggedIn;
loginMessage = event.loginMessage;
}
Calling remote services
Starting from this basic example, we can easily add a simple interation with a server through a Flex RemoteObject. Tide adds a simple layer over RemoteObject that simplifies the remoting API and allows more advanced features. We can now modify the controller by injecting a Tide RemoteObjectProxy :
com/myapp/controllers/LoginController.as[In]
public var loginService:RemoteObjectProxy;
[Observer]
public function loginHandler(event:LoginEvent):void {
loginService.login(event.username, event.password, loginResult, loginFault);
}
private function loginResult(event:TideResultEvent):void {
loggedIn = true;
loginMessage = "";
}
private function loginFault(event:TideFaultEvent):void {
loggedIn = false;
loginMessage = event.fault.faultString;
}
The injection of the client proxy automatically picks up sensible defaults when possible: the destination and source are set to the property name, and the channel of the RemoteObject is the one defined in services-config.xml when there is only one. This works automatically for example with the GraniteDS/EJB3 or GraniteDS/Spring service factories. If the defaults cannot be determined reliably (for example when there is more than one remoting channel in the Flex configuration) or are not what you want, it is possible to configure the proxy component manually (the destination is always the component name) in the main mxml.
Tide.getInstance().addComponentWithFactory("loginService", RemoteObjectProxy, { source: "loginService", channelId: "my-graniteamf" });
RemoteObjectProxy is a Flex dynamic class, meaning that it is possible to call any method on it. The method arguments are passed as is as arguments to the server method. The two last arguments are callback handlers for results and faults and are optional.
This quick introduction has described the main principles and features of the Tide framework. The framework provides many more advanced features such as entity caching or data paging and can benefit from a very tight integration with the rest of GraniteDS when using an EJB3, Spring or Seam backend. More details are available in the documentation here
Hopefully Tide can be useful for any Flex developer wanting to build cleanly architectured applications without the burden of an overly complex framework. The SWC libraries can be found in the build folder of the GraniteDS distribution here artifact.
Opinions expressed by DZone contributors are their own.
Comments