Simple MVC with ActionScript 3

October 20, 2008 :: 5 Comments

In my search for a framework to implement the MVC pattern in my AS3 projects I found many complex solutions. These solutions seem to be very robust, and are probably good to explore for very large RIA projects. In my case, working in the fast paced world of online advertising, I needed a solution that was a little more basic and flexible.

Most of the projects I work on are small microsites and widgets for ad campaigns, I'm also working inside of the Flash IDE and interacting with designers who don't know much about AS, so be forwarned that the solution I'm using is implemented with that in mind.

Controllers

Controllers were a tough one for me to conceptualize in Actionscript 3, coming from the world of CakePHP and Ruby on Rails. So what helped was to refresh my mind with the core idea of a controller.

According to Wikipedia a controller does the following:

  1. A controller handles the input event from the user interface, often via a registered handler or callback.
  2. The controller notifies the model of the user action, possibly resulting in a change in the model's state. (e.g. controller updates user's Shopping cart).

The easiest way for me to implement this was using the Document Class. Here is an example:

 
package com.gn.controllers {
 
    /* Import Classes */
    import flash.display.*;
    import flash.events.*;
    import com.gn.views.VideoView;
    import com.gn.views.QuizView;
    import com.gn.views.ThumbnailView;
    import com.gn.models.XMLDataModel;
    import com.gn.events.XMLDataEvent;
    import com.gn.models.QuizModel;
    import com.gn.events.QuizEvent;
 
    /**
     *  Document Class: Quiz Based Video Player
     *    
     *  @langversion ActionScript 3
     *  @playerversion Flash 9.0.0
     *
     *  @author Adam.Duro
     *  @since  10.10.2008
     */
    public class AppController extends MovieClip {
 
	    /* Public Properties */
	    public var videoView:VideoView;
	    public var thumbnailView:ThumbnailView;
	    public var quizView:QuizView;
	    public var xmlDataModel:XMLDataModel;
	    public var quizModel:QuizModel;
 
	    /**
	     * Constructor
	     * 
	     * @private
	     */
    	public function AppController(){
    		super();
    		init();
    	}
 
    	/**
    	 * Initialize Application
    	 * 
    	 * @private
    	 */
    	private function init():void
    	{
    	    /* Initialize Models */
    	    xmlDataModel = new XMLDataModel();
    	    quizModel = new QuizModel();
 
    	    /* Initialize Views */
    	    videoView = new VideoView(vidContainer_mc, this);
    	    thumbnailView = new ThumbnailView(thumbContainer_mc, this);
    	    quizView = new QuizView(quizContainer_mc, this);
    	}
 
    	/**
    	 * Start Quiz Engine
    	 * 
    	 * @private
    	 */
    	public function startQuizEngine():void
    	{
    	    var xmlData:XML = xmlDataModel.data.titles.title[0];
    	    quizModel.loadQuiz(0, xmlData);
    	}
 
    	public function loadMovie(index:Number, xmlData:XML):void
    	{
    	    quizModel.loadQuiz(index, xmlData);
    	}
 
    	/**
    	 * Unlock Movie
    	 * 
    	 * @private
    	 */
    	public function unlockMovie():void
    	{
    	    quizModel.unlockMovie();
    	}
 
    }
 
}
 

So this class is responsible for instantiating the models, and view classes. It then passes reference to itself and the models along to the view classes. It is also is used to process user interaction from the views. If a user clicks a button that needs to interact with a model, it goes through the Controller to do that.

Models

Models hold data. They also interact with external web services and databases. I think of them as my data/state layer in an ActionScript project. In my simple MVC framework, Models are subclasses of the EventDispatcher class. For each Model I also create a custom Event class. Here is an example:

Model
 
package com.gn.models {
 
    /* Import Classes */
    import flash.net.*;
    import flash.events.*;
    import com.gn.events.XMLDataEvent;
 
    /**
     *  XML Data Model
     *    
     *  @langversion ActionScript 3
     *  @playerversion Flash 9.0.0
     *
     *  @author Adam.Duro
     *  @since  10.10.2008
     */
    public class XMLDataModel extends EventDispatcher {
 
	    /* Static Constants */
	    static const DATA_URL:String = 'flash/xml/datasheet.xml';
 
	    /* Private Properties */
	    private var _XMLLoader:URLLoader;
 
	    /* Public Properties */
	    public var data:XML;
 
	    /**
	     * Constructor
	     * 
	     * @private
	     */
    	public function XMLDataModel(){
    	    init();
    	}
 
    	/**
    	 * Initialize Model
    	 * 
    	 * @private
    	 */
    	private function init():void
    	{
    	    _XMLLoader = new URLLoader();
    	    _XMLLoader.addEventListener(Event.COMPLETE, xmlLoaded);
    	    _XMLLoader.load(new URLRequest(DATA_URL));
    	}
 
    	/**
    	 * Listener: XML Finished Loading
    	 * 
    	 * @private
    	 */
    	private function xmlLoaded(e:Event):void
    	{
    	    data = new XML(e.target.data);
    	    dispatchEvent(new XMLDataEvent(XMLDataEvent.XML_LOADED));
    	}
 
    }
 
}
 
Model Event Class
 
package com.gn.events {
 
    /* Import Classes */
    import flash.events.Event;
 
    /**
     *  Event: XML Data Event
     *    
     *  @langversion ActionScript 3
     *  @playerversion Flash 9.0.0
     *
     *  @author Adam.Duro
     *  @since  10.10.2008
     */
    public class XMLDataEvent extends Event {
 
        /* Static Constants */
        public static const XML_LOADED:String = "onXMLLoaded";
 
        /**
         *  Constructor
         *  
         *  @private
         */
        public function XMLDataEvent( type:String, bubbles:Boolean=true, cancelable:Boolean=false ){
        	super(type, bubbles, cancelable);
        }
 
        /**
         *  Override: Event Clone Function
         *  
         *  @inheritDoc
         */
        override public function clone() : Event {
        	return new XMLDataEvent(type, bubbles, cancelable);
        }
 
    }
 
}
 

When the model is loaded or modified, it dispatches an event from its associated ModelEvent. Any views that are listening for updates on that model receive those events.

Views

Views handle the presentation layer of the application. They are the GUI. In my case, I am often using Flash files that have been prepared by a designer. Thus, most of them consist of MovieClips within MovieClips, so on and so forth. Most of my views are wrapper classes for these MovieClips.

Rather than use the Linkage feature in the Flash IDE to bind these classes to their associated MovieClips, I use a private var to store reference to the MovieClip. That reference is passed in via the controller, when the view is instantiated. Here is that again for reference:

 
/* Initialize Views */
videoView = new VideoView(vidContainer_mc, this);
thumbnailView = new ThumbnailView(thumbContainer_mc, this);
quizView = new QuizView(quizContainer_mc, this);
 

The first argument is the container MovieClip, and the second is a reference to the controller so that the view can make calls to it when a user interaction occurs. Here is an example of a full view. (Be aware, this is going to be long):

 
package com.gn.views {
 
    /* Import Classes */
    import flash.display.*;
    import flash.events.*;
    import flash.text.*;
    import com.gn.controllers.AppController;
    import com.gn.events.QuizEvent;
    import com.gn.objects.Answer;
    import gs.TweenLite;
 
    /**
     *  View: Quiz View
     *    
     *  @langversion ActionScript 3
     *  @playerversion Flash 9.0.0
     *
 
 
 
     *  @author Adam.Duro
     *  @since  10.10.2008
     */
    public class QuizView extends MovieClip {
 
	    /* Private Properties */
	    private var _quizContainer:MovieClip;
	    private var _AppController:AppController;
 
	    /**
	     * Constructor
	     * 
	     * @private
	     */
    	public function QuizView(clip:MovieClip, controller:AppController){
    		_quizContainer = clip;
    		_AppController = controller
    		init();
    	}
 
    	/**
    	 * Initialize View
    	 * 
    	 * @private
    	 */
    	private function init():void
    	{
    	    /* Add Event Listeners */
    	    _AppController.quizModel.addEventListener(QuizEvent.QUIZ_LOADED, onQuizLoad);
    	    _AppController.quizModel.addEventListener(QuizEvent.MOVIE_UNLOCKED, onMovieUnlock);
    	}
 
    	/**
    	 * Listener: On Quiz Load
    	 * 
    	 * @private
    	 */
    	private function onQuizLoad(e:QuizEvent):void
    	{
    	    /* Show Quiz Container if Hidden */
    	    if (!_quizContainer.visible) {
    	        TweenLite.to(_quizContainer, 0.5, {autoAlpha: 1});
    	    }
 
    	    /* Populate Question */
    	    _quizContainer.question_txt.text = e.target.currentData.question;
    	    _quizContainer.question_txt.autoSize = TextFieldAutoSize.CENTER;
 
    	    /* Position Answers Container */
    	    _quizContainer.answersContainer_mc.y = _quizContainer.question_txt.textHeight + 1;
 
    	    /* Remove Old Answers from a previous Quiz */
    	    if (_quizContainer.answersContainer_mc.numChildren > 0) {
    	        for (var h:int = 0; h < _quizContainer.answersContainer_mc.numChildren; i++) {
    	            _quizContainer.answersContainer_mc.removeChildAt(h);
    	        }
    	    }
 
    	    /* Populate Answers for Current Quiz */
    	    for (var i:int = 0; i < e.target.currentData.answers.answer.length(); i++) {
    	        var answer:Answer = new Answer(i, e.target.currentData.answers.answer[i]);
    	        answer.isCorrect = (e.target.currentData.answers.answer[i].@correct == 1);
    	        if (i !== 0) {
    	            answer.y = _quizContainer.answersContainer_mc.height + 10;
    	        }
    	        answer.addEventListener(MouseEvent.CLICK, onAnswerClick);
    	        _quizContainer.answersContainer_mc.addChild(answer);
    	    }
    	}
 
    	/**
    	 * Listener: On Answer Click
    	 * 
    	 * @private
    	 */
    	private function onAnswerClick(e:MouseEvent):void
    	{
    	    if (e.target.isCorrect) {
    	        e.target.markRight(_AppController.unlockMovie);
    	    } else {
    	        e.target.markWrong();
    	    }
    	}
 
    	/**
    	 * Listener: On Movie Unlocked
    	 * 
    	 * @private
    	 */
    	private function onMovieUnlock(e:QuizEvent):void
    	{
    	    TweenLite.to(_quizContainer, 0.5, {autoAlpha: 0});
    	}
 
    }
 
}
 

Hooking Everything Together

So here is a basic over view of the flow an application takes when using this framework:

  1. The Controller (Document Class) is loaded
  2. The Controller instantiates the models
  3. The Controller instantiates the views, passing reference to the MovieClip on stage that contains the views various DisplayObjects
  4. When a Model is updated it dispatches an Event
  5. The views are setup to listen for those events, and react accordingly.
  6. When a user interacts with the app, the view calls a method on the Controller, which either updates the model, or handles the user interaction and reports back to the view.

Wrapping Up

This concludes a very fast paced overview of how one ActionScript developer implements MVC. This by no means is the only way. If you have questions or suggestions, please feel free to leave a comment.

Digg!


Comments

Matt Ronchetti :: November 2nd, 2008
Nice overview of the MVC pattern in AS. How about an RSS feed for the blog?
Adam Duro :: November 6th, 2008
Matt, Thanks. I hope it helps someone.

I am going to add an RSS feed to the blog in the next few days.
florian :: November 23rd, 2008
hey nice exmaple,
i was also searching for a simple mvc exmaple to understand the mvc basics! I´m new to MVC and I felt like you and coded an easy mvc example on my own... :) you can have a look at it on my blog (http://www.spierala.de). Our examples are similar but slightly different. You seem to use the controller to initialize the application. Your controller knows about the view and model. View accesses models data via the controller ( _AppController.quizModel ). I use the View for application initialization - it holds references of model and controller and knows about everything... My Controller is just waiting for to be called by the View and then calls functions of the model... Our Models are nearly the same - both extending Event Dispatcher, which makes sence. Why do you create a custom Event Class? As far as I can see the Event is not containing any special data? You can get the e.target Data also with normal Events. Could you make your project ready for download to have a deeper look at it? Thanks, Florian
Adam Duro :: November 23rd, 2008
@florian The reason I use a custom event class for my models is so I can trigger specifically named event types as static constants (ie. ModelEvent.EVENT_TYPE = "eventType"). Now I suppose that I could just put those event type constants in the model themselves or in a VO class, but I feel that creating a custom event class seems to structure the application in a more readable and organized fashion.

According to your architecture you put all your view code into the main document class? Doesn't that force you to put a whole lot of code for different components of the application all in one class? Do you have just one model, one view, and one controller for the entire application? I'm interested to know how you have approached this.

If you'd like, I can email you a zip file of an example application, but at this point I don't have any code that I can publish publicly. I will see if I can put something together to post publicly.
Eric :: January 1st, 2009
You're missing some things, I don't know why your controller is in the document class and why does it extend MovieClip? Beacuse you put all the display objects on it? The idea of MVC is to have everything loosely coupled. You should be programming to an interface for the Model and the Controller. The model and the views communicate through get methods. You are right about the model extending EventDispatcher. You are also missing the Composite Pattern for the views. You should get a copy of O'Reilly's Actionscript 3.0 Design Patterns, it is fantastic!

Leave a Comment