SimpleSampleApplicationExplained

### Cairngorm 3 - [ Home ][1] - [ Guidelines ][3] - [ Tools ][4] - [ Libraries Downloads ][2]
### Cairngorm 2 - [ Home ][5] - [ Framework Downloads ][6] - [ Eclipse Plugin ][7]
### Project - [ Source ][8] - [ Bug Database ][9] - [ Submitting a Patch ][10] - [ Developer Documentation ][11] - [ Forums ][12] - [ License ][13]

A Simple Sample Application Explained

Introduction

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.

The Package Structure

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:

  • presentation (concerns visual appearance and presentation behaviour)
  • application (concerns coordination of presentation and infrastructure components)
  • domain (soley focused on the problem domain)
  • infrastructure (concerns i.e. remote services or UI frameworks)

These packages showcase our recommendation for a layered architecture.
Let's dive into these architectural layers starting with the presentation layer.

Look, No Script-block Functions

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:

<fx:Script>
    <![CDATA[
        [Inject]
        [Bindable]
        public var model:ToolbarPM;
    ]]>
</fx:Script>

<s:Button styleName="contactsAddButton"
          toolTip="Add Contact"
          click="model.addContact()"/>

<mx:Spacer width="100%"/>

<s:Label text="Search:"/>

<s: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.

Enter the Presentation Model

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:

<fx:Script>
    <![CDATA[
        import insync.domain.Contact;

        [Inject]
        [Bindable]
        public var model:ContactsListPM;
    ]]>
</fx: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.

Coordinating the Components

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));
    }
}
Encapsulate Operations in Commands

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 . For more information about IDataCache, click here.

Sequencing Operations

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.

Keep Your Objects Small

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.

References

Martin, R. Single Responsibility Principle. http://www.objectmentor.com/resources/articles/srp.pdf of OOD principles .


Related

Wiki: _Checklist

Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.