Architecting a Flex App

Web developers who take Flex for a spin may initially be confused by the user interface model.  Although a typical, servlet-style, request-response model will work in Flex, There Is A Better Way.  Thanks to the "[Binding]" tag in the Actionscript language, you can bind your view to your model data so that changes to the model are automatically reflected in the view.  The Cairngorm microarchitecture formalizes this approach, and is a great starting point for developers who want to figure out how to "make it all work together".  In this post I’ll describe how variable binding, feature-driven development, and Cairngorm all work together for NoteTag.

Here’s how a typical, non-trivial Flex app might be architected:

Domain

  • The classes that make up the domain model.  In NoteTag, this includes Notes, Tasks, and Subscriptions.  (A Subscription is a collection of related Notes or Tasks.)

Model

  • A Singleton that holds bindable instances of the domain model.  In NoteTag, the ModelLocator Singleton holds the user’s list of Subscriptions, the user’s Connections, the current Subscription, the current Note, etc.

View

  • The UI components (generally, though not always, MXML files).  The UI components that are state-dependent are bound to instance variables in the ModelLocator.  Any changes to data in the ModelLocator will cause the UI to automatically update, provided that that data is marked as "[Bindable]".  An example in NoteTag is the NoteListView, which presents the list of Notes in the current Subscription.  If the current Subscription or any of its Notes change then the NoteListView will automatically update to reflect the changes.

Controller

  • Infrastructure for implementing features as event-driven Commands.  Examples in NoteTag include GetSubscriptionCommand, GetNoteCommand, etc.

Business

  • Business logic classes perform operations on Domain objects, often making calls to remote services and returning the results asynchronously.  The SubscriptionManager class is the entry point for most of NoteTag’s business logic.

Service

  • The services layer, which holds all classes that are used to make remote service (HTTPService, RemoteService, and WebService) calls.  NoteTag uses a set of service factory classes, which decouple the configuration of specific HTTP services from the components that make calls to HTTP services.

Most application features touch on some or all of these components.  Here’s the workflow for a typical feature:

  1. The View broadcasts an event.
  2. The singleton Controller receives the event, maps it to its corresponding Command, and executes the Command.
  3. The Command delegates to the appropriate Business object to perform the business logic.
  4. The Business object performs the business logic, possibly making one or more asynchronous calls to various Services, and returns the result by dispatching a new event to the Command.
  5. The Command assigns the result to the singleton Model.
  6. Any Views that are bound to the data in the singleton Model are automatically updated.

So how would this work for a specific feature?  When the user selects a Note from the list of Notes (at the top of the screen, below), the Note is loaded from the appropriate repository (Blogger or TypePad) and displayed in the editor (at the bottom of the screen, below):

 

1. Broadcasting the Event

When the user clicks on the first Note in the list, NoteListView dispatches an event, as follows:

// NoteListView.mxml
private function getSelectedEntry(event:ListEvent) : void
{
  var selectedEntry:TagBasedEntry =
    TagBasedEntry(currentFeed.entries[event.rowIndex-1]);

  Application.application.dispatchEvent(
    new GetNoteEvent(selectedEntry.metadata,true));
}

2. Responding to the Event

Because NoteTag’s Front Controller has registered itself to listen for all Command Events, it will be notified when GetNoteEvent is dispatched:
// NoteTagController.as
public class NoteTagController extends FrontController
{
  public function NoteTagController()
  {
    addCommand(LoginEvent.EVENT_LOGIN, LoginCommand);
    addCommand(GetNoteEvent.EVENT_GET_NOTE, GetNoteCommand);
    addCommand(GetTaskEvent.EVENT_GET_TASK, GetTaskCommand);
    addCommand(PostNoteEvent.EVENT_POST_NOTE, PostNoteCommand);
    // more commands...
  }
}

Cairngorm’s FrontController provides the infrastructure for listening for events and responding to them by executing the appropriate command.

3. Executing the Command

To retrieve the Note, NoteTag needs to make a call to the blog server that stores the user’s notes.  The GetNoteCommand, which implements Cairngorm’s Command interface, takes care of this:

// GetNoteCommand.as
internal class GetNoteCommand extends ChainedCommand
{
 
public override function execute(event:CairngormEvent):void
 
{
    var initialEvent:GetNoteEvent = GetNoteEvent(event);

   
var subscriptionManager:SubscriptionManager =
     
ModelLocator.getInstance().subscriptionManager;

   
setNextEventHandler(subscriptionManager,
     
handleLoadNote,
     
LoadNoteEvent.EVENT_LOAD_NOTE,
     
onSubscriptionFault,
     
SubscriptionFaultEvent.EVENT_SUBSCRIPTION_FAULT);

    subscriptionManager.loadNote(initialEvent.metadata);
  }

  private function handleLoadNote(event:LoadNoteEvent):void
  {
    // handle result here...
  }

  // ...
}

(You may have noticed that GetNoteCommand extends ChainedCommand — this class chains asynchronous calls together using the setNextEventHandler method.  In a future post, I’ll go into greater detail on ChainedCommand, and asynchronous responders in general.)

4. Performing the Business Logic

The SubscriptionManager handles the loading of the Note by executing a series of HTTP service calls to the tag server and the blog server.  When the note has been loaded, the SubscriptionManager will dispatch a LoadNoteEvent, which will be handled by GetNoteCommand.handleLoadNote (see the next item).

5. Updating the Model

GetNoteCommand responds to the event by assigning the loaded Note to the appropriate data member on the ModelLocator:

// GetNoteCommand.as
internal class GetNoteCommand extends ChainedCommand
{
 
// ...

  private function handleLoadNote(event:LoadNoteEvent):void
  {
    ModelLocator.getInstance().currentNote = event.note;
  }

  // ...
}

6. Updating the View

Any views that are bound to the ModelLocator’s currentNote member will automatically update themselves to reflect the new data.  NoteView, the component that’s responsible for displaying the Note in an editor, is one such view:

// NoteView.mxml
<mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml"
   
xmlns:view="com.adobe.kiwi.notetag.view.*"
 
 
xmlns="*">
 
<mx:Script>
  <![CDATA[
    import com.adobe.kiwi.notetag.model.*;

    [Bindable] private var model:ModelLocator = ModelLocator.getInstance();
  ]]>
  </mx:Script>

  <view:NoteEdit id="noteEditor" width="100%" height="100%"
   
note="{model.currentNote}" />

</mx:VBox>

 

Every other feature — publishing a Note, fetching a Subscription, updating a Task — is implemented with the same Event-to-Command-to-Model-to-View approach.  Command-specific Events can be dispatched from multiple contexts, without knowing which Views will be affected.  Views can bind to Model changes without knowing where the originating Event was dispatched from.  Loose coupling leads to cleaner, more maintainable code.