This article takes a whistle-stop tour of a simple Cairngorm application to demonstrate the architecture in action.
The example shown was part of the presentation "Using Flex Frameworks to build Data Driven Applications" at the MAX 2009 conference. The client side source of the example is now maintained and extended as part of Cairngorm. For more information about this presentation view Christophe Coenraets blog .
This example uses the Parsley Application framework , however this tutorial doesn't go into the Parsley feature set and instead follows on guidelines that Cairngorm specifies on top of that. Therefore, the conceptual practices used here are applicable to a variety of application frameworks available for the Flash platform today.
Packages are an important way to organize code. A consistent package structure makes it easier to navigate the code base, and easier to understand the dependencies between different parts. The InsyncBasic application only shows how Cairngorm separates code by architectural layer. Behind the insync package, you'll find the following packages, describing architectural layers:
These packages showcase our recommendation for a layered architecture.
Let's dive into these architectural layers starting with the presentation layer.
To begin with, open up the main application file, InsyncBasic.mxml , and navigate down through the view hierarchy. The first thing you'll notice is an absence of Script-block functions in the MXML view components. This keeps the views focussed on sizing and layout concerns, making them easier to read and change as the visual design evolves. Here's the Toolbar component:
<mx:Script>
<![CDATA[
[Inject]
[Bindable]
public var model:ToolbarPM;
]]>
</mx:Script>
<mx:Button
styleName=
"contactsAddButton"
toolTip=
"Add Contact"
click=
"model.addContact()"
/>
<mx:Spacer width=
"100%"
/>
<mx:Label text=
"Search:"
/>
<mx:TextInput id=
"searchBox"
change=
"model.search(searchBox.text)"
/>
Notice the Inject tag. This is part of the Parsley Application framework to inject the ToolbarPM into the Toolbar view.
The variables and functions that might have been in the Script-block are instead extracted into presentation models. Each significant MXML view component has its own presentation model (PM). The PM is focussed on the state and behaviour required to present data to the user and process user input. It knows nothing about any other presentation model and it doesn't have knowledge of the application around it.
Here's the ToolbarPM , which trims white-spaces from user input before other client side components receive a search event.
public class ToolbarPM extends EventDispatcher
{
[MessageDispatcher]
public
var
dispatcher:Function;
public
function
addContact():
void
{
dispatcher(ContactEvent.newAddContactEvent());
}
public
function
search(keywords:
String
):
void
{
if
(keywords ==
null
)
return
;
keywords=StringUtil.trim(keywords);
dispatcher(
new
SearchEvent(keywords));
}
}
A PM should not reference a view component directly; instead the view observes the PM. The wiring between a view component and its corresponding PM takes place through binding expressions and in-line event handlers. Here is the ContactsList view component:
<mx:Script>
<![CDATA[
import insync.domain.Contact;
[Inject]
[Bindable]
public var model:ContactsListPM;
]]>
</mx:Script>
<mx:DataGrid id=
"list"
width=
"100%"
height=
"100%"
dataProvider=
"{ model.contacts.items }"
doubleClickEnabled=
"true"
itemDoubleClick=
"model.editContact(Contact(list.selectedItem))"
>
<mx:columns>
<mx:DataGridColumn dataField=
"firstName"
headerText=
"First Name"
/>
<mx:DataGridColumn dataField=
"lastName"
headerText=
"Last Name"
/>
<mx:DataGridColumn dataField=
"phone"
headerText=
"Phone"
/>
</mx:columns>
</mx:DataGrid>
With this approach there is little room for logical errors in the view, and the real logic instead exists in the PM, where it can easily be unit tested. Unit testing view components directly is more difficult due to the asynchronous component life-cycle and less dependencies with other components such as other view controls. A PM also simplifies a component with driving out the used API of a view component from the larger UIComponent API available in MXML.
The different components of an application need to be coordinated to provide useful features to the user. When something is typed into the search box, the results need to appear in the contacts list below. This behaviour is separated from presentation concerns, so the layout can be changed independently.
In the InsyncBasic application, the search operation is invoked by dispatching a SearchEvent from the ToolbarPM :
public
function
search(keywords:
String
):
void
{
if
(keywords ==
null
)
return
;
keywords = StringUtil.trim(keywords);
if
(keywords.
length
> 0)
{
dispatcher(
new
SearchEvent(keywords));
}
}
The SearchEvent is handled by the application layer, a Command invokes the RPC operation with invoking a remote service and adding the successful result into a domain object of the domain layer.
public class SearchContactsCommand
{
[Inject]
public
var
contacts:Contacts;
[Inject]
public
var
cache:IDataCache;
[Inject]
public
var
service:RemoteObject;
public
function
execute(event:SearchEvent):AsyncToken
{
return
service.getContactsByName(event.keywords) as AsyncToken;
}
public
function
result(items:IList):
void
{
contacts.addContacts(cache.synchronize(items));
}
}
The Command follows a convention from Parsley's DynamicCommand feature. The request of the SearchEvent is handled by the method named "execute", while the result is handled by the method named "result". If this example would need to handle a error response additionally, then another method could have been specified named "error". This convention is enfored by declaring a DynamicCommand inside a Parsley context.
<DynamicCommand type=
"{ SearchContactsCommand }"
/>
Also note the injected IDataCache utility, which is part of the [ Cairngorm Integration library ][10] . For more information about IDataCache, click here .
The Insync application refreshes the search view with the latest data once a new contact has been saved. This is easy to do using the data synchronization feature of LiveCycle Data Services, however using only RPC operations as the Insync application, the client side needs to manually invoke a search operation after the save operation has returned successfully. This simple sequencing can be achieved with a dedicated object in the application layer, the RefreshSearchAfterSaveController .
public class RefreshSearchAfterSaveController
{
[MessageDispatcher]
public
var
dispatcher:Function;
private
var
lastSearch:
String
="";
[MessageHandler(selector="search")]
public
function
onSearch(event:SearchEvent):
void
{
lastSearch=event.keywords;
}
[CommandResult(selector="save")]
public
function
onSaveComplete():
void
{
dispatcher(
new
SearchEvent(lastSearch));
}
}
For more evolved requirements on sequencing that are often useful for i.e. application start-ups or sequencing of domain behaviour, check out the Task library .
Note the small objects of the previous samples. The RefreshSearchAfterSaveController from above or the Contacts domain object below only really have one responsiblity. All their methods reference all instance properties and therefore achieve a high functional cohesion. This keeps your code simple, focused and more resilient to change. Check out the Single Responsibility Principle for more information.
[
Event
(name="itemsChange", type="flash.events.
Event
")]
public class Contacts extends EventDispatcher
{
public static const ITEMS_CHANGE:
String
= "itemsChange";
private
var
_items:IList =
new
ArrayCollection();
[Bindable("itemsChange")]
public
function
get items():IList
{
return
_items;
}
public
function
addContacts(items:IList):
void
{
_items = items;
dispatchEvent(
new
Event
(ITEMS_CHANGE));
}
public
function
addContact(contact:Contact):
int
{
var
index:
int
= -1;
if
(items.getItemIndex(contact) == -1)
{
items.addItem(contact);
index = items.
length
- 1;
}
return
index;
}
public
function
addItemAt(contact:Contact, index:
int
):
void
{
items.addItemAt(contact, index);
}
public
function
removeContact(contact:Contact):
int
{
var
index:
int
= items.getItemIndex(contact);
if
(index != -1)
{
items.removeItemAt(index);
}
return
index;
}
public
function
removeContactAt(index:
int
):Contact
{
return
Contact(items.removeItemAt(index));
}
}
This concludes the basic tour through InsyncBasic. InsyncBasic only shows one functional area, dealing with contacts. The InsyncModularExtended sample application and tutorial focuses on how Cairngorm recommends to deal with additional functional areas such as messaging and expenses and is therefore a logical next step to learn more about Cairngorm.
Martin, R. Single Responsibility Principle. http://www.objectmentor.com/resources/articles/srp.pdf of OOD principles .
[10]: Cairngorm Libraries#CairngormLibraries-Integration