Building a Flash site using PureMVC
At a recent FlashCodersNY meeting where the discussion centered around the subject of the MVC (Model-View-Controller) design pattern, it struck me that a lot of Flash developers, especially those coming from design backgrounds, did not see the value in using the MVC design pattern.
Well, I’m not here to make the case for using MVC but if you have spent enough time struggling with structuring an application so that it’s clearly represented by a set of well-defined relationships and responsibilities, you would’ve likely tried to implement some kind of MVC pattern. Now, some design pattern is certainly better none. Trouble is, every developer has his or her own approach to MVC. When a team of developers are working on the same project, things can get confusing pretty quickly if there are no common standards to adhere to.
This brings us to the PureMVC framework. Created by Cliff Hall, this framework provides a set of well-defined protocols for implementing the MVC design pattern with your application, rendering the grunt work of manually hooking up an MCV structure a thing of the past. However, great as PureMVC may be, it does not present an easy learning curve for some, especially those who are just getting started with OOP and design patterns.
In this tutorial, I will show you how to build a Flash site using PureMVC. If you’re familiar with the Flash IDE and have some experience with OOP and writing classes, you’re pretty much ready to go.
1. Download Source Code
Download the source files for this tutorial here.
2. Creating the Flash assets
Open fla/PureMVCSite.fla in the Flash IDE. In the Library Panel, you’ll see that I have created various assets for our Flash site:
- A
sitemovieclip that serves as a container clip with the UI elements laid out in the desired fashion - A
headermovieclip containing a dynamic textfield for the site header - A
navmovieclip containing 3navButtoninstances - A
bodymovieclip containing a dynamic multiline textfield for the body content
The site movieclip symbol is linked to the class Site. The nav movieclip symbol is linked to the class MainNav. Both these classes belong to the package com.hubflanger.puremvc.view.component. In the PureMVC world, these are known as “view components”, not to be confused with the built-in Flash components. These components communicate with the PureMVC framework via their associated Mediators, allowing them to be “loosely-coupled”, thereby granting you great flexibility in changing their behavior without impacting the rest of your application.
In the ActionScript 3.0 Setting of the Publish Settings, you’ll see that Classpath has been set to point to “../as“. This tells the Flash compiler that the src/as folder is where you’ll look for the class files for this application.
3. Examining the PureMVC package
The PureMVC package org.puremvc.as3 (version 2.0.3) is located at src/as/org/puremvc/as3 and has been included for your convenience. The AS3 port of the PureMVC framework is hosted here. I strongly encourage you to spend some time reading the online documentation if possible.
At any time during this tutorial, if you feel the need for a clearer understanding of PureMVC terminologies such as Mediator, Proxy or Notification, please feel free to refer to these classes. Cliff Hall has done an excellent job of commenting them and the comments will provide a clear picture of each class’ role in the framework.
4. Examining PureMVCSite.as and ApplicationFacade.as
The main timeline of PureMVCSite.fla is linked to the document class PureMVCSite which is initialized when the application starts. PureMVCSite.as resides at the root of the as folder. When the application launches, PureMVCSite creates a Singleton instance of ApplicationFacade. The ApplicationFacade is the entry point to the PureMVC framework. When you instantiate ApplicationFacade, a whole slew of events take place behind the scenes to wire up your application with the PureMVC framework.
package
{
import com.hubflanger.puremvcsite.ApplicationFacade;
import flash.display.Sprite;
public class PureMVCSite extends Sprite
{
private var facade:ApplicationFacade;
public function PureMVCSite()
{
facade = ApplicationFacade.getInstance();
facade.startup( this.stage );
}
}
}
Let’s first examine the static constants defined at the beginning of this class. STARTUP, INITIALIZE_SITE and SECTION_CHANGED represent the Notification names of the events that our application will be responding to. A Notification is the default messaging deployed by PureMVC to inform the framework of an event that has been dispatched. In PureMVC land, sending a Notification is synonymous with broadcasting an event. Only difference is, it does a little more than just broadcasting it to everyone, whether they want to hear it or not. PureMVC finds the select audience who are “ticket holders” to an event, and drives them to the venue. I will explain this in greater detail later.
package com.hubflanger.puremvcsite
{
import org.puremvc.as3.interfaces.IFacade;
import org.puremvc.as3.patterns.facade.Facade;
import com.hubflanger.puremvcsite.controller.StartupCommand;
public class ApplicationFacade extends Facade implements IFacade
{
public static const STARTUP:String = "startup";
public static const INITIALIZE_SITE:String = "initializeSite";
public static const SECTION_CHANGED:String = "sectionChanged";
public static function getInstance() : ApplicationFacade
{
if ( instance == null ) instance = new ApplicationFacade();
return instance as ApplicationFacade;
}
override protected function initializeController() : void
{
super.initializeController();
registerCommand( STARTUP, StartupCommand );
}
public function startup( stage:Object ):void
{
sendNotification( STARTUP, stage );
}
}
}
ApplicationFacade overrides initializeController() to register StartupCommand with the STARTUP Notification. Behind the scenes, the Controller adds StartupCommand to its commandMap array and notes that StartupCommand is interested in listening for the STARTUP notification event.
The startup() method in ApplicationFacade is then explicitly called by PureMVCSite, passing in a reference to the Stage. This creates a Notification object with the name “startUp” and a reference to the Stage assigned to the its body property.
5. Examining StartupCommand.as
Upon receiving the Notification, the Controller iterates through its commandMap and retrieves StartupCommand as an object that is interested in the STARTUP notification. This results in the execute() method in StartupCommand being called.
package com.hubflanger.puremvcsite.controller
{
import flash.display.Stage;
import org.puremvc.as3.interfaces.ICommand;
import org.puremvc.as3.interfaces.INotification;
import org.puremvc.as3.patterns.command.SimpleCommand;
import com.hubflanger.puremvcsite.ApplicationFacade;
import com.hubflanger.puremvcsite.view.StageMediator;
import com.hubflanger.puremvcsite.model.SiteDataProxy;
public class StartupCommand extends SimpleCommand implements ICommand
{
override public function execute( note:INotification ) : void
{
var stage:Stage = note.getBody() as Stage;
facade.registerMediator( new StageMediator( stage ) );
facade.registerProxy( new SiteDataProxy() );
}
}
}
The execute() method in StartupCommand retrieves the reference to the Stage from the Notification and passes that along to an instance of the StageMediator being created. It also creates an instance of SiteDataProxy.
facade is a built-in property of the Mediator and Proxy base classes from which StageMediator and SiteDataProxy extends respectively. It refers to the ApplicationFacade instance which extends the Facade base class.
facade.registerMediator() registers the newly created Mediator instance with the View which stores it in its mediatorMap. The Mediator instance can be retrieved during runtime via a simple reference of its static NAME property using facade.retrieveMediator().
Similarly, facade.registerProxy() registers the newly created Proxy instance with the Model which stores it in its proxyMap. The Proxy instance can also be retrieved by passing its NAME property to the facade.retrieveProxy() method.
At this point, you’ll probably start to notice a pattern here. The Model, View and Controller all have methods and properties that mirror each other, tying the Proxy to the Model, the Mediator to the View and the Command to the Controller. You’ll also notice that the Facade indeed provides a “shortcut” to accessing various parts of your application within the PureMVC framework. Nobody talks to the Model, View or Controller directly. Everybody goes through the middle man named Facade.
6. Examining StageMediator.as
The StageMediator facilitates the communication between the Stage and the PureMVC framework. The Stage instance is referenced via the viewComponent property inherited from the Mediator base class. In PureMVC convention, it is also commonplace to create an accessor method such as “get stage()“. Two very important methods of the Mediator instance are the listNotificationInterests() method and the handleNotification() method, which every Mediator subclass must override.
listNotificationInterests() returns an array of Notification names as defined in ApplicationFacade, representing events that this particular Mediator is interested in. When the View runs through its list of Observers, it checks each Mediator against its Notification interests. If there is a match pertaining to a specific Notification, the handleNotification() method of that Mediator is called. This is what I was referring to earlier by the select audience who are “ticket holders” to an event. In this case, our StageMediator is interested in the INITIALIZE_SITE Notification.
The Mediator base class extends Notifier which means that in addition to responding to Notifications, it is also capable of sending out Notifications via the sendNotification() method. In other words, it can dispatch events. Although, unlike a MovieClip, it can’t dispatch any Flash Events, instead, it dispatches Notification objects.
package com.hubflanger.puremvcsite.view
{
import com.hubflanger.puremvcsite.ApplicationFacade;
import com.hubflanger.puremvcsite.view.component.Site;
import flash.display.Stage;
import flash.events.MouseEvent;
import org.puremvc.as3.interfaces.*;
import org.puremvc.as3.patterns.mediator.Mediator;
public class StageMediator extends Mediator implements IMediator
{
public static const NAME:String = "StageMediator";
public function StageMediator( viewComponent:Object )
{
super( NAME, viewComponent );
}
override public function listNotificationInterests():Array
{
return [
ApplicationFacade.INITIALIZE_SITE
];
}
override public function handleNotification( note:INotification ):void
{
switch ( note.getName() )
{
case ApplicationFacade.INITIALIZE_SITE:
initializeSite();
break;
}
}
private function initializeSite():void
{
var site:Site = new Site();
facade.registerMediator( new SiteMediator( site ) );
facade.registerMediator( new NavMediator( site.nav ) );
stage.addChild( site );
var navMediator:NavMediator = facade.retrieveMediator( NavMediator.NAME ) as NavMediator;
sendNotification( ApplicationFacade.SECTION_CHANGED, navMediator.currentSection );
}
protected function get stage():Stage
{
return viewComponent as Stage;
}
}
}
7. Examining SiteDataProxy.as
SiteDataProxy represents the data model for the application. It loads in dynamic data via xml and then parses and stores that information in a built-in property named “data“. Like the Mediator, the Proxy object also extends Notifier which makes it capable of sending out Notifications via the sendNotification() method. Unlike the Mediator, the Proxy does not have Notification interests and one can and should only update the Proxy via a Command.
package com.hubflanger.puremvcsite.model
{
import com.hubflanger.puremvcsite.ApplicationFacade;
import flash.events.Event;
import flash.net.URLLoader;
import flash.net.URLRequest;
import org.puremvc.as3.interfaces.IProxy;
import org.puremvc.as3.patterns.proxy.Proxy;
public class SiteDataProxy extends Proxy implements IProxy
{
public static const NAME:String = "SpriteDataProxy";
public var navIDs:Array;
public function SiteDataProxy( )
{
super( NAME, new Object() );
var loader:URLLoader = new URLLoader();
loader.addEventListener( Event.COMPLETE, onDataLoaded );
try {
loader.load( new URLRequest( "data.xml" ));
} catch ( error:Error ) {
trace( "Unable to load requested document." );
}
}
private function onDataLoaded( evt:Event ):void
{
var xml:XML = new XML( evt.target.data );
xml.ignoreWhitespace = true;
data.header = xml.header.children().toXMLString();
var sections:XMLList = xml.sections.section;
navIDs = new Array();
for ( var i:uint=0; i<sections.length(); i++ )
{
var section:XML = sections[ i ];
var id:String = section.@id;
navIDs[ i ] = id;
var vo:SectionVO = new SectionVO( id,
section.@label,
section.content );
data[ id ] = vo;
}
sendNotification( ApplicationFacade.INITIALIZE_SITE );
}
}
}
When SiteDataProxy is done parsing the xml data, it sends out a INITIALIZE_SITE Notification, resulting in the handleNotification() method of StageMediator being called. This in turn calls the initializeSite() method which initializes the SiteMediator and NavMediator instances. These two Mediators serve to facilitate communication between the Site movieclip and the MainNav movieclip with the framework.
8. Examining SiteMediator.as
SiteMediator retrieves data from SiteDataProxy and calls site.init() to initialize the header text. It also declares an interest in the SECTION_CHANGED Notification event and calls site.updateBody() to update its content when such an event is dispatched.
package com.hubflanger.puremvcsite.view
{
import com.hubflanger.puremvcsite.ApplicationFacade;
import com.hubflanger.puremvcsite.model.*;
import com.hubflanger.puremvcsite.view.component.Site;
import org.puremvc.as3.interfaces.*;
import org.puremvc.as3.patterns.mediator.Mediator;
public class SiteMediator extends Mediator implements IMediator
{
public static const NAME:String = "SiteMediator";
private var _siteDataProxy:SiteDataProxy;
public function SiteMediator( viewComponent:Object )
{
super( NAME, viewComponent );
_siteDataProxy = facade.retrieveProxy( SiteDataProxy.NAME ) as SiteDataProxy;
var data:Object = _siteDataProxy.getData();
site.init( data.header );
}
override public function listNotificationInterests():Array
{
return [
ApplicationFacade.SECTION_CHANGED
];
}
override public function handleNotification( note:INotification ):void
{
switch ( note.getName() ) {
case ApplicationFacade.SECTION_CHANGED:
update( note.getBody() as String );
break;
}
}
private function update( s:String ):void
{
var data:Object = _siteDataProxy.getData();
var vo:SectionVO = data[ s ];
var content:XMLList = vo.content;
site.updateBody( content.toXMLString() );
}
protected function get site():Site
{
return viewComponent as Site;
}
}
}
9. Examining NavMediator.as and MainNav.as
NavMediator retrieves navigation id and label info from SiteDataProxy and passes that along to the MainNav instance, allowing MainNav to initialize its navButton instances. MainNav registers itself as an event listener of MOUSE_DOWN events triggered by the navButton instances.
NavMediator in turn registers itself as an event listener of the NAV_BUTTON_PRESSED UIEvent bubbled up by the MainNav instance. Upon receiving such an event, NavMediator checks it against its currentSection variable to determine if a SECTION_CHANGED Notification should be sent.
When the SECTION_CHANGED Notification is sent, SiteMediator will respond and update the content in the body movieclip, and NavMediator will update its navButton instances to display the correct state depending on each button’s id.
package com.hubflanger.puremvcsite.view
{
import com.hubflanger.puremvcsite.ApplicationFacade;
import com.hubflanger.puremvcsite.model.*;
import com.hubflanger.puremvcsite.view.component.MainNav;
import com.hubflanger.puremvcsite.view.event.UIEvent;
import org.puremvc.as3.interfaces.*;
import org.puremvc.as3.patterns.mediator.Mediator;
public class NavMediator extends Mediator implements IMediator
{
public static const NAME:String = "NavMediator";
private var _siteDataProxy:SiteDataProxy;
public var currentSection:String;
public function NavMediator( viewComponent:Object )
{
super( NAME, viewComponent );
_siteDataProxy = facade.retrieveProxy( SiteDataProxy.NAME ) as SiteDataProxy;
nav.addEventListener( UIEvent.NAV_BUTTON_PRESSED, onNavButtonPressed );
var data:Object = _siteDataProxy.getData();
var navIDs:Array = _siteDataProxy.navIDs;
var navLabels:Array = new Array();
currentSection = navIDs[ 0 ];
for ( var i:uint=0; i<navIDs.length; i++ )
{
var id:String = navIDs[ i ];
var vo:SectionVO = data[ id ];
navLabels[ id ] = vo.label;
}
nav.init( navIDs, navLabels );
}
override public function listNotificationInterests():Array
{
return [
ApplicationFacade.SECTION_CHANGED
];
}
override public function handleNotification( note:INotification ):void
{
switch ( note.getName() ) {
case ApplicationFacade.SECTION_CHANGED:
nav.update( note.getBody() as String );
break;
}
}
private function onNavButtonPressed( evt:UIEvent ):void
{
if ( evt.id != currentSection ) {
currentSection = evt.id;
sendNotification( ApplicationFacade.SECTION_CHANGED, evt.id );
}
}
protected function get nav():MainNav
{
return viewComponent as MainNav;
}
}
}
package com.hubflanger.puremvcsite.view.component
{
import com.hubflanger.puremvcsite.view.event.UIEvent;
import flash.display.MovieClip;
import flash.events.*;
public class MainNav extends MovieClip
{
public var btn0:MovieClip;
public var btn1:MovieClip;
public var btn2:MovieClip;
private var navButtons:Array;
public function MainNav()
{
navButtons = [ btn0, btn1, btn2 ];
}
public function init( navIDs:Array, labels:Array ):void
{
for ( var i:uint=0; i<navIDs.length; i++ )
{
var id:String = navIDs[ i ];
var btn:MovieClip = navButtons[ i ];
btn.id = id;
btn.txt.text = labels[ id ];
btn.buttonMode = true;
btn.mouseChildren = false;
btn.addEventListener( MouseEvent.MOUSE_DOWN, onMouseDownHandler );
}
}
public function update( s:String ):void
{
for ( var i:uint=0; i<navButtons.length; i++ )
{
var btn:MovieClip = navButtons[ i ];
if ( btn.id == s ) {
btn.txt.textColor = 0x4B1E18;
} else {
btn.txt.textColor = 0xFFFFFF;
}
}
}
private function onMouseDownHandler( evt:Event ):void
{
dispatchEvent( new UIEvent( UIEvent.NAV_BUTTON_PRESSED, evt.target.id ));
}
}
}
10. In Closing
This is pretty much the gist of what happens within a PureMVC Flash application. It is my hope that this real-world example will help you get a jump start on using the PureMVC framework for your Flash projects. I also hope this tutorial managed to demonstrate the value and power of the MVC design pattern. As you start developing applications of greater complexity, using a framework such as PureMVC will help immensely in keeping your classes loosely-coupled, and easier to scale and maintain. Happy coding!

April 14th, 2008 at 10:25 am
[…] Check it out here: http://hubflanger.com/building-a-flash-site-using-puremvc/ […]
April 14th, 2008 at 8:26 pm
Great article Yee. Hope to see you at FITC!
April 15th, 2008 at 12:16 pm
Thank you, it’s exactly what I was looking for.
There is a problem with the display of the SiteDataProxy, NavMediator and MainNav classes. My guess is that your blog engine doesn’t really like smaller than signs in your for loops.
April 15th, 2008 at 4:58 pm
Thanks! I fixed the code.
April 26th, 2008 at 6:13 am
Great detail article, im working in the construction of an IM messenger and your article is extremly valuable. A pleasure to read it.
April 27th, 2008 at 8:04 am
This is the best “Getting Started” tutorial on PureMVC! Thanks for sharing.
May 2nd, 2008 at 10:51 am
Hi
great article. Would it be possible to transform this example to AS2?
Or perhaps save the .fla files in Flash8-format, so someone else
can transform the whole project to AS2 using the AS2 port of PureMVC?
Thanx a lot for any information
Achim
May 2nd, 2008 at 1:01 pm
Great overview! Thank you so much for your time and being so thorough.
May 2nd, 2008 at 1:02 pm
Thank you so much for your time and being so thorough. Keep it up!
May 21st, 2008 at 2:17 am
Good article, but I have one suggestion. I think the Proxy should have some proper functions to make his data accessible. Now every part that want to read his data has to get his untyped vo and work with that.
I think the proxy should at least have a methods & properties like:
function get numSections():int;
function getSectionVO(id:int):SectionVO;
It would be even better if it had:
function get numSections():int;
getSectionContent(id:int):XMLList)
function get numButtons():int;
getButtonId(index:int):String
getLabel(id:int):String
When a proxy controls access you can change at any moment how the proxy gets that info. So this his makes it more flexible and easier to read.
Keep up the good work
May 26th, 2008 at 2:26 pm
Thanks, Peter. You are right. To follow best practice, the data model should be a value object and not a generic Object. The strict typing will allow the compiler to catch errors that it may otherwise not.
May 30th, 2008 at 3:03 pm
[…] Read Tutorial and download support files No Comments Leave a Commenttrackback addressThere was an error with your comment, please try again. name (required)email (will not be published) (required)url […]
June 16th, 2008 at 5:17 pm
[…] Achei um post com um exemplo implementado nesse framework, link: http://hubflanger.com/building-a-flash-site-using-puremvc/ […]