GraniteDS comes with a built-in client framework named Tide that is based on the concept of contextual components and serves as the basis for many advanced features (such as data management or simplified remoting).
This client framework implements some classic features of usual Java frameworks such as Spring or Seam and provides a programming model that should look familiar to Java developers. However it tries to be as transparent and unobtrusive as possible and yet stay close from the Flex framework concepts and traditional usage.
The framework features notably:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*"
preinitialize="Tide.getInstance().initApplication()">
<mx:Script>
import org.granite.tide.Tide;
import org.granite.tide.Component;
import org.granite.tide.events.TideResultEvent;
[In]
public var helloService:Component;
private function hello():void {
helloService.hello(iname.text, helloResult);
}
private function helloResult(event:TideResultEvent):void {
lmessage.text = event.result as String;
}
</mx:Script>
<mx:TextInput id="iname"/>
<mx:Button label="Hello" click="hello()"/>
<mx:Label id="lmessage"/>
</mx:Application>
This is almost exactly the same example that we have seen in the Tide Remoting chapter. In this first step
we see only the injection feature of Tide with the annotation [In]
that can be placed on any writeable property of the component
(public or with a setter).
Tide will by default consider any injection point with a type Component
(or any type extending Component
)
as a remoting injection and will create and inject a client proxy for a service named helloService
(by default the name of the property
is used as the name of the injected component).
This is more than enough for a Hello World application, but if we continue the application this way, everything will be in the same MXML. So in a first
iteration we want at least to separate the application initialization part and the real UI part. We can for example create a new MXML file named
Hello.mxml
:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*"
preinitialize="Tide.getInstance().initApplication()">
<mx:Script>
import org.granite.tide.Tide;
</mx:Script>
<Hello id="hello"/>
</mx:Application>
Hello.mxml:
<?xml version="1.0" encoding="utf-8"?>
<mx:Panel
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*">
<mx:Metadata>[Name]</mx:Metadata>
<mx:Script>
import org.granite.tide.Component;
import org.granite.tide.events.TideResultEvent;
[In]
public var helloService:Component;
private function hello():void {
helloService.hello(iname.text, helloResult);
}
private function helloResult(event:TideResultEvent):void {
lmessage.text = event.result as String;
}
</mx:Script>
<mx:TextInput id="iname"/>
<mx:Button label="Hello" click="hello()"/>
<mx:Label id="lmessage"/>
</mx:Panel>
This is a bit better, the main UI is now defined in its own MXML. Tide has not much to do here, but note that we have added the [Name]
metadata annotation on the MXML to instruct Tide that it has to manage the component and inject the dependencies on properties marked with
[In]
. This was not necessary in the initial case because the main application itself is always registered as a component managed by Tide
(under the name application
).
The next step is to decouple our UI component from the server interaction, so we can for example reuse the same UI component in another context or simplify the testing of the server interaction with a mock controller.
The first thing we can do is introduce a controller component (the C in MVC) that will handle this interaction and an interface so that we can easily switch the controller implementation. Then we can just bind it to the MXML component:
package com.myapp.controller { [Bindable] public interface IHelloController { function hello(name:String):void; function get message():String; } }
package com.myapp.controller { import org.granite.tide.Component; import org.granite.tide.events.TideResultEvent; [Name("helloController")] public class HelloController implements IHelloController { [In] public var helloService:Component; [Bindable] public var message:String; public function hello(name:String):void { helloService.hello(name, helloResult); } private function helloResult(event:TideResultEvent):void { message = event.result as String; } } }
We have to configure the controller in the main MXML and use it in the view:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*"
preinitialize="Tide.getInstance().initApplication()">
<mx:Script>
import org.granite.tide.Tide;
Tide.getInstance().addComponents([HelloController]);
</mx:Script>
<Hello id="hello"/>
</mx:Application>
<?xml version="1.0" encoding="utf-8"?>
<mx:Panel
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*">
<mx:Metadata>[Name]</mx:Metadata>
<mx:Script>
import com.myapp.controller.IHelloController;
[Bindable] [Inject]
public var helloController:IHelloController;
</mx:Script>
<mx:TextInput id="iname"/>
<mx:Button label="Hello" click="helloController.hello(iname.text)"/>
<mx:Label id="lmessage" text="{helloController.message}"/>
</mx:Panel>
This is already quite clean, and completely typesafe. The annotation [Inject]
indicates that Tide should inject any managed
component which class extends or implements the specified type, contrary to the annotation [In]
that is used to inject a
component by name. Here the instance of HelloController
will be injected, in a test case you could easily configure an
alternative TestHelloController
implementing the same interface.
This kind of architecture is inspired by JSF (Java Server Faces) and works fine. However there is still a bit of coupling between the views and the controllers, and it does not really follow the usual event-based style of the Flex framework. To obtain a more pure MVC model, we have to add a model component that will hold the state of the application, and an event class dispatched through the Tide event bus to decouple the view and the controller:
package com.myapp.events { import org.granite.tide.events.AbstractTideEvent; public class HelloEvent extends AbstractTideEvent { public var name:String; public function HelloEvent(name:String):void { super(); this.name = name; } } }
package com.myapp.model { [Bindable] public interface IHelloModel { function get message():String; function set message(message:String):void; } }
package com.myapp.model { [Name("helloModel")] public class HelloModel implements IHelloModel { [Bindable] public var message:String; } }
The controller will now observe our custom event, and set the value of the message property in the model:
package com.myapp.controller { import org.granite.tide.Component; import org.granite.tide.events.TideResultEvent; import com.myapp.events.HelloEvent; [Name("helloController")] public class HelloController implements IHelloController { [In] public var helloService:Component; [Inject] public var helloModel:IHelloModel; [Observer] public function hello(event:HelloEvent):void { helloService.hello(event.name, helloResult); } private function helloResult(event:TideResultEvent):void { helloModel.message = event.result as String; } } }
Lastly we configure the new model component and dispatch the custom event from the UI:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*"
preinitialize="Tide.getInstance().initApplication()">
<mx:Script>
import org.granite.tide.Tide;
Tide.getInstance().addComponents([HelloController, HelloModel]);
</mx:Script>
<Hello id="hello"/>
</mx:Application>
<?xml version="1.0" encoding="utf-8"?>
<mx:Panel
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*">
<mx:Metadata>[Name]</mx:Metadata>
<mx:Script>
import com.myapp.events.HelloEvent;
import com.myapp.model.IHelloModel;
[Bindable] [Inject]
public var helloModel:IHelloModel;
</mx:Script>
<mx:TextInput id="iname"/>
<mx:Button label="Hello" click="dispatchEvent(new HelloEvent(iname.text))"/>
<mx:Label id="lmessage" text="{helloModel.message}"/>
</mx:Panel>
The main difference here is that we use an event to communicate between the view and the controller. This would allow for example many controllers
to react to the same user action. The view does not know which component will handle the event, and the controllers simply specify that they are interested
in the event HelloEvent
with the annotation [Observer]
on a public handler method. Tide automatically wires
the dispatcher and the observers through its event bus by matching the event type.
Note that the HelloEvent
class extends a (pseudo) abstract class of the Tide framework. If you don't want any such dependency,
you can use any Flex event but then you have to add an annotation [ManagedEvent]
on the dispatcher to instruct Tide which events it
has to manage. See more below in the section Event Bus.
Now we have a completely decoupled and testable architecture, however everything is wired typesafely, meaning that any error will be detected at compile time and not at runtime.
With some Java server frameworks (Spring and CDI) we can even achieve complete client/server type safety by generating a typed client proxy. The controller would then look like:
package com.myapp.controller { import org.granite.tide.Component; import org.granite.tide.events.TideResultEvent; import com.myapp.events.HelloEvent; import com.myapp.service.HelloService; [Name("helloController")] public class HelloController implements IHelloController { [Inject] public var helloService:HelloService; [Inject] public var helloModel:IHelloModel; [Observer] public function hello(event:HelloEvent):void { helloService.hello(event.name, helloResult); } private function helloResult(event:TideResultEvent):void { helloModel.message = event.result as String; } } }
Hopefully you have now a relatively clear idea on what it's all about. The following sections will describe all this in more details.
<mx:Application ...
preinitialize="Tide.getInstance().initApplication()">
...
</mx:Application>
<mx:Application ...
preinitialize="Spring.getInstance().initApplication()">
...
</mx:Application>
The Tide framework makes heavy use of Flex annotations, so you will need to configure your build system (Flash Builder, Ant or Maven) correctly so the Flex compiler keeps the necessary annotations at runtime (see the Project Setup chapter for Ant, Maven and Flash Builder).
The core concepts of the Tide framework are the context and the component.
There are two main kinds of contexts:
The global context object can easily be retrieved from the Tide singleton:
var tideContext:Context = Tide.getInstance().getContext();
var tideContext:Context = Tide.getInstance().getContext("someConversationId");
Note however that this is not the recommended way of working with conversation contexts. See the Conversations section.
Components can be registered programmatically by any of the following methods:
Tide.getInstance().addComponent()
:
Tide.getInstance().addComponent("myComponent", MyComponent):This method takes two main arguments: the component name and the component class.
Tide.getInstance().addComponent(componentName, componentClass, inConversation, autoCreate, restrict);
inConversation
is a boolean
value indicating whether the component in conversation-scoped
(it is false
by default), autoCreate
is true
by default and indicates that the component will
be automatically instantiated by the container. Finally restrict
is related to security and indicates that the component instance
has to be destroyed when the user logs out from the application (so that its state cannot be accessed by unauthenticated users).
Tide.getInstance().addComponentWithFactory("myComponent", MyComponent, { property1: value1, property2: value2 });Of course, this assumes that the component class has accessible setters for the properties specified in the initialization map. Values may be string expressions of the form
#{component.property}
, and are then evaluated at run time as a chain of properties starting
from the specified contextuel component. All other values are assigned as is.
It is alternatively possible (and indeed recommended) to describe the metadata of the component with annotations in the component class. This simplifies the component registration and is often more readable.
Tide.getInstance().addComponents([MyComponent]); [Name("myComponent")] public class MyComponent { public MyComponent():void { } }
A component class must have a default constructor.
Once a component is registered, you can get an instance of the component from the Context
object by its name, for example
tideContext.myComponent
will return the unique instance of the component MyComponent
that we have defined before.
You can also retrieve the instance of a component that extend a particular type with tideContext.byType(MyComponent)
. Of course
it is more useful when specifying an interface so you can get its configured implementation: tideContext.byType(IMyComponent)
.
When many implementations of an interface are expected to exist in the context, you can use tideContext.allByType(IMyComponent)
to retrieve all of them.
If no component has been registered with a particular name, tideContext.someName
will by default return a client proxy for a remote
service named someName
. In particular tideContext.someName
will return null
only if a component
named someName
has been configured with the metadata autoCreate
set to false
.
When using dependency injection annotations ([In]
, [Out]
and [Inject]
) on component properties,
Tide implicitly registers a component of the target type when it is a concrete class (not an interface):
[Name("myInjectedComponent")] public class MyInjectedComponent { [In] public var myComponent:MyComponent; }
Will implicity register a component of class MyComponent
, even if you have never called Tide.addComponent()
for
this type.
Besides all these options for registering components, it is also possible to dynamically assign a component instance at any time in a Tide context with
tideContext.myComponent = new MyComponent()
. This allows you to precisely control the instantiation of the component and will
implicitly register the corresponding component from the object class. For example you can use this feature to switch at runtime between different
implementations of a component interface.
The last case is the one of UI components that are added and removed from the Flex stage. One of the things that Tide does at initialization time
in the method initApplication()
is registering listeners for the Flex events add
and remove
.
On the add
event, it automatically registers any component annotated with [Name]
and puts its instance in the context.
It also removes the instance from the context when getting the remove
event.
Note that this behaviour can incur a significant performance penalty due to the ridiculously slow implementation of reflection in ActionScript
so it can be disabled by Tide.getInstance().initApplication(false)
. You will then have to wire the UI components manually.
[Name("myInjectedComponent")] public class MyInjectedComponent { [In("myComponent")] public var myComponent:IMyComponent; }
[In] public var myComponent:IMyComponent;
You can also use property chain expressions of the form #{mySourceComponent.myProperty}
:
[In("#{mySourceComponent.myProperty}")] public var myComponent:IMyComponent;
[In(create="true")] public var myComponent:IMyComponent;
This ensures that myComponent
will never be null
.
[Name("myOutjectingComponent")] public class MyOutjectingComponent { [Bindable] [Out] public var myComponent:IMyComponent; public function doSomething():void { myComponent = new MyComponent(); } }
[Name("myOutjectingComponent")] public class MyOutjectingComponent { [Bindable] [Out(remote="true")] public var myEntity:MyEntity; public function doSomething():void { myEntity = new MyEntity(); } }
<?xml version="1.0" encoding="utf-8"?>
<mx:Panel
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*">
<mx:Metadata>[Name]</mx:Metadata>
<mx:Script>
import com.myapp.events.HelloEvent;
[Bindable] [In]
public var message:String;
</mx:Script>
<mx:TextInput id="iname"/>
<mx:Button label="Hello" click="dispatchEvent(new HelloEvent(iname.text))"/>
<mx:Label id="lmessage" text="{message}"/>
</mx:Panel>
package com.myapp.controller { import org.granite.tide.events.TideResultEvent; import com.myapp.events.HelloEvent; import com.myapp.service.HelloService; [Name("helloController")] public class HelloController implements IHelloController { [Inject] public var helloService:HelloService; [Bindable] [Out] public var message:String; [Observer] public function hello(event:HelloEvent):void { helloService.hello(event.name, helloResult); } private function helloResult(event:TideResultEvent):void { this.message = event.result as String; } } }
[Name("myInjectedComponent")] public class MyInjectedComponent { [Inject] public var bla:IMyComponent; }
[Inject] public var myContext:Context;
[Name("myComponent")] public class MyComponent { [PostConstruct] public function init():void { // ... } [Destroy] public function cleanup():void { /// ... } }
Let's see in a first step what kind of events can be managed:
org.granite.tide.events.TideUIEvent
are considered as untyped events and only their name is used to match
against observers.
org.granite.tide.events.TideUIEvent
(or TideUIEvent.TIDE_EVENT
), in particular all events
extending the AbstractTideEvent
class are considered as typed events and only their class is used to match against observers.
[ManagedEvent]
annotation on the dispatcher component are also matched by their type.
There are two ways of dispatching untyped events:
public function doSomething():void { dispatchEvent(new TideUIEvent("myEvent", arg1, { arg2: "value" })); }
TideUIEvent
takes a variable list of arguments that will be propagated to all observers.
public function doSomething():void { tideContext.raiseEvent"myEvent", arg1, { arg2: "value" }); }
public class MyEvent extends Event { public var data:Object; public function MyEvent(data:Object):void { super(TideUIEvent.TIDE_EVENT, true, true); this.data = data ; } }
You can also simply extend the existing AbstractTideEvent
class:
public class MyEvent extends AbstractTideEvent { public var data:Object; public function MyEvent(data:Object):void { super(); this.data = data ; } }
public class MyEvent extends Event { public var data:Object; public function MyEvent(data:Object):void { super("myEvent", true, true); this.data = data ; } }
[Name("myComponent")] [ManagedEvent(name="myEvent")] public class MyComponent extends EventDispatcher { public function doSomething():void { dispatchEvent(new MyEvent({ property: "value" })); } }
[Observer("myEvent")] public function eventHandler(event:TideContextEvent):void { // You can get the arguments from the events.params array var arg1:Object = event.params[0]; var arg2:Object = event.params[1]["arg2"]; ... // arg2 should be equal to "value" }
[Observer("myEvent")] public function eventHandler(arg1:Object, arg2:Object):void { // arg2["arg2"] should be equals to "value" }
One method can listen to more than one event type by specifying multiple [Observer]
annotations:
[Observer("myEvent")] [Observer("myOtherEvent")] public function eventHandler(arg1:Object, arg2:Object):void { // arg2["arg2"] should be equals to "value" }
Or by separating the event types with commas:
[Observer("myEvent, myOtherEvent")] public function eventHandler(arg1:Object, arg2:Object):void { // arg2["arg2"] should be equals to "value" }
Observers for typed events can have only one form:
[Observer] public function eventHandler(event:MyEvent):void { // Do something }
There are other possibilities than the annotation [Observer]
to register event observers:
Tide.getInstance().addEventObserver("myEvent", "myComponent", "myMethod")
can be used to register the method myMethod
of the component myComponent
as observer for the event myEvent
. This is exactly equivalent as putting the
annotation [Observer("myEvent")]
on the method.
Tide.getInstance().addContextEventListener("myEvent", listener)
can be used to directly register an event listener method for a
particular event. It can also be called from the context object with tideContext.addContextEventListener("myEvent", listener)
.
Now you should be able to easily connect all parts of your application through events.
event.context
.
myComponent
is the name of a global component,
tideContext.myComponent
will always return the instance of the global component for any existing context.
[In]
:
[Name("myConversationComponent", scope="conversation")] public class MyConversationComponent { [In] public var myComponent:MyComponent; // This will always inject the instance of the global component }
<mx:List id="list" dataProvider="{customerRecords}"
change="dispatchEvent(new TideUIConversationEvent(list.selectedItem.id, "viewRecord", list.selectedItem))")/>
[Name("customerRecordController")] public class CustomerRecordController { [Observer("viewRecord")] public function selectRecord(record:Record):void { // Start the conversation // For example create a view and display it somewhere } }
Here is a more complete example of end-to-end conversation handling by a controller:
[Name("customerRecordController", scope="conversation")] public class CustomerRecordController { [In] public var mainTabNavigator:TabNavigator; [In(create="true")] public var recordView:RecordView; [Observer("viewRecord")] public function viewRecord(record:Record):void { recordView.record = record; mainTabNavigator.addChild(recordView); } [Observer("closeRecord")] public function closeRecord(event:TideContextEvent):void { mainTabNavigator.removeChild(recordView); event.context.meta_end(); } }
<mx:Panel label="Record #{record.id}"> <mx:Metadata>[Name("recordView", scope="conversation")]</mx:Metadata> <mx:Script> [Bindable] public var record:Record; </mx:Script> <mx:Label text="{record.description}"/> <mx:Button label="Close" click="dispatchEvent(new TideUIEvent('closeRecord'))"/> </mx:Panel>
viewRecord
.
[In(create="true")]
), sets it
record
property and adds it to the main tab navigator. Note that we could have outjected the record from the controller and
injected it in the view but
If the user clicks on many elements in the list, one tab will be created for each element.
Each context having its own entity cache has some implications:
uid
) can exist once in each context.
IConversationEvent
)
or the other way (global observers of conversation events), Tide has to translate the event payload from one cache to the other. In the previous example,
the Record
received by the controller is NOT the same instance as the one dispatched from the list, it is the copy of this object
in the conversation context entity cache. That means that you can do whatever you want on this object, it will not be reflected on the source list.
tideContext.meta_mergeInGlobalContext()
or tideContext.meta_mergeInParentContext()
(for nested conversations).
Also when ending a conversation context, tideContext.meta_end(true)
will merge the changes in the parent context before ending the
conversation. tideContext.meta_end(false)
will drop any change made in the conversation context and is suitable for
Cancel buttons for example.
Tide provides a few extension points that can be used to extend its functionality.
First there are four events that are dispatched on some internal events:
org.granite.tide.startup
: Dispatched at application startup, can be used to do some global initialization.
org.granite.tide.contextCreate
: Dispatched at creation of a new conversation context, can be used to do initialization of the
context.
org.granite.tide.contextDestroy
: Dispatched at destruction of a conversation context, can be used to cleanup resources.
org.granite.tide.contextResult
: Dispatched at each remoting result.
org.granite.tide.contextFault
: Dispatched at each remoting fault, can be used to run global handling of faults.
org.granite.tide.login
: Dispatched at user login (or relogin when the user refreshes the browser page).
org.granite.tide.logout
: Dispatched before user logout.
org.granite.tide.loggedOut
: Dispatched after user logout.
All these events can be observed from any component as standard Tide events:
[Name("myComponent")] public class MyComponent { [Observe("org.granite.tide.startup")] public function startup():void { // Do some initialization stuff here... } }
org.granite.tide.plugin.addComponent
: Dispatched when a new component is registered, can be used to participate in the scan of
the annotations. event.params.descriptor
contains the internal component descriptor (of type ComponentDescriptor
),
see the API documentation for details on this class, and event.params.type
contains the Type
for the
component class that can be used to retrieve annotations or metadata.
org.granite.tide.plugin.setCredentials
: Dispatched when the user credentials are defined, it can be used to set the user credentials
on some object of the plugin. event.params
has two parameters username
and password
.
org.granite.tide.plugin.loginSuccess
: Dispatched when the user has been logged in successfully. event.params.sessionId
contains the user session id received from the server.
org.granite.tide.plugin.loginFault
: Dispatched when the user login has failed.
org.granite.tide.plugin.logout
: Dispatched when the user logs out.
public class TideTrace implements ITidePlugin { private static var _tideTrace:TideTrace; public static function getInstance():TideTrace { if (!_tideTrace) _tideTrace = new TideTrace(); return _tideTrace; } public function set tide(tide:Tide):void { tide.addEventListener(Tide.PLUGIN_ADD_COMPONENT, addComponent); } private function addComponent(event:TidePluginEvent):void { var descriptor:ComponentDescriptor = event.params.descriptor as ComponentDescriptor; var type:Type = event.params.type as Type; var anno:Annotation = type.getAnnotationNoCache('Trace'); if (anno != null) trace("Component added: " + descriptor.name); } }
Tide.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); ... } }
You can think of it as a XML configuration file, such as Seam components.xml
or Spring context.xml
.
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(); Tide.getInstance().addModule(loadedModule, _moduleAppDomain); }
<mx:ModuleLoader id="moduleLoader"
applicationDomain="{new ApplicationDomain(ApplicationDomain.currentDomain)}"
ready="Tide.getInstance().addModule(moduleLoader.child, moduleLoader.applicationDomain)"/>
You can then change the loaded module with this code :
private function changeModule(modulePath:String):void { if (moduleLoader.url != modulePath) { moduleLoader.applicationDomain = new ApplicationDomain(ApplicationDomain.currentDomain); moduleLoader.unloadModule(); moduleLoader.loadModule(modulePath); } }
Tide provides a way to integrate deep linking with the MVC framework. It uses a technique inspired by JAX-RS
so that changes in the
browser url will trigger a method on a component. It first requires to enable the corresponding Tide plugin just after the Tide initialization with :
Tide.getInstance().initApplication(); Tide.getInstance().addPlugin(TideUrlMapping.getInstance());
You will also need to keep the annotation [Path]
in your compilation options in Flash Builder / Ant / Maven.
Once enabled, the plugin will listen to browser url changes, and split the url after # in two parts. The part before the first slash will identify the
target controller, and the part after the first slash will determine the target method.
In the previous example, the controller has to be annotated with [Path("product")]
and the method with [Path("display/tv")]
:
[Name("productController")] [Path("product")] public class ProductController { [Path("display/tv")] public function displayTv():void { // Do something to display the TV... } }
Of course you won't want to have a different method for each kind of resource so you can use placeholders that will match method arguments :
[Name("productController")] [Path("product")] public class ProductController { [Path("display/{0}")] public function display(productType:String):void { // Do something to display the product... } }