Oct
17Simplified Cairngorm, Easy MVC for Adobe Flex
17
Posted: 17th October 2007
Tags: ActionScript, as3, easymvc, Flex, RIAs
Posted in Flex
Comments: 28 Comments »
Like many others, I have been struggling to fully get my head fully around the Cairngorm Micro Architecture, and even with the excellent Cairngorm Creator, I find it a little overkill for many Adobe Flex projects I am involved with, in fact, so does Steven Webster (one of the creators of Cairngorm).
So yesterday I sat in on an excellent Adobe eSeminar, presented by Tom Bray of SearcherCoders who presented an easy / simplified Model View Controller architecture based on Cairngorm.
To demonstrate the need for these frameworks, Tom started with an excellent example of an application based on a simple chat client. Inspired by his excellent Chatopica perhaps?:
- <?xml version=”1.0″ encoding=”utf-8″?>
- <mx:Application xmlns:mx=“http://www.adobe.com/2006/mxml“ layout=“vertical“>
- <mx:Script>
- <![CDATA[
- import mx.collections.ArrayCollection;
- [Bindable]
- public var rooms:ArrayCollection = new ArrayCollection( ["Flex", "Flash", "AIR", "ColdFusion" ] );
- [Bindable]
- public var users:ArrayCollection = new ArrayCollection( ["John", "Paul", "George", "Ringo" ] );
- [Bindable]
- public var currentRoom:String;
- [Bindable]
- public var messages:String = “”;
- private function joinRoom( room:String ):void
- \{
- currentRoom = room;
- views.selectedChild = chatPanel;
- \}
- private function sendMessage( message:String ):void
- \{
- messages += message + “\\n”;
- \}
- ]]>
- </mx:Script>
- <mx:ViewStack id=“views“ resizeToContent=“true“>
- <mx:Panel title=“Room List“ width=“300“ height=“400“>
- <mx:VBox width=“100%“ height=“100%“>
- <mx:List id=“roomList“ width=“100%“ height=“100%“
- borderSides=“top“ dataProvider=“\{rooms\}“/>
- <mx:Button width=“100%“ label=“Chat“ enabled=“\{roomList.selectedItem != null\}“
- click=“joinRoom(roomList.selectedItem as String)“/>
- </mx:VBox>
- </mx:Panel>
- <mx:Panel title=“Topic: \{currentRoom\}“ id=“chatPanel“ width=“500“ height=“400“>
- <mx:HBox width=“100%“ height=“100%“ paddingLeft=“10“ paddingBottom=“10“
- paddingRight=“10“ paddingTop=“10“>
- <mx:VBox width=“150“ height=“100%“>
- <mx:Label text=“User List“/>
- <mx:List width=“100%“ height=“100%“ dataProvider=“\{users\}“/>
- </mx:VBox>
- <mx:VBox width=“100%“ height=“100%“ >
- <mx:Label text=“Chat“/>
- <mx:TextArea id=“chatText“ text=“\{messages\}“
- editable=“false“ width=“100%“ height=“100%“ wordWrap=“true“/>
- <mx:HBox width=“100%“>
- <mx:TextInput id=“messageInput“ width=“100%“ minWidth=“0“/>
- <mx:Button label=“Send“ click=“sendMessage( messageInput.text )“/>
- </mx:HBox>
- </mx:VBox>
- </mx:HBox>
- </mx:Panel>
- </mx:ViewStack>
- </mx:Application>
The above example application is a classic example of how most of us start out developing in Flex, we end up putting all our properties and event handlers in the same file. This may be fine for a tiny application but if we want to scale it up we are going to run into problems.
For example, Flex offers us the ability to create a custom MXML component, that we can then reuse through our application, so as Tom showed, maybe we want to take the code that displays the Room list (above) and copy this into a separate mxml component . We can easily cut and paste the code and create a new MXML file RoomList.mxml with the following code:
- <?xml version=”1.0″ encoding=”utf-8″?>
- <mx:Panel title=“Room List“ xmlns:mx=“http://www.adobe.com/2006/mxml“ width=“300“ height=“400“>
- <mx:VBox width=“100%“ height=“100%“>
- <mx:List id=“roomList“ width=“100%“ height=“100%“
- borderSides=“top“ dataProvider=“{rooms}“/>
- <mx:Button width=“100%“ label=“Chat“ enabled=“{roomList.selectedItem != null}“
- click=“joinRoom(roomList.selectedItem as String)“/>
- </mx:VBox>
- </mx:Panel>
The problem is that we now have introduced two errors as the ArrayCollection ‘rooms’ is no longer in this mxml file then we cannot bind to it as the dataProvider for the roomList, also the event handler ‘joinRoom’ is no longer accessible. We could move these into this file, but then they couldn’t access the view stack and the problem goes on. So what we really need is to centralise our data into a central model and also centralise our even handlers into a controller.
Centralising Data into a Model
To centralise data, Tom took the Cairngorm’s modelLocator pattern, based around a singleton pattern . Where by the singleton ensures there is only ever one instance of itself, we define our Model as follows:
- package com.chatopica.chat.model
- {
- public class ChatModel
- {
- private static var instance:ChatModel;
- public function ChatModel()
- {
- if( instance != null )
- {
- throw( new Error( “there can be only one instance of ChatModel“ ) );
- }
- }
- public static function getInstance():ChatModel
- {
- if( instance == null )
- {
- instance = new ChatModel();
- }
- return instance;
- }
- }
- }
This is a typical singleton, implemented in ActionScript 3. As Tom pointed out in the eSeminar, ActionScript does not allow for private constructors, so here he is throwing an error if we try and instantiate the class and it is already instantiated. We can now access this Model anywhere in our application by simply typing
- ChatModel.getInstance()
So the next thing to do is move our ArrayCollection rooms into the Model so we end up with the following:
- package com.chatopica.chat.model
- {
- import mx.collections.ArrayCollection;
- public class ChatModel
- {
- private static var instance:ChatModel;
- [Bindable]
- public var rooms:ArrayCollection = new ArrayCollection( [ new Room("Flex"), new Room("Flash"), new Room("AIR"), new Room("ColdFusion") ] );
- public function ChatModel()
- {
- if( instance != null )
- {
- throw( new Error( “there can be only one instance of ChatModel“ ) );
- }
- }
- public static function getInstance():ChatModel
- {
- if( instance == null )
- {
- instance = new ChatModel();
- }
- return instance;
- }
- }
- }
Because our ArrayCollection is now in the Model that we can access from anywhere. We can change the mx:List ‘roomList’ in RoomList.mxml to use the rooms ArrayCollection in the Model as the dataProvider as follows:
- <mx:List id=“roomList“ width=“100%“ height=“100%“ borderSides=“top“ dataProvider=“{ChatModel.getInstance().rooms}“/>
We have now fixed the first issue but we still have the issue of the click handler for the button being inaccessible. We can solve this by centralising our events into a Controller
Centralising Events into a Controller
As with many event driven languages, ActionScript has a handy feature known as event bubbling. When an event is fired in a container, if the event has been set to bubble (by default events don’t bubble) it will also fire on it’s parent container, and in turn fire on the grandparent container all the way up to the main Application MXML class. By bubbling events, we can register event listeners on the System Manager and listen for any global application events that are set to bubble. So we can dispatch an event anywhere in our application using the following, and we can listen for it on the System Manager:
- dispatchEvent(new Event(‘myEvent‘, true))
Note: We have set the second parameter (bubbles) on the Event constructor to true, which tells the event dispatcher to bubble this event.
As we are centralising our events we may need to send related information along with the event, for use by the event handler. For example, as the event handler is no longer able to directly query the roomList, to see which item has been selected (i.e. roomList.selectedItem), we need to send this information with the event. We can easily do this by creating a custom event that extends the Event class as follows:
- package com.chatopica.chat.events
- {
- import com.chatopica.chat.model.Room;
- import flash.events.Event;
- public class JoinRoomEvent extends Event
- {
- public static const JOIN:String = “joinRoom“;
- public var room:Room;
- public function JoinRoomEvent( room:Room )
- {
- super( JOIN, true );
- this.room = room;
- }
- }
- }
Above we have simply extended the event using inheritance and have added a public property called room (of type Room, Tom has created a Room class that holds the name as well as a collection of Users). We have also declared the event name/type as a constant JOIN = “joinRoom”. This allows us to reference this event anywhere. In our constructor, we call the constructor on the parent class (Event) setting the event name/type to “joinRoom” and stating that it should bubble (like this we don’t have to specify that it bubbles when we create a new JoinRoomEvent). We then set the room property from the parameter passed to the constructor.
So now we can fire a JoinRoomEvent anywhere in our application, passing in a room object. The event handler listening for this event will have access to this room object. So we can fix the last error in RoomList.mxml as follows:
- <mx:Button width=“100%“ label=“Chat“ enabled=“{roomList.selectedItem != null}“
- click=“dispatchEvent( new JoinRoomEvent( roomList.selectedItem as Room ) )“/>
So far we have created a custom event and we have discussed where to look to find bubbling events but we have not looked at how to do this.
The Controller
As I mentioned before, we can listen to all events that have been set to bubble on the systemManager and as UIComponent (anyone who has created a custom component will know this one) has a reference to systemManager within it, we can extend this class. The other benefit of extending UIComponent for our class that will be our central event handler or Controller is that we can then use it in MXML.
We extend the UIComponent as follows:
- package com.chatopica.chat.controller
- {
- import com.chatopica.chat.events.JoinRoomEvent;
- import com.chatopica.chat.events.SendMessageEvent;
- import com.chatopica.chat.model.ChatModel;
- import flash.events.Event;
- import mx.core.UIComponent;
- import mx.events.FlexEvent;
- public class ChatController extends UIComponent
- {
- public function ChatController()
- {
- addEventListener( FlexEvent.CREATION_COMPLETE, setupEventListeners );
- }
- private function setupEventListeners( event:Event ):void
- {
- systemManager.addEventListener( JoinRoomEvent.JOIN, handle_joinRoom );
- }
- private function handle_joinRoom( event:JoinRoomEvent ):void
- {
- ChatModel.getInstance().currentActivity = ChatModel.VIEWING_CHAT;
- }
- }
- }
Note:: Above we have not directly added an event listener to the systemManager in the ChatController constructor. In fact we can’t to do this, because at the time the class is instantiated the systemManager is not initialised and is Null. We would therefore be adding an event listener to nothing. We get around this by adding an event listener for the global FlexEvent.CREATION_COMPLETE event, and at that point we can then add our event handler(s) to the systemManager.
Controlling the View Stack
The final thing we have left to achieve is the ability to change the View Stack from the controller, we first need to store the currentActivity or state in the Model as follows:
- public static const VIEWING_CHAT:String = “viewingChat“;
- public static const VIEWING_ROOMS:String = “viewingRooms“;
- [Bindable]
- public var currentActivity:String;
In addition to the currentActivity property above, we have also added a constant value for each state. Unfortunately it is not as simple as binding this property to the mx:ViewStack, but that does not mean it isn’t easy. All we have to use is the ChangeWatcher class that monitors a property and fires and event when it changes. We do this in the file MainView.mxml that holds out viewstack as follows:
- <?xml version=”1.0″ encoding=”utf-8″?>
- <mx:ViewStack xmlns:mx=“http://www.adobe.com/2006/mxml“ resizeToContent=“true“ xmlns:views=“com.chatopica.chat.views.“>
- <mx:Script>
- <![CDATA[
- import mx.events.PropertyChangeEvent;
- import mx.binding.utils.ChangeWatcher;
- import com.chatopica.chat.model.ChatModel;
- private var activityWatcher:ChangeWatcher = ChangeWatcher.watch( ChatModel.getInstance(), "currentActivity", handle_activityChange );
- private function handle_activityChange( event:PropertyChangeEvent ):void
- {
- if( event.newValue == ChatModel.VIEWING_CHAT )
- {
- selectedChild = chatPanel;
- }
- }
- ]]>
- </mx:Script>
- <views:RoomList/>
- </mx:ViewStack>
Now, when the value of currentActivity in the ChatModel changes, Flex will dispatch a PropertyChangedEvent. We can then listen for this event and change the view, accessing what the new value of currentActivity is in the newValue property of the PropertyChangeEvent (shown above as event.newValue).
Refactor the rest of your application…
Having done this for the RoomList component, we can now repeat the process for the code that implements the chat window. Finally we are simply left with the following in our mx:Application tags:
- <?xml version=”1.0″ encoding=”utf-8″?>
- <mx:Application xmlns:mx=“http://www.adobe.com/2006/mxml“ layout=“vertical“ xmlns:views=“com.chatopica.chat.views.“ xmlns:controller=“com.chatopica.chat.controller.*“>
- <controller:ChatController/>
- <views:MainView/>
- </mx:Application>
This process now makes our code cleaner, loosely coupled and more DRY (Don’t Repeat Yourself), easy to scale and we can reuse these components without the risk of breaking our system.
Thanks again to Tom for the excellent eSeminar which has really clarified my understanding for the necessities of using an architecture like this.
Update 23/01/08: I have added an article on scaling up EasyMVC, you can read it by clicking here.
For more information:
Comments below..



Great summary of Tom Bray’s EasyMVC presentation. I also attended the e-seminar, but missed some of the details because of my lack of experience with Flex. Your article covered all of my questions. Thanks.