PureMVC: An Eventful Coding Experience

learning it the fun way

Developers in general have a love or hate kind of relationship with design patterns. I am suspicious of anyone who say they just loooove such and such design pattern. Mainly because patterns should appear in your life and immediately be followed by "f****, why didn't I do things like this before!" So there should be some shame attached to discovering it, a sense of regret I guess. Otherwise you are doing it wrong.

But joking aside, patterns should always work as that new shortcut in your daily commute that you never knew existed and feel really stupid for having wasted all that time before. Meta patterns are like that too, only more so. And that's what MVC is, a meta pattern, a jambalaya of patterns, meant to help you encapsulate your code until you feel slightly unhuman: cold and distant. But in a good way.

PureMVC: Thinking so you Don't Have To

PureMVC is a framework that uses a jambalaya (6 patterns last time I counted) and lets you--well, sometimes forces you to-- encapsulate the shit out of your code, to use the technical term. It is an incredibly simple collection of classes, that allows you to do an incredibly simple thing, and comes with an unreadable PDF file to help you understand it.

PureMVC is awesome, helpful and extremely simple. And the PDFs and drawings will all make sense once you manage to use the thing. But probably not before. Plus it's free.

When should you use it? If you are developing something that has buttons in it, do yourself a favor and use PureMVC. If you only use Events in your code if someone is pointing a gun at you and telling you to do so, use PureMVC. If you are using forms or result sets from databases, use PureMVC.

Using it makes far more sense for more "traditional" applications where you have different sections, multiple application states, displaying section and state specific data. It breaks down a bit when you have movie clips doing most of the View bit, and by that I mean if your application is mainly about moving sprites, resizing them, rotating them, and all that. Not that you can't use the framework in such cases, in fact the tutorial I wrote follows just such a case, but then the Controller side of things might get left out a bit. But you will see what I mean when we get to the actual tutorial.

Developing games with PureMVC might be weird, but then again it depends on the type of game. The Event model of the framework can fit very nicely with most types of games I've written, but the main advantages of the framework, by far, are: encapsulation and the management of user interface. Plus using MVC in everything you do will never hurt anybody.

I wanted with this tutorial to use PureMVC in this "interface manager" capacity, which is the one I use the most, unless I'm working on a more traditional type of application. The application I chose has buttons galore, at least three well defined sections, a bunch of states, manipulation and control of loaded data... and yet, the main point of the application is the manipulation of display objects.

PureMVC: The Framework

the ins and don'ts

Here I'll cover a quick overview of the framework and its elements. PureMVC is made of 3 parts:

Proxies: where the data should be stored. Or maybe, and this is the preferred way, where you store references to the objects where the data is kept, so it acts as an intermediary between the data objects and whatever needs that data.

Whether or not the calls to get the data are also stored in Proxies is a matter of preference. Some people use Commands to run the queries, or url requests. Complexity of data will probably determine how you setup things.

Proxies are kept isolated. They may talk to other parts but not listen to them. The proxy has to be accessed through its public interface. That's Encapsulation, son!

Commands: They are the links between the parts, they make the M talk to the V and vice versa, and yet keep them apart. Commands are useful to get things running, or to create intermediary stages in your event model. For instance: upon data loading, you want a lot of things to happen in a certain order, so the data loaded event triggers a Command which will put the house in order so to speak, making calls to multiple view components.

Mediators: these are wrappers for visual components. For example, you have an About Me section in your app, then the AboutMe movieClip that displays it will be wrapped by an AboutMeMediator. This is helpful for encapsulation. The mediators are part of the framework and so are able to "speak" and "listen" to it. It lets its wrapped visual component know only what it needs to know. So when the AboutMe data is ready for use in the AboutMeProxy, the AboutMeCommand might send a notification stating it so. Then the AboutMeMediator who has been listening for just such a notification tells its wrapped component, the AboutMe movieclip to show the loaded data. Mediators listen and talk to the framework.

Sounds complicated? It isn't. Not really. Repetition of these steps will make everything clearer. Also, I used the maximum number of steps in the previous example. You don't need a Command at all if you don't want one. Most of the time, Proxies and Mediators can do most of the work. But using Commands can make future changes and expansions of your application a lot easier to accomplish.

The Application: FlashNose 3D

turning on the giggling

For the tutorial I wanted to use something with a complex interface, multiple states, but at the same time fun. So no forms, no database resultsets... The idea for the application itself is not new; at least since Flash 5 or 6 I remember seeing similar things, particularly after masks could be set dynamically. And since the classes that allow for bitmap manipulation came about I've been wanting to use bitmapdata instead of masks to create the same result. Then X and Y rotation came along, use of cameras and the FileReference object for Flash... I felt it was about time a new version of the old app should be made. I will not cover these new features in the tutorial, PureMVC might be enough for now, if later you find out you need any of the other code, you might use the source as a reference.

So the application loads an image, and lets the user resize it, crop it and mark a center. Based on this center point, the application then slices the image and stack the slices up. These slices are moved according to mouse position and the distort effect happens. The results range from something akin to Sloth from the movie Goonies, to the Simpsons' Mr. Burns or a giant poodle. It all depends on what the user picks as a center point.

So there is a lot of bitmap manipulation and not much else really. FileReference is used to open an image from the user's system. Camera and Video objects are used to capture an image... But other than that, the code is very simple.

For the distortion effect I created three types of "effects". I show them here with vector slices.

The first one will allow the whole stack of slices to be moved across the stage. Click inside the movie to start the effect.

The second and third have the last slice fixed in place so that the ones on top of that "pop out" as the mouse move. Click inside the movie to start the effect.

The difference between second and third effect is the addition of X and Y rotation. Click inside the movie to start the effect.

The Components

The best way to get to know a PureMVC application is to check its Proxies and Mediators. This application has two proxies. One for the Image Data, which will store the bitmapData from the original image (useful for UNDOs.) And one for the list of already formatted images, which I call the SAMPLER. When you finish formatting an image, the formatted bitmapdata is stored as a SAMPLER. The same proxy allows for the storage of DEMO images. You can have up to 12 samplers, counting demos. But aside from the first 5 demos, images created by the user are not permanently stored. So if you close the application, the data is lost.

I don't use a separate object for the data, but store it in the Proxy itself. I usually don't do it this way, but since the data is very simple I thought I would make a better case for PureMVC if I avoided using too many small classes. In an application with multiple recordsets and calls to a database, you will definetely profit from using separate objects and let the Proxies act as...well... proxies.

The Mediators

There are 6 mediators. Which might usually mean there are 6 main components in the application. In this case this is somewhat true.

We start with the MainAppMediator. It is very common in PureMVC applications to have one meditator for the stage of the application. This mediator then adds all the other visual components and wraps them with their own mediators. It also keeps a permanent reference to the stage.

Then comes:

- one mediator for the LOADER section, where user chooses to load a local file or, if available, an image through the camera.

- there is a mediator for the SAMPLER menu, where the demos and user formatted images are stored.

- a mediator for the IMAGE NAV, where buttons to format the image and control effects are placed.

- amediator for IMAGE VIEW where, guess what, the image is viewed.

- and finally a mediator for the IMAGE itself. Not exactly necessary, but it serves to illustrate something which can be a bit confusing: a mediator which is created by another mediator (in this case the IMAGE VIEW mediator is responsible for creating the IMAGE mediator.)

Picking Meditators will make or break your application. You can go crazy and wrap each button with a mediator. Or you can group too many things under one Mediator and have a hell of a time with Notifications. I defend the solution that the best architecture will reveal itself to you as you build the application. And pre planning never hurt anyone.

Mediators should be created regarding the types of Notifications they will listen to. If these notifications affect all elements inside a visual component in one way or another, it makes sense to wrap that visual component with one Mediator.

Another common mistake is to try to make every action in the application (particularly in the view part) be triggered by notifications. This in general results in a lot of mediators as well so it is related to the first problem. The framework encourages this, since it helps with encapsulation, but it's best to go easy with this. The rule of thumb for me is: The application must make sense first and foremost. But since level of confusion is something relative to individuals, the best thing is to trust what you find best.

The good news is that the framework always work the same way. There is a limited number of paths for actions. Command->Proxy->Mediator, Command->Mediator, Proxy->Mediator... And even though you might have to work on some application that has a lot of these actions going on, they are not difficult to track and understand. A tylenol might be all you need.

The Tutorial

As a warning: Use the actionscript files in the source zip to see what you need to import, extend and implement to create any of the framework classes. The tutorial will teach you how to USE pureMVC not implement it. Trust me, it is best to start with less information; you may refer back to the source code for all the tiny details. So the best way to follow the tutorial is to download the source first and follow the illuminating text alongside the scripts.

Building the thing: Facade and Startup

finally some code

It is common to create separate packages for MODEL, CONTROLLER and VIEW. Why? Everyone is doing it! So go ahead.

Now you need to "hook up" your application to the framework. So in comes the Facade. The singleton that holds references to all other elements in the framework: all the Commands, the Mediators and the Proxies. So Facade is your mom, basically. By they way, it's pronouced Fuck-A-de (seriously, it's French.)

At the root of wherever you put those folders you create a class and call it ApplicationFacade. Here we will register the first and only command we'll use for this application. The ever present StartupCommand.

override protected function initializeController():void {
	super.initializeController();		
	registerCommand(Notifications.STARTUP, StartupCommand);
}
public function startup( stage:Object ):void {
	sendNotification(Notifications.STARTUP, stage );
}

StartupCommand then must be a class, you will keep it inside the CONTROLLER folder. (Remember: Commands are to Controller the way Proxies are to Model and Mediators are to View.)

I like to create a Notifications class which will store all the Notifications used in the app. Think of them as EVENT types. If it makes sense to break them into more files, dividing them by elements for instance so you have proxy notifications, controller notifications... do so. Most developers dump them all in the ApplicationFacade class. But I call that rude.

The Command

So back to the Command: StartupCommand. The important part of this class is the execute method.

When you register a command you register it with a specific notification (or Event type). Broadcast that event and every command attached to it will run EXECUTE.

Noticed how the execute method receives a NOTE as a parameter? Every notification may send a NOTE. And the content of that NOTE may be retrieved with a NOTE.getBody()

The StartupCommand receives the STAGE as the note's content. In the application facade, you will remember, the STARTUP notification was set with the stage attached to it as the NOTE parameter.

sendNotification(Notifications.STARTUP, stage );

Whoever listens to this notification will receive the STAGE as a note.

The Command will create the ImageDataProxy (found in the model folder) and the MainAppMeditator (found in the view folder). You create a Mediator by passing it its wrapped component.

The Command ends by sending a note itself. The note is of type INIT_INTERFACE (a public static constant found in the NOTIFICATIONS class)

Now all the things that can listen to notificaitons (Mediators and Commands) and who are interested in this particular notification will get all excited!

Document Class

package {
	
	import flash.display.Sprite;
	import flash.events.Event;
	import nose3d.ApplicationFacade;
	
	
	[SWF(width="625", height="650", backgroundColor="#000000", frameRate="30")]
	
	public class Main extends Sprite {
		
		function Main () {
			addEventListener(Event.ADDED_TO_STAGE, initMe, false, 0, true);
		}
		//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
		private function initMe (event:Event):void {
			var facade:ApplicationFacade = ApplicationFacade.getInstance();
			facade.startup(stage);
		}
	}
}

The Application Facade

package nose3d {
	
	import org.puremvc.as3.interfaces.IFacade;
    import org.puremvc.as3.patterns.facade.Facade;
	
	import nose3d.controller.*;
	
	public class ApplicationFacade extends Facade implements IFacade {
		
		 
		public static function getInstance(): ApplicationFacade {            
			if (instance == null) {
				instance = new ApplicationFacade();
			}
			return instance as ApplicationFacade;
		}
 
		override protected function initializeController():void {
			super.initializeController();		
			registerCommand(Notifications.STARTUP, StartupCommand);
		}
		
		public function startup( stage:Object ):void {
			sendNotification(Notifications.STARTUP, stage );
		}
		
	}
}

The StartupCommand

package nose3d.controller {
	
	import flash.display.Stage;
	
	import nose3d.view.MainAppMediator;
	import nose3d.model.ImageDataProxy;
	import nose3d.model.SamplerListProxy;
	import nose3d.Notifications;
	
	import org.puremvc.as3.patterns.command.SimpleCommand;
	import org.puremvc.as3.interfaces.ICommand;
	import org.puremvc.as3.interfaces.INotification;
	public class StartupCommand extends SimpleCommand implements ICommand {
	
		override public function execute( note:INotification ):void {
			
			//create the MainAppMediator
			var stage:Stage = note.getBody() as Stage;
            facade.registerMediator( new MainAppMediator( stage ) );
            
			//create the ImageProxy
			facade.registerProxy( new ImageDataProxy((stage.loaderInfo.url.indexOf("http") == -1)) );
			
			
			//create the SamplerProxy
			facade.registerProxy( new SamplerListProxy() );
			
			sendNotification(Notifications.INIT_INTERFACE);
		}
	}
}

The MainAppMediator

for all things must begin

Mediators and Proxies have NAMEs so they can be easily retrieved from the FACADE. The anatomy of the Mediator is very simple. It has a list of NOTIFICATIONS it is interested in, and then a SWITCH that states all it will do when it receives each one of those NOTIFICATIONS. The last part of a Mediator is usually a method to retrieve the reference to its wrapped component, which in the case of MainAppMediator means the STAGE object.

But this last method is optional, it is only done to save time, otherwise you have to use the MEDIATOR.getViewComponent() method and cast it to the right object type every time you use it.

Here is a list of the Notifications the MainAppMediator is interested in and the actions it will perform following each notification type:

Notifications.INIT_INTERFACE:Create the interface by adding all the other visual components and wrapping them with their own Mediators.


Notifications.IMAGE_LOADING: Will display a preload animation while the image is loading.


Notifications.IMAGE_FAILED: Will clear the preload if loading fails


Notifications.SAMPLER_IMAGE_LOADED: Will clear the preload if sampler image is loaded


Notifications.IMAGE_LOADED: Will clear the preload if image is loaded

Code for the MainAppMediator

package nose3d.view {
	
	import flash.display.Stage;
	import nose3d.Notifications;
	import nose3d.view.component.*;
	import org.puremvc.as3.interfaces.*;
	import org.puremvc.as3.patterns.mediator.Mediator;
	public class MainAppMediator extends Mediator implements IMediator {
		public static const NAME:String = "MainAppMediator";
		private var _preloader:Preloader;
		public function MainAppMediator (viewComponent:Object) {
			super( NAME, viewComponent );
		}
		override public function listNotificationInterests():Array {
			return [ 
					Notifications.INIT_INTERFACE,
					Notifications.IMAGE_LOADING,
					Notifications.IMAGE_FAILED,
					Notifications.SAMPLER_IMAGE_LOADED,
					Notifications.IMAGE_LOADED
				   ];
		}
	   override public function handleNotification(note:INotification):void {
			switch (note.getName()) {
				
				case Notifications.INIT_INTERFACE:     	
					initInterface();
					break;
				case Notifications.IMAGE_LOADING:     	
					showPreloader();
					break;
				case Notifications.IMAGE_FAILED:     	
					hidePreloader();
					break;
				case Notifications.IMAGE_LOADED:     	
					hidePreloader();
					break;
				case Notifications.SAMPLER_IMAGE_LOADED:     	
					hidePreloader();
					break;
			}
		}
		
		private function initInterface():void {
			
			stage.addChild(new AppBg());
			
			//add Smapler
			var sampler:SamplerNav = new SamplerNav();
			facade.registerMediator( new SamplerNavMediator(sampler));
			stage.addChild(sampler);
			
			//add load nav
			var loaderNav:ImageLoaderMc = new ImageLoaderMc();
			facade.registerMediator(new ImageLoaderMediator(loaderNav));
			stage.addChild(loaderNav);
			
			//add image viewer
			var imgView:ImageViewerMc = new ImageViewerMc();
			facade.registerMediator(new ImageViewerMediator(imgView));
			stage.addChild(imgView);
			
			//add image nav
			var imgNav:ImageNavMc = new ImageNavMc();
			facade.registerMediator(new ImageNavMediator(imgNav));
			stage.addChild(imgNav);
	
        	
		}
		
		private function showPreloader ():void {
			if (_preloader && _preloader.stage) {
				stage.removeChild(_preloader);
				_preloader = null;
			}
			_preloader = new Preloader();
			stage.addChild(_preloader);
		}
		
		private function hidePreloader ():void {
			if (_preloader && _preloader.stage) stage.removeChild(_preloader);
		}
		
		private function get stage():Stage{
			return viewComponent as Stage;
		}
	}
} 

The ImageLoaderMediator

for the imageloader sprite

This mediator won't listen to anything, unless it's coming from its own component, the ImageLoader sprite. The sprite contains two buttons. One will call the ImageDataProxy to use FileReference to load an image file from the user's system. Another will activate the camera if one is present, by alerting the framework with a notification of type SHOW_CAMERA.

What is different here is that this Mediator retrieves a reference to the ImageDataProxy and uses that proxy's public interface to load the local image.

I should add here, again, that it is best to use Commands as the middle man between Mediators and Proxies. But I thought the creation of many tiny Command type classes could discourage people from using PureMVC. But if you do decide to use Commands, then simply make the Mediator fire a notification which is mapped to a Command and let the command do the rest.

Code for ImageLoaderMediator

package nose3d.view {
	import flash.events.Event;
	
	import nose3d.Notifications;
	import nose3d.view.component.ImageLoader;
	import nose3d.dto.ImageLoadEvent;
	import nose3d.model.ImageDataProxy;
	import org.puremvc.as3.interfaces.*;
	import org.puremvc.as3.patterns.mediator.Mediator;
	public class ImageLoaderMediator extends Mediator implements IMediator {
		public static const NAME:String = "ImageLoaderMediator";
		
		private var _imageDataProxy:ImageDataProxy;
		public function ImageLoaderMediator (viewComponent:Object) {
			super( NAME, viewComponent );
			
			_imageDataProxy = facade.retrieveProxy(ImageDataProxy.NAME) as ImageDataProxy;
			initListeners();
			
		}
		
		private function get component():ImageLoader{
			return viewComponent as ImageLoader;
		}
		
		private function initListeners ():void {
			component.addEventListener(ImageLoader.LOAD, onLoadClick, false, 0, true);
			component.addEventListener(ImageLoader.SNAP_SHOT, onLoadFromCamera, false, 0, true);
		}
		
		private function onLoadClick (event:Event):void {
			_imageDataProxy.loadLocalImage ();
		}
		
		private function onLoadFromCamera (event:Event):void {
			sendNotification(Notifications.SHOW_CAMERA);
		}
	}
}

The ImageDataProxy

data for the loaded image

Here things could get a bit more complicated and perhaps deservedly so. What I mean by this is that I could have created a LoadLocalImage object, and a LoadSamplerImage object and have the proxy reference these, instead of having all that code inside the proxy itself. But I thought it would be less confusing to put all that iside the proxy. Once you get the gist of things, you may do it the way it suits you best.

Every proxy has a DATA object. It may be anything. This object can be retrieved by anything else in the framework through the PROXY.getData() method.

In this application the ImageDataProxy makes a simple Object that contains values for URL and BITMAPDATA, and the url might even be null.

If the user loads an image from the local file system, ImageDataProxy will load that image and store its bitmapdata. If the image comes from the demos in the SAMPLER menu, then an image is loaded from the local server, and the bitmapdata is stored. If the camera is used instead, then once the user takes a snapshot, the bitmapdata for that is stored in the ImageDataProxy DATA object.

While the image is loading, the proxy sends a notification of type IMAGE_LOADING, and if it fails or is successful, other notifications are sent.

Also there is a distinction to whether it was a SAMPLER image which was loaded or not. A SAMPLER image is already formatted and already contains information about its center, so the effect can be applied to it as soon as the image is available. The other type of image must be formatted first by the user and so requires more steps.

Code for the ImageDataProxy

package nose3d.model {
    
	import flash.display.Loader;
	import flash.display.BitmapData;
	import flash.net.URLRequest;
	import flash.events.Event;
	import flash.events.IOErrorEvent;
	
	import flash.net.FileFilter;
	import flash.net.FileReference;
	
	import nose3d.Notifications;
    import nose3d.dto.Sampler;
	  
    import org.puremvc.as3.interfaces.IProxy;
    import org.puremvc.as3.patterns.proxy.Proxy;

	
    public class ImageDataProxy extends Proxy implements IProxy {
       
		public static const NAME:String = "ImageDataProxy";
		private var _loader:Loader;
		private var _fromSampler:Boolean;
		private var _file:FileReference;
		
		
		public function ImageDataProxy (locally:Boolean){
			super(NAME);
		}
		
		
		public function loadImage (url:String, fromSampler:Boolean = false):void {
			
			_fromSampler = fromSampler;
			var loader:Loader = new Loader();
			
			loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onImageLoaded); 
			loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, onImageFailed);
			loader.load(new URLRequest(url));
			sendNotification(Notifications.IMAGE_LOADING);
		}
		
		public function loadLocalImage ():void {
			_file = new FileReference();
			var imgFilter:FileFilter = new FileFilter("Images (*.jpg, *.jpeg, *.gif, *.png)", "*.jpg;*.jpeg;*.gif;*.png");
			_file.addEventListener(Event.SELECT, onFileSelected, false, 0, true);
			_file.addEventListener(IOErrorEvent.IO_ERROR, onImageFailed);
			_file.browse([imgFilter]);
		}
		
		public function loadSampler (sampler:Sampler):void {
			data = sampler;
		}
		
		public function loadFromCamera (bitmapData:BitmapData):void {
			data = new Object ();
			data.bitmapData = bitmapData;
			_fromSampler = false;
			sendNotification(Notifications.IMAGE_LOADED);
		}
		//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
		private function onImageLoaded (event:Event):void {
			event.target.removeEventListener(Event.COMPLETE, onImageLoaded); 
			
			data = new Object ();
			data.url = event.target.url;
			data.bitmapData = event.target.content.bitmapData;
			
			if (!_fromSampler) {
				sendNotification(Notifications.IMAGE_LOADED);
			} else {
				sendNotification(Notifications.SAMPLER_IMAGE_LOADED);
			}
		}
		
		private function onImageFailed (event:IOErrorEvent):void {
			sendNotification(Notifications.IMAGE_FAILED);
		}
		
		private function onFileSelected(event:Event):void {
			sendNotification(Notifications.IMAGE_LOADING);
			_file.addEventListener(Event.COMPLETE, onFileLoaded); 
			_file.load();
		}
		private function onFileLoaded (event:Event):void {
			_fromSampler = false;
			var fileReference:FileReference=event.target as FileReference;
			var loader:Loader = new Loader();
			loader.loadBytes(fileReference.data);
			loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onImageLoaded, false, 0, true);
		}
    }
}

The SamplerListProxy

data for the demos

Here we store the original and static data of the demos, which is comprised of URL, center POINT and BITMAPDATA.

Once the demo image is loaded, its data is updated and the BITMAPDATA is stored so we don't have to load the image again.

The public interface of this proxy allows for the Mediators to set the selected SAMPLER and retrieve it, as well as update the SAMPLER object with the loaded bitmapdata.

Code for the SamplerListProxy

package nose3d.model {
	
	import flash.geom.Point;
	import flash.display.BitmapData;
	
	import nose3d.Notifications;
	import nose3d.dto.Sampler;
	import org.puremvc.as3.interfaces.IProxy;
	import org.puremvc.as3.patterns.proxy.Proxy;
	
	public class SamplerListProxy extends Proxy implements IProxy {
		public static const NAME:String = "SamplerListProxy";
	
		private var _samplerList = [
									new Sampler("images/sly.jpg" , new Point(142,168), 20, null),
									new Sampler("images/house.jpg" , new Point(185,189), 10, null),
									new Sampler("images/vader.jpg" , new Point(148,132), 20, null),
									new Sampler("images/bush.jpg" , new Point(179,233), 10, null),
									new Sampler("images/obama.jpg" , new Point(108,178), 8, null)
									];
		
		private var _selected:Sampler;
		
		
		public function SamplerListProxy (){
			super( NAME, new Object() );
			data = _samplerList;
		}
	
		public function set selected (value:Sampler):void {
			_selected = value;
			
		}
		public function get selected ():Sampler {
			return _selected;
		}
		
		public function addSampler (sampler:Sampler):void {
			if (_samplerList.length == 12) return;
			_samplerList.push(sampler);
			sendNotification(Notifications.SAMPLER_CREATED);
		}
		
		public function updateSelected (data:BitmapData):void {
			var index:int = _samplerList.indexOf(_selected);
			if (index != -1) {
				_selected.bitmapData = data;
				_samplerList[index] = _selected;
			}
		}
		
	}
}

The SamplerNavMediator

for the samplernav sprite

This Mediator begins by retrieving a reference to the ImageDataProxy and the SamplerListProxy (again, you might want to use commands for these). Then it builds the Sample Nav based on the data already stored in the SamplerListProxy (the demos.) This Mediator also listens to two Notifications:

Notifications.SAMPLER_CREATED: It recreates the sampler menu when a new one is created.


Notifications.IMAGE_LOADED: If the user loads an image from their computer or the camera, if there is a SAMPLER button selected in the sampler menu, it is disselected.

The Mediator also listens to events dispatched by its viewComponent (the SamplerNav sprite.) So when the user clicks on one of the Sampler buttons, an event is dispatched, and an object is passed through this event that carries data from the selected sampler.

If the data contains bitmapdata information, the mediator sends a notification to the framework alerting it with Notifications.SAMPLER_SELECTED. If there is no bitmapdata attached to this SAMPLER yet, then the ImageDataProxy is told to load the URL contained in the selected Sampler object.

Code for SamplerNavMediator

package nose3d.view {
	import nose3d.Notifications;
	import nose3d.view.component.SamplerNav;
	import nose3d.model.SamplerListProxy;
	import nose3d.model.ImageDataProxy;
	import nose3d.dto.SamplerEvent;
	import nose3d.dto.Sampler;
	
	import org.puremvc.as3.interfaces.*;
	import org.puremvc.as3.patterns.mediator.Mediator;
	
	
	public class SamplerNavMediator extends Mediator implements IMediator {
		
		public static const NAME:String = "SamplerNavMediator";
		
		private var _samplerListProxy:SamplerListProxy;
		private var _imageDataProxy:ImageDataProxy
		
		public function SamplerNavMediator (viewComponent:Object) {
			super( NAME, viewComponent );
			
			_samplerListProxy = facade.retrieveProxy( SamplerListProxy.NAME ) as SamplerListProxy;
			_imageDataProxy = facade.retrieveProxy( ImageDataProxy.NAME ) as ImageDataProxy;
			
			component.buildNav(_samplerListProxy.getData() as Array);
			initListeners();
		
		}
		
		override public function listNotificationInterests():Array {
			return [ 
					Notifications.SAMPLER_CREATED,
					Notifications.IMAGE_LOADED
				   ];
		}
		
		
		override public function handleNotification(note:INotification):void {
			switch (note.getName()) {
				
				case Notifications.SAMPLER_CREATED:    
					component.buildNav(_samplerListProxy.getData() as Array);
					break;
				
				case Notifications.IMAGE_LOADED:    
					component.clearSelection();
					break;
			}
		}
		
		
		private function initListeners ():void {
			component.addEventListener(SamplerEvent.SELECTED, onSamplerSelected, false, 0, true);
		}
		
		private function get component():SamplerNav{
			return viewComponent as SamplerNav;
		}
		
		private function onSamplerSelected (event:SamplerEvent):void {
			
			var sampler:Sampler = event.getSampler();
			
			_samplerListProxy.selected = sampler;
			if (sampler.bitmapData) {
				sendNotification(Notifications.SAMPLER_SELECTED, event.getSampler());
			} else {
				_imageDataProxy.loadImage(sampler.url, true);
			}
		}
		
	}
}

A Word on DTOs

when you can't get enough events

It is very common when building an application with PureMVC to use custom events. Almost always these events will be dispatched by view components and listened to by their wrappers, the Mediators. But depending on how you organize your code, you might have Proxies listening in or even Commands. So I usually create a package alongside the framework and call it DTO (Data Transfer Objects.) These might not contai only events, the package might include classes of objects that are carried by event objects. One instance of this is the class Sampler which is passed around through the SamplerEvent.

This application has 4 custom events. And their use will become clearer once we start using them. One such event we already saw is the SamplerEvent.

The SamplerNavMediator listens to the SamplerNav button click. When the button is clicked it dispatches an event with the Sampler object inside it. The SamplerNavMediator then retrieves this object and uses the information contained inside of it to either load a fresh new image or display one previously loaded.

The Sampler Object

package nose3d.dto {
	import flash.display.BitmapData;
	import flash.geom.Point;
	
	
	public class Sampler {
		
		private var _url:String;
		private var _center:Point;
		private var _startRadius:int;
		private var _bitmapData:BitmapData;
		
		
		public function Sampler (url:String, center:Point, startRadius:int, bitmapData:BitmapData = null) { 
			_url = url;
			_center = center;
			_startRadius = startRadius;
			_bitmapData = bitmapData;
		}
		
		public function get url ():String {
			return _url;
		}
		
		public function get bitmapData ():BitmapData {
			return _bitmapData;
		}
		
		public function set bitmapData (value:BitmapData):void {
			_bitmapData = value;
		}
		
		
		public function get center ():Point {
			return _center;
		}
		
		public function get startRadius ():int {
			return _startRadius;
		}
	}
	
}

The CropEvent

package nose3d.dto {
	import flash.events.Event;
	import flash.geom.Rectangle;
	
	public class CropEvent extends Event {
		
		public static const CROP:String = "crop";
		
		private var _crop:Rectangle;
		
		public function CropEvent (type:String, crop:Rectangle , bubbles:Boolean = false, cancelable:Boolean = false) { 
			super(type, bubbles, cancelable);
			_crop = crop;
		}
		
		public function getCropRectangle ():Rectangle {
			return _crop;
		}
		
		public override function clone():Event { 
			return new CropEvent(type,_crop, bubbles, cancelable);
		}
		
	}
	
}

The CenterEvent

package nose3d.dto {
	import flash.events.Event;
	import flash.geom.Point;
	
	public class CenterEvent extends Event {
		
		public static const CENTER:String = "center";
		
		private var _radius:int;
		private var _origin:Point;
		
		public function CenterEvent (type:String, radius:int, origin:Point, bubbles:Boolean = false, cancelable:Boolean = false) { 
			super(type, bubbles, cancelable);
			_radius = radius;
			_origin = origin;
		}
		
		public function getCenterRadius ():int {
			return _radius;
		}
		
		public function getCenterPoint ():Point {
			return _origin;
		}
		
		public override function clone():Event { 
			return new CenterEvent(type,_radius, _origin, bubbles, cancelable);
		}
		
	}
	
}

The ImageLoadEvent

package nose3d.dto {
	import flash.events.Event;
	
	public class ImageLoadEvent extends Event {
		
		public static const LOAD:String = "load";
		
		private var _url:String;
		
		public function ImageLoadEvent (type:String, url:String, bubbles:Boolean = false, cancelable:Boolean = false) { 
			super(type, bubbles, cancelable);
			_url = url;
		}
		
		public function getImageURL():String {
			return _url;
		}
		
		public override function clone():Event { 
			return new ImageLoadEvent(type, _url, bubbles, cancelable);
		}
		
	}
	
}

The SamplerEvent

package nose3d.dto {
	import flash.events.Event;
	
	public class SamplerEvent extends Event {
		
		public static const NEW_SAMPLER:String = "sampler";
		public static const SELECTED:String = "selected";
		
		private var _data:Sampler;
		
		public function SamplerEvent (type:String, data:Sampler, bubbles:Boolean = false, cancelable:Boolean = false) { 
			super(type, bubbles, cancelable);
			_data = data;
		}
		
		public function getSampler():Sampler {
			return _data;
		}
		
		public override function clone():Event { 
			return new SamplerEvent(type, _data, bubbles, cancelable);
		}
		
	}
	
}

The ImageNavMediator

for the imagenav sprite

The heart of the application. This is where we have 99% of the buttons used in the interface, and also where we display state changes. There are a total of 7 states in this application. They are:

Start: When the application first loads. We show no buttons in the ImageNav, only the introduction to the application.

Sampler: When the user has clicked on a Sampler Image. The ImageNav displays only the ZOOM and EFFECT buttons.

Failed: When an image failed loading. All buttons are hidden.

Snapshot: When the user chose to use the camera to capture an image. We show the SNAP button.

Step1: A new image was loaded then the first step (formatting) is displayed. We show the MOVE, RESIZE, CROP, UNDO, CLEAR and NEXT buttons.

Step2: After an image is formatted, the user moves to step 2 where the center of the stacked slices is picked. We show the CENTER button, plus UNDO, CLEAR and NEXT buttons.

Step3: On step 3 the user then can run the effect. Again we show ZOOM and EFFECT, but in this case the user may choose to UNDO or CLEAR the image.

The notifications this Mediator listens to are:

Notifications.SAMPLER_SELECTED: Shows Sampler state. This notification is sent when the user clicked on a SAMPLER button and the image linked to that button has already been loaded and so its bitmapdata is available to be displayed.


Notifications.SAMPLER_IMAGE_LOADED: Show sSampler state. Similar to the previous one, but this notification is sent when a SAMPLER is selected but the image linked to it has not been loaded yet. The notifcation is sent only after the image is loaded, and that is done by the ImageDataProxy.


Notifications.IMAGE_LOADED: Shows Step1 state, reset the effect index to 1 (meaning, when the user clicks on the effect button, it will run the first effect, and then increment)


Notifications.IMAGE_FAILED: Shows Failed state.


Notifications.SHOW_CAMERA: Shows Snapshot state.


Notifications.IMAGE_UPDATED: Enables the UNDO button, making that action available to the user.


Notifications.SET_IMAGE_CENTER: Enables the NEXT button, since the user cannot move to Step3 until the stack center is set.


Notifications.RUN_EFFECT_1: Increments effect index to 2.


Notifications.RUN_EFFECT_2: Increments effect index to 3.


Notifications.RUN_EFFECT_3: Resets effect index to 1. These may look weird now, because the ImageNav is not responsible to actually running the effects, but the same Notification can be listened to by different Mediators, and so while the ImageViewerMediator will actually run the effect when it receives these notifications, all the ImageNav needs to do is update the index for the next time the user clicks on the EFFECT button. So this component knows nothing of what other components are doing. It doesn't need to. And this is a good thing.


The rest of this Mediator is taken up by listeners. There are 10 buttons in the viewComponent, they each dispatch an event when clicked and the wrapper listens to each one of those. And then there is a separate event for certain types of buttons which can be SELECTED and DISSELECTED. But most of what the Mediator does after receiving these events is inform the framework the user clicked one of the buttons (informing that a certain tool has been selected, or that the user wants to undo, or that the user wants to clear the image...)

Code for ImageNavMediator

package nose3d.view {
	import flash.events.Event;
	
	import nose3d.Notifications;
	import nose3d.view.component.ImageNav;

	import org.puremvc.as3.interfaces.*;
	import org.puremvc.as3.patterns.mediator.Mediator;

	public class ImageNavMediator extends Mediator implements IMediator {
		public static const NAME:String = "ImageNavMediator";
		
		private var _effect:int = 1;
		public function ImageNavMediator (viewComponent:Object) {
			super( NAME, viewComponent );
			initListeners();
		}
		override public function listNotificationInterests():Array {
			return [ 
					Notifications.SAMPLER_SELECTED,
					Notifications.IMAGE_LOADED,
					Notifications.IMAGE_FAILED,
					Notifications.SAMPLER_IMAGE_LOADED,
					Notifications.SHOW_CAMERA,
					Notifications.IMAGE_UPDATED,
					Notifications.SET_IMAGE_CENTER,
					Notifications.RUN_EFFECT_1,
					Notifications.RUN_EFFECT_2,
					Notifications.RUN_EFFECT_3					
				   ];
		}

	   override public function handleNotification(note:INotification):void {
			
			switch (note.getName()) {
				
				
				case Notifications.SAMPLER_SELECTED:     	
					component.state = "sampler";
					break;
				
				case Notifications.SAMPLER_IMAGE_LOADED:     	
					_effect = 2;
					component.state = "sampler";
					break;
					
				case Notifications.IMAGE_LOADED:
					_effect = 1;
					component.state = "step1";
					break;
				case Notifications.IMAGE_FAILED:
					component.state = "failed";
					break;
				case Notifications.SHOW_CAMERA:
					component.state = "snapshot";
					break;
				case Notifications.IMAGE_UPDATED:     	
					component.enableUndo(true);
					break;
				case Notifications.SET_IMAGE_CENTER:     	
					component.enableNext(true);
					break;
				case Notifications.RUN_EFFECT_1:     	
					_effect = 2;
					break;
				case Notifications.RUN_EFFECT_2:     	
					_effect = 3;
					break;
				case Notifications.RUN_EFFECT_3:     	
					_effect = 1;
					break;
			}
			
		}
		
		private function get component():ImageNav{
			return viewComponent as ImageNav;
		}
		
		private function initListeners ():void {
			component.addEventListener(ImageNav.MOVE, onSelectMove, false, 0, true);
			component.addEventListener(ImageNav.RESIZE, onSelectResize, false, 0, true);
			component.addEventListener(ImageNav.CROP, onSelectCrop, false, 0, true);
			component.addEventListener(ImageNav.ZOOM, onSelectZoom, false, 0, true);
			component.addEventListener(ImageNav.CENTER, onSelectCenter, false, 0, true);
			component.addEventListener(ImageNav.EFFECT, onSelectEffect, false, 0, true);
			component.addEventListener(ImageNav.SNAP, onSelectSnap, false, 0, true);
			component.addEventListener(ImageNav.UNDO, onUndo, false, 0, true);
			component.addEventListener(ImageNav.NEXT, onNext, false, 0, true);
			component.addEventListener(ImageNav.CLEAR, onClear, false, 0, true);
			component.addEventListener(ImageNav.DISSELECT, onDisselect, false, 0, true);
		}
		
		private function onSelectMove (event:Event):void {
			sendNotification(Notifications.CLEAR_TOOL);
			sendNotification(Notifications.SELECT_MOVE_TOOL);
		}
		private function onSelectResize (event:Event):void {
			sendNotification(Notifications.CLEAR_TOOL);
			sendNotification(Notifications.SELECT_RESIZE_TOOL);
		}
		private function onSelectCrop (event:Event):void {
			sendNotification(Notifications.CLEAR_TOOL);
			sendNotification(Notifications.SELECT_CROP_TOOL);
		}
		private function onSelectCenter (event:Event):void {
			sendNotification(Notifications.CLEAR_TOOL);
			sendNotification(Notifications.SELECT_CENTER_TOOL);
		}
		private function onSelectZoom (event:Event):void {
			sendNotification(Notifications.CLEAR_TOOL);
			sendNotification(Notifications.SELECT_ZOOM_TOOL);
		}
		private function onSelectEffect (event:Event):void {
			
			switch (_effect) {
				case 1:
					sendNotification(Notifications.RUN_EFFECT_1);
				break;
				case 2:
					sendNotification(Notifications.RUN_EFFECT_2);
				break;
				case 3:
					sendNotification(Notifications.RUN_EFFECT_3);
				break;
			}
		}
		private function onSelectSnap (event:Event):void {
			sendNotification(Notifications.SNAP_PICTURE);
		}
		private function onDisselect (event:Event):void {
			sendNotification(Notifications.CLEAR_TOOL);
		}
		
		private function onUndo (event:Event):void {
			sendNotification(Notifications.CLEAR_TOOL);
			_effect = 1;
			switch (component.state) {
				
				case "step1":
					sendNotification(Notifications.UNDO_IMAGE_STEP_1);
					component.enableUndo(false);
				break;
				case "step2":
					component.state = "step1";
					sendNotification(Notifications.UNDO_IMAGE_STEP_2);
				break;
				case "step3":
					component.state = "step2";
					sendNotification(Notifications.UNDO_IMAGE_STEP_3);
				break;
				
			}
		}
		private function onNext (event:Event):void {
			sendNotification(Notifications.CLEAR_TOOL);
			switch (component.state) {
				case "start":
					component.state = "step1";
				break;
				case "step1":
					component.state = "step2";
				break;
				case "step2":
					component.state = "step3";
					sendNotification(Notifications.NEW_SAMPLER);
				break;
				
			}
		}
		private function onClear (event:Event):void {
			sendNotification(Notifications.CLEAR_IMAGE);
			component.state = "start";
		}
	}
}

The ImageViewerMediator

for the imageviewer sprite

The arms and legs of the application.

Here you will find the longest list of Notifications yet.

Notifications.SAMPLER_IMAGE_LOADED: Upon receiving this notification the Mediator will grab the BitmapData loaded and use it to create an Image Object, wrapped in its ImageMediator. This Image object is then formatted using the data from the SELECTED SAMPLER in the SamplerListProxy. Then a call is made to the SamplerListProxy and the Selected Sampler is updated with the loaded bitmapdata, so that we have this information stored in memory. It ends by sending a notification to RUN_EFFECT_1


Notifications.SAMPLER_SELECTED: Similar to the previous one, but with this we use the bitmapdata already stored in the SAMPLER object.


Notifications.IMAGE_LOADED: This too will create an Image object with the loaded bitmapdata, but this time from ImageDataProxy. The image however is not formatted, but left to the user to do so.


Notifications.IMAGE_FAILED: If any Image object is currently being displayed, it is destroyed.


Notifications.SHOW_CAMERA: An empty Image object is created. It will grab its bitmapdata from the Video object inside a loop. In this state, the ImageView component will display the video until the user clicks the SNAP button in the ImageNav, which then sends the next notification.


Notifications.SNAP_PICTURE: The loop showing the camera is removed, the Video and Camera objects are destroyed and the bitmapdata from the last frame displayed is sent to the ImageDataProxy as a loaded image.


Notifications.CLEAR_TOOL: The viewComponent clears the current selected tool.


Notifications.SELECT_MOVE_TOOL: User has clicked on the MOVE button in ImageNav. The viewComponent displays a movieClip with the MOVE icon, which is dragged by the Mouse.


Notifications.SELECT_CROP_TOOL: User has clicked on the CROP button in ImageNav. The viewComponent displays a movieClip with the CROP icon, which is dragged by the Mouse.


Notifications.SELECT_RESIZE_TOOL: User has clicked on the RESIZE button in ImageNav. The viewComponent displays a movieClip with the RESIZE icon, which is dragged by the Mouse.


Notifications.SELECT_CENTER_TOOL: User has clicked on the CENTER button in ImageNav. The viewComponent displays a movieClip with the CENTER icon, which is dragged by the Mouse.


Notifications.SELECT_ZOOM_TOOL: User has clicked on the ZOOM button in ImageNav. The viewComponent displays a movieClip with the ZOOM icon, which is dragged by the Mouse.


Notifications.IMAGE_UPDATED: the Image Object is refreshed with the updated bitmapdata.


Notifications.CLEAR_IMAGE: Similar to IMAGE_FAILED, this will destroy the current Image object and remove it from the viewComponent.

Other than these Notifications, there are three very important events dispatched by the viewComponent (the ImageViewer sprite), when the user CROPS, RESIZE or picks the CENTER of the image. These are all custom events from the DTO package and most of the notifications they trigger are listened to by the ImageMediator.

The one main difference here is that this mediator is responsible for the creation and removal of yet another Mediator, the ImageMediator. This is the only different kind of code you will find here.

To remove a mediator all you need to do is call the removeMediator() method in the Facade.

Code for ImageViewerMediator

package nose3d.view {
	import flash.events.Event;
	
	import nose3d.Notifications;
	import nose3d.view.component.ImageViewer;
	import nose3d.view.component.Image;
	import nose3d.view.ImageMediator;
	import nose3d.model.ImageDataProxy;
	import nose3d.model.SamplerListProxy;
	import nose3d.dto.CropEvent;
	import nose3d.dto.CenterEvent;
	import nose3d.dto.Sampler;
	
	
	import org.puremvc.as3.interfaces.*;
	import org.puremvc.as3.patterns.mediator.Mediator;

	public class ImageViewerMediator extends Mediator implements IMediator {
		public static const NAME:String = "ImageViewerMediator";
		
		private var _imageDataProxy:ImageDataProxy;
		private var _samplerListProxy:SamplerListProxy;
		
		
		private var _imageMediator:ImageMediator;
		
	
		public function ImageViewerMediator (viewComponent:Object) {
			super( NAME, viewComponent );
			_imageDataProxy = facade.retrieveProxy( ImageDataProxy.NAME ) as ImageDataProxy;
			_samplerListProxy  = facade.retrieveProxy( SamplerListProxy.NAME ) as SamplerListProxy;
			initListeners();
			
		}
		override public function listNotificationInterests():Array {
			
			return [ 
					Notifications.SAMPLER_IMAGE_LOADED,
					Notifications.SAMPLER_SELECTED,
					Notifications.IMAGE_LOADED,
					Notifications.IMAGE_FAILED,
					Notifications.SHOW_CAMERA,
					Notifications.SNAP_PICTURE,
					Notifications.CLEAR_TOOL,
					Notifications.SELECT_MOVE_TOOL,
					Notifications.SELECT_CROP_TOOL,
					Notifications.SELECT_RESIZE_TOOL,
					Notifications.SELECT_CENTER_TOOL,
					Notifications.SELECT_ZOOM_TOOL,
					Notifications.IMAGE_UPDATED,
					Notifications.CLEAR_IMAGE
				   ];
		}

	   override public function handleNotification(note:INotification):void {
			
			switch (note.getName()) {
				
				case Notifications.SAMPLER_IMAGE_LOADED:     	
					showSampleImage();
					break;
				case Notifications.SAMPLER_SELECTED:     	
					showStoredSampleImage(note.getBody() as Sampler);
					break;
				case Notifications.IMAGE_LOADED:     	
					showLoadedImage();
					break;
				case Notifications.IMAGE_FAILED:     	
					component.destroyImage();
					break;
				case Notifications.SHOW_CAMERA:     	
					showCameraImage();
					break;
				case Notifications.SNAP_PICTURE:     	
					showSnapShot();
					break;
				case Notifications.IMAGE_UPDATED:     	
					component.showImage(_imageMediator.component);
					break;
				case Notifications.CLEAR_TOOL:     	
					component.clearTool();
					break;
				case Notifications.SELECT_MOVE_TOOL:     	
					component.addTool("move");
					break;
				case Notifications.SELECT_CROP_TOOL:     	
					component.addTool("crop");
					break;
				case Notifications.SELECT_RESIZE_TOOL:     	
					component.addTool("resize");
					break;
				case Notifications.SELECT_CENTER_TOOL:     	
					component.addTool("center");
					break;
				case Notifications.SELECT_ZOOM_TOOL:     	
					component.addTool("zoom");
					break;
				case Notifications.CLEAR_IMAGE:     	
					component.destroyImage();
					break;
				
			}
		}
		
		private function showStoredSampleImage (sampler:Sampler):void {
			component.destroyImage();
			
			facade.removeMediator(ImageMediator.NAME);
			
			var image:Image = new Image(sampler.bitmapData);
			image.setCenter(sampler.center);
			image.setStartRadius(sampler.startRadius);
			
			facade.registerMediator(new ImageMediator(image));
			_imageMediator = facade.retrieveMediator(ImageMediator.NAME) as ImageMediator;
			
			component.showImage(image);
			
			sendNotification(Notifications.RUN_EFFECT_1);
		}
		
		private function showSampleImage ():void {
			component.destroyImage();
			facade.removeMediator(ImageMediator.NAME);
			
			var image:Image = new Image(_imageDataProxy.getData().bitmapData);
			image.setCenter(_samplerListProxy.selected.center);
			image.setStartRadius(_samplerListProxy.selected.startRadius);
			
			facade.registerMediator(new ImageMediator(image));
			_imageMediator = facade.retrieveMediator(ImageMediator.NAME) as ImageMediator;
			
			component.showImage(image);
			
			//update sampler information with the loaded bitmapData
			_samplerListProxy.updateSelected(_imageDataProxy.getData().bitmapData);
			
			sendNotification(Notifications.RUN_EFFECT_1);
		}
		
		private function showLoadedImage ():void {
			
			component.destroyImage();
			facade.removeMediator(ImageMediator.NAME);
			
			var image:Image = new Image(_imageDataProxy.getData().bitmapData);
			
			facade.registerMediator(new ImageMediator(image));
			_imageMediator = facade.retrieveMediator(ImageMediator.NAME) as ImageMediator;
			
			component.showImage(image);
		}
		private function showCameraImage ():void {
			
			if (_imageMediator && _imageMediator.component.showingCamera()) return;
			
			component.destroyImage();
			facade.removeMediator(ImageMediator.NAME);
			
			var image:Image = new Image();
			
			facade.registerMediator(new ImageMediator(image));
			_imageMediator = facade.retrieveMediator(ImageMediator.NAME) as ImageMediator;
			
			component.showCamera(image);
			
		}
		
		private function showSnapShot ():void {
			_imageDataProxy.loadFromCamera(_imageMediator.component.bitmapData);
		}
		private function get component():ImageViewer{
			return viewComponent as ImageViewer;
		}
		
		private function initListeners ():void {
			component.addEventListener(CropEvent.CROP, onCropDone, false, 0, true);
			component.addEventListener(ImageViewer.RESIZE, onResizeImage, false, 0, true);
			component.addEventListener(CenterEvent.CENTER, onCenterDone, false, 0, true);
		}
		
		private function onCropDone (event:CropEvent):void {
			sendNotification(Notifications.IMAGE_UPDATED);
			sendNotification(Notifications.CROP_IMAGE, event.getCropRectangle());
		}
		private function onResizeImage (event:Event):void {
			sendNotification(Notifications.IMAGE_UPDATED);
			sendNotification(Notifications.RESIZE_IMAGE);
		}
		
		private function onCenterDone (event:CenterEvent):void {
			sendNotification(Notifications.SET_IMAGE_CENTER, event);
		}
	}
}

The ImageMediator

for the image sprite

This Mediator will be responsible for all updates done to the Image component. The Notifications it is interested in are:

Notifications.CROP_IMAGE: This notification sends as the Note body a Rectangle object, this rectangle marks the cropped area.


Notifications.RESIZE_IMAGE: With this the Image redraws itself using the current scaleX and scaleY and then sets its new scaleX and scaleY to 1.


Notifications.SET_IMAGE_CENTER: This notification sends as the Note body a CenterEvent object, which holds the center radius picked by the user and the center Point object.


Notifications.RUN_EFFECT_1: Image runs effect 1.


Notifications.RUN_EFFECT_2: Image runs effect 2.


Notifications.RUN_EFFECT_3: Image runs effect 2 with axis rotation turned on.


Notifications.NEW_SAMPLER: When the image is formatted and a center point is picked, the current bitmapdata, the image's url, the center point and the start radius are stored as a new SAMPLER object and added to the SamplerListProxy. Now the user can return to this image by clicking on its Sampler button in the SamplerNav.


Notifications.UNDO_IMAGE_STEP_1: This means last UNDO possible, it simply has to send the notification that an image has been loaded, and the whole process starts again with the original bitmapdata currently stored in ImageDataProxy. One Notification and all is taken care of.


Notifications.UNDO_IMAGE_STEP_2: This undo clears any information about the Center point. Although the user is currently on STEP 2, any cropping and resizing will only be UNDO if user decides to refresh the image.


Notifications.UNDO_IMAGE_STEP_3: Here the user is ready to run the effect, so this UNDO clears the Center point, and some other Mediator will send the user back to STEP 3 where a new Center will be picked.

And this is it. Feel free to explore the actual Components. I hope to do a tutorial of a simple Game built in PureMVC so you might see how that can be done. I trust that this tutorial at least made it clear how helpful PureMVC can be when dealing with buttons and states.

Code for ImageMediator

package nose3d.view {
	import flash.geom.Rectangle;
	import flash.geom.Point;
	import nose3d.Notifications;
	import nose3d.view.component.Image;
	import nose3d.model.SamplerListProxy;
	import nose3d.model.ImageDataProxy;
	import nose3d.dto.Sampler;
	import org.puremvc.as3.interfaces.*;
	import org.puremvc.as3.patterns.mediator.Mediator;

	public class ImageMediator extends Mediator implements IMediator {
		public static const NAME:String = "ImageMediator";
		
		private var _samplerListProxy:SamplerListProxy;
		private var _imageDataProxy:ImageDataProxy;
		public function ImageMediator (viewComponent:Object) {
			super( NAME, viewComponent );
			_samplerListProxy = facade.retrieveProxy (SamplerListProxy.NAME) as SamplerListProxy;
			_imageDataProxy = facade.retrieveProxy (ImageDataProxy.NAME) as ImageDataProxy;
		}
		override public function listNotificationInterests():Array {
			return [ 
					Notifications.CROP_IMAGE,
					Notifications.RESIZE_IMAGE,
					Notifications.SET_IMAGE_CENTER,
					Notifications.RUN_EFFECT_1,
					Notifications.RUN_EFFECT_2,
					Notifications.RUN_EFFECT_3,
					Notifications.NEW_SAMPLER,
					Notifications.UNDO_IMAGE_STEP_1,
					Notifications.UNDO_IMAGE_STEP_2,
					Notifications.UNDO_IMAGE_STEP_3
					
				   ];
		}

	   override public function handleNotification(note:INotification):void {
			switch (note.getName()) {
				
				case Notifications.CROP_IMAGE:     	
					component.crop(note.getBody() as Rectangle);
					sendNotification(Notifications.IMAGE_UPDATED);
					break;
				case Notifications.RESIZE_IMAGE:     	
					component.resize();
					sendNotification(Notifications.IMAGE_UPDATED);
					break;
				case Notifications.SET_IMAGE_CENTER:     	
					component.setCenter(note.getBody().getCenterPoint());
					component.setStartRadius(note.getBody().getCenterRadius());
					break;
				case Notifications.RUN_EFFECT_1: 
					component.runEffect1();
					break;
				case Notifications.RUN_EFFECT_2:  
					component.runEffect2(false);
					break;
				case Notifications.RUN_EFFECT_3:     	
					component.runEffect2(true);
					break;
				case Notifications.NEW_SAMPLER: 
					_samplerListProxy.addSampler(
							new Sampler(
									_imageDataProxy.getData().url,
									component.center,
									component.startRadius,
									component.bitmapData)
												);
					break;
				case Notifications.UNDO_IMAGE_STEP_1: 
					//****** revert to originally loaded image  *********
					sendNotification(Notifications.IMAGE_LOADED);
					break;
				case Notifications.UNDO_IMAGE_STEP_2: 
					//****** Image already cropped and resized, clear center info *********
					component.setCenter(null);
					component.setStartRadius(0);
					component.update();
					sendNotification(Notifications.IMAGE_UPDATED);
					
					break;
				case Notifications.UNDO_IMAGE_STEP_3:     	
					//****** Image already cropped and resized, clear center info *********
					component.setCenter(null);
					component.setStartRadius(0);
					component.update();
					sendNotification(Notifications.IMAGE_UPDATED);
					break;
			}
		}
		
		public function get component():Image{
			return viewComponent as Image;
		}
		
	}
}