EladElrom.com

Deep Dive Into Technology

Small utility class to handle Flex, ActionScript keyboard operations such as Copy, Paste, Cut, Undo, Redo etc

Here’s a quick small utility class that you can use as a starting point to build a keyboard tracker to handle Copy, Paste, Cut, Undo, Redo etc operations from the user.

package utils
{
	public final class KeyBoardCombination
	{
		public static const UNDO:int  = 0;
		public static const REDO:int  = 1;
		public static const CUT:int   = 2;
		public static const PASTE:int = 3;
		public static const COPY:int  = 4;

		public static function  get getCombinationCollection():Vector.<array>
		{
			var combinationCollection:Vector.<array> = new Vector.<array>();
			combinationCollection.push( new Array( 17, 90 ) );
			combinationCollection.push( new Array( 17, 16, 90 ) );
			combinationCollection.push( new Array( 17, 88 ) );
			combinationCollection.push( new Array( 17, 86 ) );
			combinationCollection.push( new Array(17, 67) );

			return combinationCollection;
		}

		public static function checkKeyboardCombination(keyboardPressedCollection:Array):int
		{
			var retCombinationState:int = -1;
			var combinationLength:int;
			var confirmedKeys:int = 0;
			var combinationCollection:Vector.<array> = KeyBoardCombination.getCombinationCollection;

			combinationCollection.forEach(function callback(combination:Array, selectedState:int, combinationCollection:Vector.<array>):void{

				confirmedKeys = 0;

				combination.forEach( function callback(checkKey:int, index:int, array:Array):void {

					combinationLength = combination.length+1;

					keyboardPressedCollection.forEach(function callback(keyPressed:int, idx:int, arr:Array):void {

						if (keyPressed == checkKey)
							confirmedKeys++;
					});

					if (confirmedKeys == combinationLength-1)
						retCombinationState = selectedState;
				});
			});

			return retCombinationState;
		}
	}
}

Implementation looks like this:

private var keyboardCollectionPressed:Array = new Array();
			mainView.stage.addEventListener(KeyboardEvent.KEY_DOWN, function(event:KeyboardEvent):void {
				keyboardCollectionPressed.push( event.keyCode );
			} );
			mainView.stage.addEventListener(KeyboardEvent.KEY_UP, onKeyboardUp);

		private function onKeyboardUp(event:KeyboardEvent):void
		{
			var combination:int = KeyBoardCombination.checkKeyboardCombination( keyboardCollectionPressed );

			switch (combination)
			{
				case KeyBoardCombination.UNDO:
					trace("UNDO");
					break;
				case KeyBoardCombination.REDO:
					trace("REDO");
					break;
				case KeyBoardCombination.CUT:
					trace("CUT");
					break;
				case KeyBoardCombination.PASTE:
					trace("PASTE");
					break;
				case KeyBoardCombination.COPY:
					trace("COPY");
					break;
			}

			keyboardCollectionPressed = new Array();
		}

Creating a WebBrowser app using Robotlegs MVCS implementation in less than 2 min!

MVC Frameworks are notorious for requiring developers to spend most of their time working on the framework’s classes instead of the actual code. Since I posted the article about the Ant tasks for RobotLegs I was asked couple of times to show how to use the Ant tasks.

Why Ant tasks? The whole idea of the Ant tasks is to speed up your development and allow people that are not that familiar with RobotLegs to get started quickly on PC or Mac. I decided to create a video showing how I can create an AIR web browser application using RobotLegs with the MVCS implementation and try to do that in less than 2 mins, pretty bold ha?!

The application is going to be a simple web browser, see final screen shot below:

screen-shot-2010-01-29-at-11102-am

See the video tutorial here:
[SWF]http://www.eladelrom.com/blog/wp-content/uploads/RobotLegsWebBrowser.swf, 800, 800[/SWF]

In case you are having hard time viewing the video use the following URL:
http://www.screencast.com/users/eladnyc/folders/Jing/media/239bd195-4888-4570-bfea-2ddb1362ea6c

Feel free to download the Ant tasks:
http://github.com/EladElrom/robotlegs-utilities-AntGenerator

Download the WebBrowser AIR project from here.

BTW I did end up cheating and do little copy&paste and it took 2:20 seconds… but you get the point :)

RobotLegs Presentation Model implementation that access entire component events lifecycle

Last night, I was part of the RIARadio recording about RobotLegs. I have been supportive of the framework ever since I was introduced to it by Joel. We had an awesome time talking about Frameworks, OOP and RobotLegs. I raised a question to Shaun Smith and Joel Hooks about the presentation model vs Mediator and I pointed out that I haven’t seen a solution to the problem I presented on RobotLegs forums. See here. Joel challenged me to create my own implementation. I decided to accept the challenge and take a stab at it and see what I can come up with.

Problem

You cannot capture all the lifecycle events of a components ( such as PREINITIALIZE, INIT_PROGRESS, INITIALIZE & CREATION_COMPLETE) when using the mediator in Robotlegs. In addition I haven’t seen any implementation that uses the Presentation Model (PM) so you can do stuff before the component gets created or just drop an Presentation Model implementation you have done outside of the RobotLegs framework.

Under the hood

So why can’t we capture the component life cycle events in the Mediator?

The reason is that RobotLegs uses the contextView which is using the DisplayObjectContainer. The DisplayObjectContainer capture the event once the view is already added to the stage (see in 231 in MediatorMap.as)

The view gets added to the stage and than “onViewAdded” method being called and the mediator gets created. At this point it’s already too late, since most of the events to init the component have already been fired.

Meaning, even if you’ll change the mediatorBase code to the following:

		public function preRegister():void
		{
			if (flexAvailable &amp;amp;amp;amp;&amp;amp;amp;amp; (viewComponent is UIComponentClass) &amp;amp;amp;amp;&amp;amp;amp;amp; !viewComponent['initialized'])
			{
				IEventDispatcher(viewComponent).addEventListener('preinitialize', onCreationComplete, false, 0, true);
			}
			else
			{
				onRegister();
			}
		}

You cannot guarantee it will fire the pre-initialize event, and in fact when I have done some testing I found out that many times the if statement doesn’t get executed and it uses the onRegister directly.

Solution

So the options were either to modify RobotLegs entirely or try to create a more elegant solution that will use RobotLegs architecture while providing the ability to create presentation model in RobotLegs. Since I couldn’t think of a solution other than creating my own mapping system for the presentation model I started modifying RobotLegs framework, however as I was modifying the framework it hit me that there is a more elegant solution using the existing architecture.

Feel free to view and download (right click to ‘view source’) the full example from here:

screen-shot-2010-01-23-at-110559-am

Implementation

Let’s take a look at the example in detail. My entry point, main MXML application, calls the context:ApplicationContext just like any Robotlegs (MVCS) implementation. I have added component that gets attached to the view called mainView

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
			   xmlns:s="library://ns.adobe.com/flex/spark"
			   xmlns:mx="library://ns.adobe.com/flex/halo"
			   xmlns:context="com.elad.application.*"
			   minWidth="1024" minHeight="768"
			   xmlns:view="com.elad.application.view.*">

	<fx:Declarations>
		<context:ApplicationContext contextView="{this}" />
	</fx:Declarations>

	<view:MainView id="mainView" width="100%" height="100%" />

</s:Application>

Using a presentation model will allows me to easy unit test my application better, since I can create test cases just for the logic and if needed test cases for the view. Additionally, I can replace my view easily and create different views for the same logic, which will help big time with Flex 4 new architecture and Flash Catalyst.

So using presentation model such as passive view I can than split my laundry to two piles, view and presenter:

View:
1. State is in the view
2. Transitions
3. View is passive and is not aware of the Presenter

Presenter:
1. Logic is in the Presenter.
2. Presenter observes view events
3. Presenter updates the view data.
4. Presenter ‘knows’ about the components in the view.
5. Presenter holds the data or point to a data class.

In fact, I have used the exact same example I created for Flash&Flex Magazine with very small modifications. Take a look at TweetListPresenter, which holds the responsibilities that I described:

package com.elad.application.view.presenter
{
	import com.elad.application.view.TweetListView;

	import flash.events.TimerEvent;
	import flash.utils.Timer;

	import mx.events.FlexEvent;

	import org.robotlegs.mvcs.Actor;

	import utils.twitter.TwitterHelper;
	import utils.twitter.events.TwitterHelperFailureEvent;
	import utils.twitter.events.TwitterHelperSuccessEvent;
	import utils.twitter.vo.TweetVO;

	public class TweetListPresenter extends Actor
	{
		/**
		 * Corresponding view
		 */
		private var _tweetListView:TweetListView;

		/**
		 * Method to go and retireve tweets every defined number of seconds
		 *
		 * @param seconds
		 *
		 */

		public function get tweetListView():TweetListView
		{
			return _tweetListView;
		}

		public function set tweetListView(value:TweetListView):void
		{
			_tweetListView = value;
		}

		/**
		 *  Twitter helper utility class instance
		 */
		public var twitterHelper:TwitterHelper;

		/**
		 *   Holds a timer so we will be able to update list
		 */
		public var timer:Timer;

		//--------------------------------------------------------------------------
		//
		//  Default Constructor
		//
		//--------------------------------------------------------------------------

		// called right after pre-init
		public function TweetListPresenter()
		{
			_tweetListView = new TweetListView();
			_tweetListView.width = 800;
			_tweetListView.height = 400;

			_tweetListView.addEventListener( FlexEvent.PREINITIALIZE, onPreinitialize );
			_tweetListView.addEventListener( FlexEvent.INITIALIZE, onInitialize );
			_tweetListView.addEventListener( FlexEvent.CREATION_COMPLETE, onComplete );

			twitterHelper = new TwitterHelper();
			twitterHelper.addEventListener( TwitterHelperSuccessEvent.RETRIEVE_TWEETS, onRetrieveTweets );
			twitterHelper.addEventListener( TwitterHelperFailureEvent.SERVICE_FAILURE, onFaultRequest );
		}

		public function retrieveTweetsEveryFewSeconds( seconds:int ):void
		{
			timer = new Timer( seconds*1000, 100000 );
			timer.addEventListener( TimerEvent.TIMER, onTimerHandler, false, 0, true );

			timer.start();
		}

		//--------------------------------------------------------------------------
		//
		//  Handlers
		//
		//--------------------------------------------------------------------------

		private function onPreinitialize(event:FlexEvent):void
		{
			// implememt
			trace("onPreinitialize");
		}

		private function onInitialize(event:FlexEvent):void
		{
			// implememt
			trace("onInitialize");
		}

		private function onComplete(event:FlexEvent):void
		{
			trace("onComplete");
			retrieveTweetsEveryFewSeconds( 2 );
		}

		/**
		 * Method to handle a timer event
		 *
		 * @param event
		 *
		 */
		private function onTimerHandler( event:TimerEvent ):void
		{
			twitterHelper.retrieveTweetsBasedOnHashTag( "FlashAndTheCity" );
		}

		/**
		 * Handler for results
		 *
		 * @param event
		 *
		 */
		private function onRetrieveTweets( event:TwitterHelperSuccessEvent ):void
		{
			var dataProvider:Array = new Array();

			event.collection.forEach( function callback(item:TweetVO, index:int, vector:Vector.<tweetVO>):void {
				dataProvider.push( item );
			} );

			this._tweetListView.dataGrid.dataProvider = dataProvider;
		}

		/**
		 * Handler for fault
		 *
		 * @param event
		 *
		 */
		private function onFaultRequest( event:TwitterHelperFailureEvent ):void
		{
		}
	}
}

Couple of things to notice.

  • Actor – The presenter extends Actor just like any other RobotLegs actor
  • Component’s life cycle events – The default constructor creates the passive view and set the size as well as the component’s life cycle events

The view is passive and just holds the component (in our case a DataGrid):

<s:Group xmlns:fx="http://ns.adobe.com/mxml/2009"
		 xmlns:s="library://ns.adobe.com/flex/spark"
		 xmlns:mx="library://ns.adobe.com/flex/halo"
		 width="400" height="300">

	<mx:DataGrid id="dataGrid" width="500" height="400" />

</s:Group>

The next step is to create the mediator for the main view:

	public class MainViewMediator extends Mediator
	{
		[Inject]
		public var mainView:MainView;

		[Inject]
		public var tweetListPresenter:TweetListPresenter;

		override public function onRegister():void
		{
			mainView.addElement( tweetListPresenter.tweetListView );
		}
	}

Notice that I am using the DI to inject the presenter just as we do to any Actor in RobotLegs:
[Inject] public var tweetListPresenter:TweetListPresenter;

And once the main view is registered I can than add the component to the display:
mainView.addElement( tweetListPresenter.tweetListView );

Lastly, I need to map everything up in the ApplicationContext class:

		override public function startup():void
		{
			//todo: add commands
            commandMap.mapEvent(ContextEvent.STARTUP, StartupCommand, ContextEvent, true);

			// todo: Add Model
            injector.mapSingleton( TweetListPresenter );

			// todo: Add Services

			// todo: Add View
            mediatorMap.mapView(MainView, MainViewMediator);

			// Startup complete
			dispatchEvent( new ContextEvent( ContextEvent.STARTUP ) );
		}

Take a look at the folder structure:

screen-shot-2010-01-23-at-113505-am

Feel free to download the full example from here, and let me know your thoughts.

Cheers :)

Lightweight Ant script extensions generator for Robotlegs following best practices.

RobotLegs is one of the newest AS3 Micro-Architecture framework out there and to help folks that are starting with Robotlegs, as well as make life easier for current Robotlegs users, I have created Ant script extensions that generate most of the code you need in order to work with RobotLegs following best practices.

robotlegssketchsmall

The Ant tasks will generate the following:

  • Robotlegs Folder structure.
  • Robotlegs libraries and source code
  • Automatic ThunderBolt integration to each user-gesture.
  • Creating VO.
  • Add event command user gesture
  • Add startup command
  • Add main class and Context class
  • Create model class
  • Create service class
  • Create view and mediator combo
  • Move libraries and source code to project lib

You can download the Ant tasks from GitHub:
http://github.com/EladElrom/robotlegs-utilities-AntGenerator

Feel free to add these tasks to your arsenal.. Cheers :)

Utility class to help reading/writing files in Flash 10

Before the release of Flash 10 we needed to use some sort of a server side proxy or Javascript in order to read or write file in the user’s system. We would send the request to a proxy, which will handle the request and send it back to Flash once completed.

Flash Player 10 has exposed two new methods in FileReference: load and save.

The new methods allow you to read and write data right into the user’s local system. You get information about the files such as modify date, creator, size and other properties, however unlike AIR’s FileStream API the location of the files will not be visible to us and we can only do asynchronous calls.

Asynchronous operations perform operations in the background without waiting for the operation to complete, long processes such as uploading or downloading a large file will not impact the application and the user can keep using the application. Synchronous operations on the other hand, the operation is waiting for the original operation to complete before allowing the user to do any interactions with the application.

Recently, Adobe have closed a gap and forced the load and save methods to be used follow user interaction, such as clicking a button, so the user will be aware of being asked to save or load and not see a browse window comes out of nowhere.

The process of reading and writing files is easy, however I created a helper to make the process of reading and writing files even easier. The class called: LocalFileHelper.as and it uses the FileReference API.

Let’s take a look at the code that shows a simple implementation of loading and saving a file:

<fxApplication xmlns="http://ns.adobe.com/mxml/2009"
	minWidth="1024"
	minHeight="768" initialize="initializeHandler(event)">

	     <script>
          <![CDATA[
	     		import com.elad.framework.utils.events.LocalFileErrorEvent;
	     		import com.elad.framework.utils.events.LocalFileLoadedEvent;
	     		import mx.controls.Alert;
	     		import com.elad.framework.utils.events.LocalFileEvent;
	     		import com.elad.framework.utils.enum.FileTypeFormat;
	     		import com.elad.framework.utils.LocalFileHelper;
	     		import mx.events.FlexEvent;

	     		private var localFileHelper:LocalFileHelper;

	     		protected function initializeHandler(event:FlexEvent):void
	     		{
	     			localFileHelper = new LocalFileHelper( FileTypeFormat.FILE_FILTER_TEXT_TYPE );

	     			localFileHelper.addEventListener(LocalFileEvent.FILE_LOAD_BROWSE, onFileSelect);
	     			localFileHelper.addEventListener(LocalFileEvent.FILE_SAVE_BROWSE, function():void { trace("Save browse complete"); } );
	     			localFileHelper.addEventListener(LocalFileEvent.FILE_SAVE_SUCCESSFULLY, function():void { trace("Save complete!"); } );
	     			localFileHelper.addEventListener(LocalFileEvent.FILE_CANCEL, function():void { Alert.show("Cancel"); } );
	     			localFileHelper.addEventListener(LocalFileErrorEvent.FILE_ERROR, function():void { trace("file error") } );
	     		}

	     		private function loadFile():void
	     		{
	     			localFileHelper.browse();
	     		}

				private function onFileSelect(event:LocalFileEvent):void
				{
					localFileHelper.addEventListener(LocalFileLoadedEvent.DATA_LOADED, onDataLoaded );
					localFileHelper.load();
				}

				private function onDataLoaded(event:LocalFileLoadedEvent):void
				{
					output.text = LocalFileHelper.convertByteArrayToText( event.byteLoaded );
				}

				private function saveFile():void
				{
					localFileHelper.save( output.text, "test.txt" );
				}

          ]]>
     </script>

     <fxButton label="Load" click="loadFile()" />
     <fxButton label="Save" click="saveFile()"  x="84"/>
     <textArea id="output" y="37" width="397" height="327"/>

</fxApplication>

Take a look at the utility class:

package com.elad.framework.utils
{
	import com.elad.framework.utils.enum.FileTypeFormat;
	import com.elad.framework.utils.enum.InteractionStates;
	import com.elad.framework.utils.events.LocalFileErrorEvent;
	import com.elad.framework.utils.events.LocalFileEvent;
	import com.elad.framework.utils.events.LocalFileLoadedEvent;

	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IOErrorEvent;
	import flash.net.FileReference;
	import flash.utils.ByteArray;

	public class LocalFileHelper extends EventDispatcher
	{

	    //--------------------------------------------------------------------------
	    //
	    //  Variables
	    //
	    //--------------------------------------------------------------------------

		/**
		 * @private
		 *
		 */
		private var fileType:Array;

		/**
		 * @private
		 *
		 */
		private var fileReference:FileReference;

		/**
		 * @private
		 *
		 */
		private var interactionState:String;

	    //--------------------------------------------------------------------------
	    //
	    //  Constructor
	    //
	    //--------------------------------------------------------------------------

		/**
		 * Default constructor, which call the <code>reset</code> method to set the file reference and type.
		 *
		 * @param fileType	you can set the type of files you want to use.  The file types are listed in <code>FileTypeFormat</code>
		 * @see	com.elad.framework.utils.enum.FileTypeFormat
		 *
		 */
		public function LocalFileHelper(fileType:Array=null)
		{
			reset(fileType);
		}

		/**
		 * Reset method will allow the implementation to reset the file reference and set again the file type filter.
		 * The default file type is <code>FileTypeFormat.FILE_FILTER_ALL_FILES_TYPE</code> which will allow selecting
		 * any file type.
		 *
		 * @param fileType	you can set the type of files you want to use.  The file types are listed in <code>FileTypeFormat</code>
		 * @see	com.elad.framework.utils.enum.FileTypeFormat
		 *
		 */
		public function reset(fileType:Array=null):void
		{
			if (fileType == null)
			{
				fileType = FileTypeFormat.FILE_FILTER_ALL_FILES_TYPE;
			}

			this.fileType = fileType;
			fileReference = new FileReference();
		}

		/**
		 * The browse method is useful when you need to load a file, you first browse for the file and than you load the file.
		 * When you browse for the file the file type that was selected is used.
		 *
		 */
		public function browse():void
		{
			this.interactionState = InteractionStates.BROWSE;

			fileReference.addEventListener(Event.SELECT, onFileSelectEventHandler);
            fileReference.addEventListener(Event.CANCEL, onFileCancelEventHandler);

			fileReference.browse(fileType);
		}

		/**
		 * Static method to convert the <code>ByteArray</code> data into a string.
		 *
		 * @example
		 * <listing version="3.0">
		 * 	var text:String = LocalFileHelper.convertByteArrayToText( event.byteLoaded );
		 * </listing>
		 *
		 * @param data
		 * @return
		 *
		 */
		public static function convertByteArrayToText(data:ByteArray):String
		{
			var retVal:String;
			retVal = data.readUTFBytes(data.bytesAvailable);

			return retVal;
		}

		/**
		 * Method to load a file.  The method add event listeners and use the <code>fileReference.load</code>
		 * method to do the loading.  To load a file you must first call the <code>browse</code> method.
		 *
		 */
		public function load():void
		{
			if (this.interactionState != InteractionStates.BROWSE)
			{
				this.dispatchEvent( new LocalFileErrorEvent( "You must browse before trying to load a file!" ) );
			}

			this.interactionState = InteractionStates.LOAD_FILE;

            fileReference.addEventListener(Event.COMPLETE, onCompleteEventHandler);
            fileReference.addEventListener(IOErrorEvent.IO_ERROR, onIOErrorEventHandler);

            fileReference.load();
		}

		/**
		 * Save method can be used to save data into a file.  The method must follow a user interaction.
		 *
		 * @param data	Any type of data
		 * @param fileName	You can set the file name, ie: "test.txt"
		 *
		 */
		public function save(data:*, fileName:String):void
		{
			this.interactionState = InteractionStates.SAVE_FILE;

			fileReference.addEventListener(Event.COMPLETE, onCompleteEventHandler);
			fileReference.addEventListener(IOErrorEvent.IO_ERROR, onIOErrorEventHandler);

			fileReference.addEventListener(Event.SELECT, onFileSelectEventHandler);
			fileReference.addEventListener(Event.CANCEL, onFileCancelEventHandler);

			fileReference.save(data, fileName);
		}

	    //--------------------------------------------------------------------------
	    //
	    //  Event handlers
	    //
	    //--------------------------------------------------------------------------

		/**
		 * Method to handle the two cases where the browse gets calls:
		 *
		 * <ul>
		 * 	<li>Once you browse to load a file</li>
		 * 	<li>Once browse is used when saving a file</li>
		 * </ul>
		 *
		 * <p>The internal <code>InteractionStates</code> is used to know which case we are dealing with.
		 *
		 * @param event
		 *
		 */
		private function onFileSelectEventHandler(event:Event):void
		{
			fileReference.removeEventListener(Event.SELECT, onFileSelectEventHandler);
			fileReference.removeEventListener(Event.CANCEL, onFileCancelEventHandler);

			if (this.interactionState == InteractionStates.BROWSE)
				this.dispatchEvent( new LocalFileEvent( LocalFileEvent.FILE_LOAD_BROWSE ) );
			else
				this.dispatchEvent( new LocalFileEvent( LocalFileEvent.FILE_SAVE_BROWSE ) );
		}

		/**
		 * Method to be called in case the used decide to cancel the option to save or load a file.
		 * The method is related to the browse window.
		 *
		 * @param event
		 *
		 */
		private function onFileCancelEventHandler(event:Event):void
		{
			fileReference.removeEventListener(Event.SELECT, onFileSelectEventHandler);
			fileReference.removeEventListener(Event.CANCEL, onFileCancelEventHandler);

			this.dispatchEvent( new LocalFileEvent( LocalFileEvent.FILE_CANCEL ) );
		}

		/**
		 * In case an IOError gets called when trying to load or save a file this method will
		 * be dispatched.
		 *
		 * @param event
		 *
		 */
		private function onIOErrorEventHandler(event:Event):void
		{
			fileReference.removeEventListener(Event.COMPLETE, onCompleteEventHandler);
			fileReference.removeEventListener(IOErrorEvent.IO_ERROR, onIOErrorEventHandler);

			this.dispatchEvent( new LocalFileErrorEvent( "FileReference: IOErrorEvent.IO_ERROR received: "+event.toString() ) );
		}

		/**
		 * Method to handle the two cases where the complete gets calls:
		 *
		 * <ul>
		 * 	<li>Once complete loading a file</li>
		 * 	<li>Once coomplete saving a file</li>
		 * </ul>
		 *
		 * <p>The internal <code>InteractionStates</code> is used to know which case we are dealing with.
		 *
		 * @param event
		 *
		 */
		private function onCompleteEventHandler(event:Event):void
		{
			fileReference.removeEventListener(Event.COMPLETE, onCompleteEventHandler);
			fileReference.removeEventListener(IOErrorEvent.IO_ERROR, onIOErrorEventHandler);

			if (this.interactionState == InteractionStates.LOAD_FILE)
				this.dispatchEvent( new LocalFileLoadedEvent( fileReference.data ) );
			else
				this.dispatchEvent( new LocalFileEvent( LocalFileEvent.FILE_SAVE_SUCCESSFULLY ) );
		}
	}
}

Example:

  • To load a file: select the load button, which will prompt you to select a text file.
  • To save a file: type in the box and select the save button. You will be prompt to browse and select a place to save the file under.

5

Passive Multi-View design pattern – Create Flex dynamic graphical GUIs for Flash 10 using Catalyst

Currently there is a growing need, in my opinion, to create dynamic GUIs for Rich Flash applications.
Why would you need different GUIs for the same application? There are few reasons:

  • AIR/Flex application – developing an application that will be used as Flex and AIR application, sharing the same code
  • Brands – Developing different brands for the same products
  • Multi-devices application – developing an application that will be deployed on different devices such as mobile, web and desktop

Let’s talk about multi-device applications. Currently you can develop application for different OS which can be deployed on your desktop, web or mobile device (up until today UMPC and MIDs).

Adobe announced at the GSMA Mobile World Congress that Flash Player 10 will be available on Smartphones running Windows Mobile, Google’s Android, Nokia S60 / Symbian, and Palm. Combine Flash 10 availability on many devices with the release of Flash Catalyst and you can start building applications that will be served on multiple devices. Additionally, Intel announced the optimization and enabling of Adobe Flash Player 10 and Adobe AIR on ARM which creates new possibilities for creating applications using AIR for devices such as mobile phones, set-top boxes, MIDs, TVs, automotive platforms, MP3 players and many others.

Using Flash Catalyst designer can create GUI for the developer which can be served on one device, but what if you need to create the same application for two devices with different GUIs?

One is a mobile device with smaller screen, let’s say the iPhone (which Adobe already has a version of Flash for the iPhone running on emulation software) and another one will be deployed on your home computer browser with a larger screen. You can create a custom Flex component that extends UIComponent and change the sub classes once there is a need for it, however, you may want to build a completely different GUI for each device based on the device capability. Some GUIs may not have the same sub component since you want the GUI to be lightweight to accommodate mobile development.

You can use Flash Catalyst using the State design pattern or use the presentation model (see some of my previous blogs entries). Flash Catalyst allows you to work with the new design/developer separation paradigm and support states but doesn’t allow you to create multi passive views on the same project.

Take a look at the demo project deployed using the Passive Multi-View on a Nokia MID device, UMPC device as an Adobe AIR project, as well as a laptop and desktop deployment:

Adobe Flex/AIR Cross Platform Experience

The solution: I created a new design pattern that is a mix of the Passive View design pattern as well as the Factory design pattern.
With the new design pattern you can create different views. All views are using the same base high level classes, and you can combine them easily with other tools such as: Degrafa, Flex FXG, Flex 3, AS3.0 with CSS.

I used a music service called MP3 Tunes to demonstrate the new design pattern and you can view the complete application here. After you login to your account, you can select the device size and it will show you two different GUIs based on the size you selected. This is just a POC, but in real life application you can use a context class to determine the GUI to be deployed based on user’s screen size or other factors. For instance, you can create a GUI for touch screen applications, or a GUI for Playstation.

To understand the design pattern let’s take a look at the Passive View design pattern and Factory design pattern.


Passive View Design Pattern

The Passive View pattern is a derivative of the Model View Presenter (MVP), which is considered a derivative of the Model-view-controller. The Passive View pattern has some similarities to the “Code Behind” implementation, since both achieve a complete separation of ActionScript logic and MXML or AS component tags. The passive presentation model allows us to easy test our application since we can create test cases just for the logic and if needed test cases for the view
In passive view pattern we split our laundry to two piles, view and presenter:

View
* State is in the view
* View is passive and is not aware of the Presenter
Presenter
* Logic is in the Presenter.
* Presenter observes view events.
* Presenter updates the view data.
* Presenter ‘knows’ about the components in the view.
* Presenter holds the data or point to a data class.

By moving all the logic out of the view, the Passive View pattern can achieve the separation of designer and developer work and create a paradigm where it’s easy to change the view. The view class contains only the components and their states, no events, logic, changes or model. This pattern works great with Flash Catalyst since the designer responsibility is to create the view (pixel) and behavior (state) so you can copy/paste the FXG code into the application and just set the id’s of each component.

Passive View Flex UML

Factory Design Pattern
Now throw in the mix the factory design pattern and you can actually create few views for each presenter.
The factory pattern is one of the most basic creational patterns that deals with the problem of creating different products without specifying the exact concrete class that will be created. This is done by by creating a separate method for creating the product abstract class, whose subclasses can override to specify the derived type of product that will be created. The best way to describe that is to think of a pizza restaurant that holds different types of Pizza such as Mushroom pizza, Bacon pizza and other types of pizza, however, they are all pizzas that include the same ingredients such as dough, cheese and others.

Factory Design Pattern UML

Now if we mix these two design patterns together we get the following UML diagram. The Creator uses the factory pattern to find out which view (product) to use and then takes that and pushes it into the presenter. The Sub-presenter and subView can be used by the main view.

UML-multi-view-passive-view-factory

Now let’s put it to work and look at the working example with some screen shots. The application creator let you decide which view to use:

mp3tunes flex application

Once a view is selected, the presenter is provided with the view and creates the GUI:
mp3tunes flex application smaller

mp3tunes flex application larger

The screen shot is of an application I created in Illustrator, converted it to Flash Catalyst and then imported it to Flex.
Let’s take a look at the Factory Class. The class holds two constants for each product and the “AbstractMusicPlayerMain” allows us to be able to select different products that extends that class.

package com.elad.mp3tunes.view
{
	import com.elad.mp3tunes.view.mobile.MusicPlayerMain320x480;
	import com.elad.mp3tunes.view.web.MusicPlayerMain530x520;

	import flash.errors.IllegalOperationError;

	public final class MusicPlayerFactory
	{
		/**
		 * Music player types enums
		 */
		public static const WEB:int = 0;
		public static const MOBILE:int = 1;

		public static function createView(musicPlayerType:Number):AbstractMusicPlayerMain
		{
			var retVal:AbstractMusicPlayerMain;

			switch (musicPlayerType)
			{
				case WEB:
					retVal = new MusicPlayerMain530x520();
				break;
				case MOBILE:
					retVal = new MusicPlayerMain320x480();
				break;
				throw new IllegalOperationError("The view type " + musicPlayerType + " is not recognized.");
			}

			return retVal;
		}
	}
}

The abstract class holds all the components that will be used by presenter, take a look:

package com.elad.musicplayer.view
{
	import com.elad.framework.musicplayer.Player;
	import mx.components.baseClasses.FxComponent;
	import mx.components.baseClasses.FxScrollBar;
	import mx.containers.Canvas;
	import mx.controls.ProgressBar;
	import mx.graphics.graphicsClasses.TextGraphicElement;

	public class AbstractMusicPlayer extends Canvas
	{
		private var player:Player = new Player();

		// text
		public var songInfoText:TextGraphicElement;
		public var currentTimeText:TextGraphicElement;
		public var totalTimeText:TextGraphicElement;

		// Buttons
		public var playButton:FxComponent;
		public var pauseButton:FxComponent;
		public var forwardButton:FxComponent;
		public var rewindButton:FxComponent;
		public var randomButton:FxComponent;
		public var replyButton:FxComponent;
		public var artistsButton:FxComponent;
		public var albumsButton:FxComponent;

		// sliders
		public var songSlider:FxScrollBar;
		public var trackProgressBar:ProgressBar;
		public var downloadProgressBar:ProgressBar;
		public var volumeProgressBar:ProgressBar;
		public var volumeSlider:FxScrollBar;

		public function AbstractMusicPlayer()
		{
			super();
		}
	}
}

The product is each main view mxml class. Keep in mind that the MXML classes allow us to implements interfaces but not to extends a class, the closest way to extend a class is to use the abstract class in the main tag instead of let’s say the Canvas tag. Using that will use the class constructor, which typically is against how abstract is created. Once we repeat the same component they will be overriding the abstract class members;

<view:AbstractMusicPlayer xmlns="http://ns.adobe.com/mxml/2009"
	xmlns:lib="MusicPlayerSmall_library.*"
	xmlns:d="http://ns.adobe.com/fxg/2008/dt"
	xmlns:th="http://ns.adobe.com/thermo/2009"
	xmlns:ai="http://ns.adobe.com/ai/2008"
	xmlns:view="com.elad.musicplayer.view.*"
	backgroundColor="0xe6e6e6"
	width="320" height="480"
	horizontalScrollPolicy="off"
	verticalScrollPolicy="off"
	borderStyle="solid" borderThickness="3">

		<!-- Track Slider -->
		<group>
			<progressBar id="downloadProgressBar"
				left="37" top="84"
				barSkin="com.elad.musicplayer.view.mobile.components.DownloadProgressBarSkin"
				trackSkin="com.elad.musicplayer.view.mobile.components.DownloadProgressTrackSkin"
				minimum="0" maximum="100"
				labelWidth="0"
				direction="right" mode="manual" />
			<progressBar id="trackProgressBar" alpha="0.5"
				left="37" top="84"
				barSkin="com.elad.musicplayer.view.mobile.components.TrackProgressBarSkin"
				trackSkin="com.elad.musicplayer.view.mobile.components.TrackProgressTrackSkin"
				minimum="0" maximum="100"
				labelWidth="0"
				direction="right" mode="manual" />
			<fxHScrollBar id="songSlider" left="32" top="72"
				skinClass="com.elad.musicplayer.view.mobile.components.HorizontalScrollbar1"/>
		</group>

</view:AbstractMusicPlayer>

The presenter handles the logic of the main application, as well as using sub presenter and sub view if needed.

package com.elad.musicplayer.view.presenter
{
	import com.elad.mp3tunes.Music;
	import com.elad.mp3tunes.enum.SortType;
	import com.elad.mp3tunes.events.AlbumDataEvent;
	import com.elad.mp3tunes.events.ArtistsResultEvent;
	import com.elad.mp3tunes.events.TrackDataEvent;
	import com.elad.mp3tunes.vo.AlbumItemVO;
	import com.elad.mp3tunes.vo.AlbumListVO;
	import com.elad.mp3tunes.vo.ArtistItemVO;
	import com.elad.mp3tunes.vo.ArtistListVO;
	import com.elad.mp3tunes.vo.TrackItemVO;
	import com.elad.mp3tunes.vo.TrackListVO;
	import com.elad.musicplayer.view.AbstractMusicPlayerMain;
	import flash.events.Event;
	import mx.collections.ArrayCollection;
	import mx.controls.Alert;
	import mx.events.ListEvent;

	/**
	 * Presentation Pattern - Passive View
	 *
	 * @author Elad
	 *
	 */
	 [Bindable]
	public class MusicPlayerMainPresenter
	{

	    //--------------------------------------------------------------------------
	    //
	    //  Variables
	    //
	    //--------------------------------------------------------------------------
		private var music:Music = Music.getInstance();
		private var artistList:ArtistListVO = null;
		private var albumList:AlbumListVO = null;
		private var trackList:TrackListVO = null;
		private var tileResultType:String;

		// Corresponding view
		private var musicPlayerMain:AbstractMusicPlayerMain;

        // Sub-presenters
        private var musicPlayerPresenter:MusicPlayerPresenter;

	    //--------------------------------------------------------------------------
	    //
	    //  Constructor
	    //
	    //--------------------------------------------------------------------------
		public function MusicPlayerMainPresenter(musicPlayerMain:AbstractMusicPlayerMain)
		{
			this.musicPlayerMain = musicPlayerMain;
			musicPlayerPresenter = new MusicPlayerPresenter(musicPlayerMain.musicPlayer);
			musicPlayerMain.dg.addEventListener(Event.CHANGE, dgChangeHandler);
			musicPlayerMain.tileList.addEventListener(Event.CHANGE, selectTileListItem);
			musicPlayerMain.musicPlayer.addEventListener(MusicPlayerPresenter.NEXT_TRACK_EVENT, nextTrack);
			musicPlayerMain.musicPlayer.addEventListener(MusicPlayerPresenter.PREVIOUS_TRACK_EVENT, previousTrack);
			musicPlayerMain.musicPlayer.addEventListener(MusicPlayerPresenter.ARTISTS_CLICK_EVENT, getAllArtists);
			musicPlayerMain.musicPlayer.addEventListener(MusicPlayerPresenter.ALBUMS_CLICK_EVENT, getAllAlbums);
			musicPlayerMain.musicPlayer.addEventListener(MusicPlayerPresenter.PLAYING_COMPLETED, function():void { nextTrack(null); } );
			getAllArtists();
		}

	    //--------------------------------------------------------------------------
	    //
	    //  Class methods
	    //
	    //--------------------------------------------------------------------------

		private function nextTrack(event:Event):void
		{
			var trackItem:TrackItemVO = trackList.getItem(++musicPlayerMain.dg.selectedIndex);
			musicPlayerPresenter.playSong(trackItem);
		}

		private function previousTrack(event:Event):void
		{
			var trackItem:TrackItemVO = trackList.getItem(--musicPlayerMain.dg.selectedIndex);
			musicPlayerPresenter.playSong(trackItem);
		}

		private function getAllAlbums(event:Event=null):void
		{
			music.addEventListener(AlbumDataEvent.ALL_ALBUMS_DATA_COMPLETED, onAllAlbumCompleted);
			music.getAllAlbums(artistList);
		}

		private function getAllArtists(event:Event=null):void
		{
			music.addEventListener(ArtistsResultEvent.ARTIST_RESULT_COMPLETED, onArtistsResult);
			music.addEventListener(ArtistsResultEvent.ARTIST_RESULT_ERROR, function(event:ArtistsResultEvent):void { Alert.show(String(event.message)); });
			music.getMusicByArtists();
		}

	    //--------------------------------------------------------------------------
	    //
	    //  Event handlers
	    //
	    //--------------------------------------------------------------------------
		protected function selectTileListItem(event:ListEvent):void
		{
			var index:Number = event.columnIndex;
			getTrackList(index);
		}

		protected function getTrackList(index:Number):void
		{
			var id:String;
			music.addEventListener(TrackDataEvent.TRACK_DATA_COMPLETED, onTrackDataComplete);
			music.addEventListener(TrackDataEvent.TRACK_DATA_ERROR, function():void { Alert.show("Error getting track data"); });
			// based on the type in the tile list
			switch (tileResultType)
			{
				case SortType.ALBUMS:
					var albumItem:AlbumItemVO = albumList.getItem(index);
					id = albumItem.albumId;
				break
				case SortType.ARTISTS:
					var artistItem:ArtistItemVO = artistList.getItem(index);
					id = artistItem.artistId;
				break
			}
			music.getTrackData(id, tileResultType);
		}

		protected function dgChangeHandler(event:ListEvent):void
		{
			var index:Number = event.rowIndex;
			var trackItem:TrackItemVO = trackList.getItem(index);
			musicPlayerPresenter.playSong(trackItem);
		}

		private function onArtistsResult(event:ArtistsResultEvent):void
		{
			music.removeEventListener(ArtistsResultEvent.ARTIST_RESULT_COMPLETED, onArtistsResult);
			tileResultType = SortType.ARTISTS;
			artistList = new ArtistListVO(event.artistList.list.source);
			var item:ArtistItemVO = artistList.getItem(0);
			var dp:ArrayCollection = new ArrayCollection();
			for (var i:Number = 0; i<artistList.list.length; i++)
			{
				item = artistList.getItem(i);
				dp.addItem({name: item.artistName, count: item.trackCount});
			}
			musicPlayerMain.tileList.dataProvider = dp;
			musicPlayerMain.tileList.selectedIndex = 0;
			getTrackList(0);
		}

		private function onAllAlbumCompleted(event:AlbumDataEvent):void
		{
			music.removeEventListener(AlbumDataEvent.ALL_ALBUMS_DATA_COMPLETED, onAllAlbumCompleted);
			tileResultType = SortType.ALBUMS;
			albumList = new AlbumListVO(event.collection.list.source);
			var item:AlbumItemVO;
			var dp:ArrayCollection = new ArrayCollection();
			for (var i:Number = 0; i<albumList.list.length; i++)
			{
				item = albumList.getItem(i);
				dp.addItem({name: item.albumTitle, count: item.trackCount});
			}
			musicPlayerMain.tileList.dataProvider = dp;
			musicPlayerMain.tileList.selectedIndex = 0;
			getTrackList(0);
		}

		private function onAlbumDataComplete(event:AlbumDataEvent):void
		{
			music.removeEventListener(AlbumDataEvent.ALBUM_DATA_COMPLETED, onAlbumDataComplete);
			music.removeEventListener(AlbumDataEvent.ALBUM_DATA_ERROR, onAlbumDataComplete);
			var albumList:AlbumListVO = new AlbumListVO(event.collection.list.source);
			var albumId:String = albumList.getItem(0).albumId;
			music.addEventListener(TrackDataEvent.TRACK_DATA_COMPLETED, onTrackDataComplete);
			music.addEventListener(TrackDataEvent.TRACK_DATA_ERROR, function():void { Alert.show("Error getting track data"); });
			music.getTrackData(albumId);
		}

		private function onTrackDataComplete(event:TrackDataEvent):void
		{
			music.removeEventListener(AlbumDataEvent.ALBUM_DATA_COMPLETED, onAlbumDataComplete);
			music.removeEventListener(TrackDataEvent.TRACK_DATA_ERROR, function():void { Alert.show("Error getting track data"); });
			trackList = new TrackListVO(event.collection.list.source);
			// show track list
			musicPlayerMain.dg.dataProvider = trackList.list.source;
			// play first song
			var trackItem:TrackItemVO = trackList.getItem(0);
			musicPlayerPresenter.playSong(trackItem);
			// set selected song
			musicPlayerMain.dg.selectedIndex = 0;
		}
	}
}

The creator then takes both the product and the presenter and create a composition:

var musicPlayerView:AbstractMusicPlayerMain = MusicPlayerFactory.createView(type);
this.addChild(musicPlayerView);
musicPlayerMainPresenter = new MusicPlayerMainPresenter(musicPlayerView);

see complete of the creator:

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
	backgroundColor="0xe6e6e6"
	x="0" y="0"
	verticalAlign="middle"
	creationComplete="creationCompleteHandler(event)">
	<mx:Style source="assets/css/main.css" />
	<mx:Script>

		<![CDATA[
			import com.elad.musicplayer.view.presenter.MusicPlayerMainPresenter;
			import com.elad.musicplayer.view.AbstractMusicPlayerMain;
			import com.elad.musicplayer.view.LoginForm;
			import mx.managers.PopUpManager;
			import mx.containers.TitleWindow;
			import mx.events.FlexEvent;
			import com.elad.musicplayer.view.MusicPlayerFactory;
			import com.elad.musicplayer.view.AbstractMusicPlayerMain;
            private var loginForm:LoginForm;
			private var musicPlayerMainPresenter:MusicPlayerMainPresenter;
			// handler after creation complete
			protected function creationCompleteHandler(event:FlexEvent):void
			{
				loginForm = LoginForm(PopUpManager.createPopUp(this, LoginForm, true));
				loginForm.addEventListener(LoginForm.LOGIN_SUCCESSFULL, onLogin);
			}

			// method to load the view
			protected function loadView(type:Number):void
			{
				hBox.visible = false;
				hBox = null;
				var musicPlayerView:AbstractMusicPlayerMain = MusicPlayerFactory.createView(type);
				this.addChild(musicPlayerView);
				musicPlayerMainPresenter = new MusicPlayerMainPresenter(musicPlayerView);
			}

			private function onLogin(event:Event):void
			{
				hBox.visible = true;
			}
		]]>
	</mx:Script>

	<mx:HBox id="hBox" visible="false">
		<mx:Button label="320x480" click="loadView(MusicPlayerFactory.MOBILE)" />
		<mx:Button label="530x520" click="loadView(MusicPlayerFactory.WEB)" />
	</mx:HBox>

</mx:Application>

To view the complete application Click here.
Please note that you have to have an account with MP3tunes Music Locker in order to login into your account and use the application.

Adobe AIR SQLite Manager help you handle your database easily.

Working with Adobe AIR you often find yourself writing the same code over and over again to do common tasks such as:

1. Connecting to a database.
2. Execute common SQL commands such as “SELECT * FROM table”.
3. Execute custom SQL commands on tables.
4. Closing connection.

Additionally, user may erase the database or the database isn’t created yet, which requires you to check that the database exists and the table exists and if not create the table.

AIR APIs create the sql database automatically, if it doesn’t exists, but you need to handle the create table SQL command and execute it or copy a sql dabase with the table.

SQLite Manager easily manage these tasks automatically. Let’s take a look;

Our contract is pretty straight forward, we just need to ensure the connection starts and ends:

/*

     Copyright (c) 2009 Elad Elrom.  Elrom LLC. All rights reserved.

    Permission is hereby granted, free of charge, to any person
    obtaining a copy of this software and associated documentation
    files (the "Software"), to deal in the Software without
    restriction, including without limitation the rights to use,
    copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the
    Software is furnished to do so, subject to the following
    conditions:

    The above copyright notice and this permission notice shall be
    included in all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
    OTHER DEALINGS IN THE SOFTWARE.

     @author  Elad Elrom
     @contact elad.ny at gmail.com

 */
package com.elad.framework.sqlite
{
	/**
	 * Describes the contract for Objects that serve as a central point to access SQLite database.
	 *
	 * @author Elad Elrom
	 *
	 */
	public interface ISQLiteManager
	{
		function start(dbFullFileName:String, tableName:String, createTableStatement:String):void
		function close():void
	}
}

The SQLite manager handles all these common tasks. It’s a singleton to ensure we don’t open more than one connection at a time as well as avoiding placing the setting information over and over again. In case the database doesn’t exists it will be created automatically and in case the table doesn’t exists it will be generated by a SQL command.

Take a look at the SQLite manager:

/*

     Copyright (c) 2009 Elad Elrom.  Elrom LLC. All rights reserved.

    Permission is hereby granted, free of charge, to any person
    obtaining a copy of this software and associated documentation
    files (the "Software"), to deal in the Software without
    restriction, including without limitation the rights to use,
    copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the
    Software is furnished to do so, subject to the following
    conditions:

    The above copyright notice and this permission notice shall be
    included in all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
    OTHER DEALINGS IN THE SOFTWARE.

     @author  Elad Elrom
     @contact elad.ny at gmail.com

 */
package com.elad.framework.sqlite
{
	import com.elad.framework.sqlite.events.StatementSuccessEvent;

	import flash.data.SQLConnection;
	import flash.data.SQLStatement;
	import flash.errors.SQLError;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.SQLErrorEvent;
	import flash.events.SQLEvent;
	import flash.filesystem.File;

	/**
	 * <code>SQLiteManager</code> help handling the database connections and calls. You pass a start properties to the singleton and you
	 * can execute common commands or custom commands.
	 *
	 * @author elad
	 *
	 * @example The following code sets the volume level for your sound:
	 * <listing version="3.0">
	 * 		var database:SQLiteManager = SQLiteManager.getInstance();
	 * 		database.start("Users.sql3", "Users", "CREATE TABLE Users(userId VARCHAR(150) PRIMARY KEY, UserName VARCHAR(150))");
	 *
	 * 		database.addEventListener(SQLiteManager.COMMAND_EXEC_SUCCESSFULLY, onSelectResult);
	 * 		database.addEventListener(SQLiteManager.COMMAND_EXEC_FAILED, onFail);
	 *
	 * 		database.executeSelectAllCommand();
	 *
	 *		private function onSelectResult(event:StatementSuccessEvent):void
	 *		{
	 *		     var result:Array = event.results.data;
	 *		}
	 *
	 *		private function onFail(event:Event):void
	 *		{
	 *		     // handle fail
	 *		}
	 * </listing>
	 *
	 */
	public class SQLiteManager extends EventDispatcher implements ISQLiteManager
	{
		/**
		 * Database file name and extension
		 */
		public var dbFullFileName:String;

		/**
		 * Database Name
		 */
		public var tableName:String;

		/**
		 * SQL command to create the database
		 */
		public var createDbStatement:String;

		// datsbase apis instances
		protected var connection:SQLConnection;
		protected var statement:SQLStatement;
		protected var sqlFile:File;

		// repeated sql command
		protected var repeateFailCallBack:Function;
		protected var repeateCallBack:Function;
		protected var repeateSqlCommand:String = "";

		// events strings
		public static var COMMAND_EXEC_SUCCESSFULLY:String = "commandExecSuccesfully";
		public static var DATABASE_CONNECTED_SUCCESSFULLY:String = "databaseConnectedSuccessfully";
		public static var COMMAND_EXEC_FAILED:String = "commandExecFailed";
		public static var DATABASE_READY:String = "databaseReady";

		// Singleton instance.
		protected static var instance:SQLiteManager;

		/**
		 * Enforce singleton design pattern.
		 *
		 * @param enforcer
		 *
		 */
		public function SQLiteManager(enforcer:AccessRestriction)
		{
			if (enforcer == null)
				throw new Error("Error enforcer input param is undefined" );
		}

		/**
		 * Opens a database connection.
		 *
		 * @param dbFullFileName the database file name for instance: Users.sql
		 * @param tableName holds the database name, for instance: Users
		 * @param createTableStatement holds the create database statment for instance: CREATE TABLE Users(userId VARCHAR(150) PRIMARY KEY, UserName VARCHAR(150))
		 *
		 */
		public function start(dbFullFileName:String, tableName:String, createTableStatement:String):void
		{
			this.dbFullFileName = dbFullFileName;
			this.tableName = tableName;
			this.createDbStatement = createTableStatement;

			connection = new SQLConnection();
			sqlFile = File.applicationStorageDirectory.resolvePath(dbFullFileName);

			try
			{
			    connection.open(sqlFile);
			    this.dispatchEvent(new Event(DATABASE_CONNECTED_SUCCESSFULLY));
			}
			catch (error:SQLError)
			{
			    trace("Error message:", error.message);
			    trace("Details:", error.details);
			    fail();
			}
		}


		/**
		 * Close connection
		 *
		 */
		public function close():void
		{
			connection.close();
		}

		/**
		 * Test the table to ensure it exists. Sends a fail call back function to create the table if
		 * it doesn't exists.
		 *
		 */
		public function testTableExists():void
		{
			var sql:String = "SELECT * FROM "+tableName+" LIMIT 1;";
			executeCustomCommand(sql, this.onDatabaseReady, this.createTable );
		}

		/**
		 * Method to create the database table.
		 *
		 */
		private function createTable():void
		{
		    statement = new SQLStatement();
		    statement.sqlConnection = connection;
		    statement.text = createDbStatement;
		    statement.execute();

		    statement.addEventListener(SQLEvent.RESULT, onDatabaseReady);
		}

		/**
		 * Common sql command: select all entries in database
		 *
		 * @param callback
		 * @param failCallback
		 *
		 */
		public function executeSelectAllCommand(callback:Function=null, failCallback:Function=null):void
		{
			var sql:String = "SELECT * FROM "+tableName+";";
			executeCustomCommand(sql, callback, failCallback);
		}

		/**
		 * Common sql command: delete all entries in database
		 *
		 * @param callback
		 *
		 */
		public function executeDeleteAllCommand(callback:Function=null):void
		{
			var sql:String = "DELETE * FROM "+tableName+";";
			executeCustomCommand(sql, callback);
		}

		/**
		 * Method to execute a SQL command
		 *
		 * @param sql SQL command string
		 * @param callback success call back function to impliment if necessery
		 * @param failCallBack fail call back function to impliment if necessery
		 *
		 */
		public function executeCustomCommand(sql:String, callBack:Function=null, failCallBack:Function=null):void
		{
		    statement = new SQLStatement();
		    statement.sqlConnection = connection;

		    statement.text = sql;

		    if (callBack!=null)
		    {
		    	statement.addEventListener(SQLEvent.RESULT, callBack);
		    }
		    else
		    {
		    	statement.addEventListener(SQLEvent.RESULT, onStatementSuccess);
		    }

		    statement.addEventListener(SQLErrorEvent.ERROR, function():void {
		    		fail();
		    	});

			try
			{
			    statement.execute();
			}
			catch (error:SQLError)
			{
				this.handleErrors(error, sql, callBack, failCallBack);
			}
		}

		/**
		 * Utility method to clean bad characters that can break SQL commands
		 *
		 * @param str
		 * @return
		 *
		 */
		public static function removeBadCharacters(str:String):String
		{
			var retVal:String = str.split("'").join("&#8217;&rsquo;");
			return retVal;
		}

		// ------------------------------HANDLERS----------------------------

		/**
		 * Method to handle SQL command that create the dataabase.
		 * If the method was created due to a fail SQL command method checks if need to repeate any SQL command.
		 *
		 * @param event
		 *
		 */
		private function onDatabaseReady(event:Event=null):void
		{
			var evt:Event = new Event(DATABASE_READY);
			this.dispatchEvent(evt);

			if (repeateSqlCommand != "")
			{
				this.executeCustomCommand(repeateSqlCommand, repeateCallBack, repeateFailCallBack);

				repeateSqlCommand = "";
				repeateFailCallBack = null;
				repeateCallBack = null;
			}
		}

		/**
		 * Handle successful calls
		 * @param event
		 *
		 */
		private function onStatementSuccess(event:SQLEvent):void
		{
			var results:Object = statement.getResult();
			var evt:StatementSuccessEvent = new StatementSuccessEvent(COMMAND_EXEC_SUCCESSFULLY, results);
			this.dispatchEvent(evt);
		}

		/**
		 * Error handler
		 *
		 * @param error
		 * @param sql
		 * @param callBack
		 * @param failCallBack
		 *
		 */
		private function handleErrors(error:SQLError, sql:String, callBack:Function, failCallBack:Function):void
		{
			trace("Error message:", error.message);
			trace("Details:", error.details);

		    if (error.details == "no such table: '"+tableName+"'")
		    {
		    	repeateSqlCommand = sql;
		    	repeateFailCallBack = failCallBack;
		    	repeateCallBack = callBack;
		    	createTable();
		    }
		    else
		    {
			    if (failCallBack != null)
			    {
			    	failCallBack();
			    }
			    else
			    {
			    	fail();
			    }
		    }
		}

		/**
		 * Handler for fail calls
		 *
		 * @param event
		 *
		 */
		private function fail(event:Event=null):void
		{
	        var evt:Event = new Event(COMMAND_EXEC_FAILED);
	        this.dispatchEvent(evt);

	        close();
		}

		/**
		 * Method function to retrieve instance of the class
		 *
		 * @return The same instance of the class
		 *
		 */
		public static function getInstance():SQLiteManager
		{
			if( instance == null )
				instance = new  SQLiteManager(new AccessRestriction());

			return instance;
		}

	}
}

class AccessRestriction {}

Here’s an example of implementing the SQL manager;

var database:SQLiteManager = SQLiteManager.getInstance();
database.start("Users.sql3", "Users", "CREATE TABLE Users(userId VARCHAR(150) PRIMARY KEY, UserName VARCHAR(150))");

database.addEventListener(SQLiteManager.COMMAND_EXEC_SUCCESSFULLY, onSelectResult);
database.addEventListener(SQLiteManager.COMMAND_EXEC_FAILED, onFail);

database.executeSelectAllCommand();

private function onSelectResult(event:StatementSuccessEvent):void
{
     var result:Array = event.results.data;
}

private function onFail(event:Event):void
{
     // handle fail
}

And here’s a custom SQL command using custom SQL command:

var sql:String =  "INSERT INTO Users VALUES('"+userVO.userId+"','"+userVO.userName+"');";

database.addEventListener(SQLiteManager.COMMAND_EXEC_SUCCESSFULLY, onInsertSuccess);
database.executeCustomCommand(sql);

Take a look at this example. Very simple and easy to insert and read from the SQLite database.

<windowedApplication xmlns="http://ns.adobe.com/mxml/2009" layout="absolute"
	initialize="initializeHandler(event)">

	<script>
		<![CDATA[
			import mx.collections.ArrayCollection;
			import mx.events.FlexEvent;
			import com.elad.framework.sqlite.events.StatementSuccessEvent;
			import com.elad.framework.sqlite.SQLiteManager;

			private var database:SQLiteManager = SQLiteManager.getInstance();

			protected function initializeHandler(event:FlexEvent):void
			{
				database.start("Users.sql3", "Users", "CREATE TABLE Users(UserId VARCHAR(150) PRIMARY KEY, UserName VARCHAR(150))");

				database.addEventListener(SQLiteManager.COMMAND_EXEC_SUCCESSFULLY, onSelectResult);
				database.addEventListener(SQLiteManager.COMMAND_EXEC_FAILED, function():void {
					trace("fail!");
				});

				readEntries();
			}

			private function insertEntry():void
			{
				var sql:String =  "INSERT INTO Users VALUES('"+String(userId.text)+"','"+userName.text+"');";
				database.executeCustomCommand(sql);
			}

			private function readEntries():void
			{
				database.executeSelectAllCommand();
			}

			private function onSelectResult(event:StatementSuccessEvent):void
			{
				var result:Array = event.results.data;
				var rowsAffected:int = event.results.rowsAffected;

				if (rowsAffected == 1)
					readEntries();

				if (result == null)
					return;

				var len:int = result.length;
				var dp:ArrayCollection = new ArrayCollection();

				for (var i:int; i<len; i++)
				{
					dp.addItem( {UserId: result[i].UserId, UserName: result[i].UserName} );
				}

				dataGrid.dataProvider = dp;
			}

		]]>
	</script>
	<panel x="5" y="5" layout="absolute" height="356">

		<vbox horizontalScrollPolicy="off" verticalScrollPolicy="off">
			<!-- Form -->
			<form width="414">
				<formItem label="User ID:">
					<fxTextInput id="userId"/>
				</formItem>
				<formItem label="User Name:">
					<fxTextInput id="userName"/>
				</formItem>
				<formItem>
					<fxButton label="Insert Entry" click="insertEntry();"/>
				</formItem>
			</form>
		</vbox>

		<!-- Results -->
		<dataGrid x="19" y="123" id="dataGrid">
			<columns>
				<dataGridColumn headerText="User Id" dataField="UserId"/>
				<dataGridColumn headerText="User Name" dataField="UserName"/>
			</columns>
		</dataGrid>

	</panel>

</windowedApplication>

SQLite Manager example

Click here to download the sample application

FlexUnit Asynchronous tests with PureMVC Framework

There are very little resources online in term of how to use FlexUnit with PureMVC and TDD, furthermore in many cases it makes more sense to create the tests after user stories are implemented.

Let’s take the application we created for Cairngorm and convert it to PureMVC framework. This is a good exercise since it can help define the different between the two frameworks.

Creating tests with existing framework may be challenging at times. In fact, there are cases where it’s makes more sense to create the test after the code is completed rather than following common TDD practices.

TDD recommend to create the test before writing the code, however working in real life application and with exsiting frameworks you may find that sometimes it’s easier to write the code before the test, especially when you use plug-ins or code generator scripts to create your user gesture automatically. I recommend to use your own judgment and see what’s works best for you.

Take a look at the post I published yesterday:
http://eladelrom.com/blog/2008/12/29/tdd-and-asynchronous-tests-with-cairngorm-applications/

View application and download the source code:
CairngormPM and TDD

In this example I am using the same FeedsPanelViewer.mxml container I created for the Cairngorm application. However, I made few small change to the container. I decided to remove the binding properties and let the mediator handle all the changes in the container.

<mx:Panel xmlns:mx="http://www.adobe.com/2006/mxml"
     layout="vertical"
     width="600" height="450"
     styleName="panelView">

     <!-- Feeds List -->
     <mx:List
          id="feedList"
         change="dispatchEvent(new UserSelectedFeedEvent(FeedVO( feedList.selectedItem )));"
          labelField="title"/>

     <!-- Detail information -->
     <mx:VBox width="540" horizontalScrollPolicy="off" verticalScrollPolicy="off">
          <mx:Spacer height="15" />
          <mx:HBox>
               <mx:Label text="FEED DETAIL:" fontWeight="bold"/>
          </mx:HBox>
          <mx:HBox>
               <mx:Label text="author:" fontWeight="bold"/>
               <mx:Label id="author" width="100%"/>
          </mx:HBox>
     </mx:VBox>

</mx:Panel>

PureMVC Mediator:

package com.elad.TDDPureMVC.view
{
     import com.elad.TDDPureMVC.events.UserSelectedFeedEvent;
     import com.elad.TDDPureMVC.model.FeedsPanelViewerProxy;
     import com.elad.TDDPureMVC.model.vo.FeedVO;
     import com.elad.TDDPureMVC.view.components.FeedsPanelViewer;

     import org.puremvc.as3.interfaces.IMediator;
     import org.puremvc.as3.interfaces.INotification;
     import org.puremvc.as3.patterns.mediator.Mediator;

     public class FeedsPanelViewerMediator extends Mediator implements IMediator
     {
          public static const NAME:String = 'FeedsPanelViewerMediator';

          private var feedsPanelViewerProxy:FeedsPanelViewerProxy;

          public function FeedsPanelViewerMediator(viewComponent:Object=null)
          {
               super(NAME, viewComponent);
               feedsPanelViewer.addEventListener(UserSelectedFeedEvent.USERSELECTEDFEED_EVENT, changeSelectedFeed);
          }

          private function changeSelectedFeed(event:UserSelectedFeedEvent):void
          {
               setDetail(event.selectedFeed);
          }

          public function get feedsPanelViewer():FeedsPanelViewer
          {
               return viewComponent as FeedsPanelViewer;
          }

          override public function listNotificationInterests():Array
          {
               return [
                    FeedsPanelViewerProxy.READ_ADOBE_FEEDS_SUCCESS,
                       ];
          }

          private function setDetail(feed:FeedVO):void
          {
               feedsPanelViewer.author.text = feed.author;
               feedsPanelViewer.category.text = feed.category;
               feedsPanelViewer.description.text = feed.description;
               feedsPanelViewer.link.text = feed.link;
               feedsPanelViewer.pubdate.text = feed.pubdate;
          }

          override public function handleNotification(notification:INotification):void
          {
               feedsPanelViewerProxy = facade.retrieveProxy(FeedsPanelViewerProxy.NAME) as FeedsPanelViewerProxy;

               switch ( notification.getName() )
               {
                    case FeedsPanelViewerProxy.READ_ADOBE_FEEDS_SUCCESS:
                         feedsPanelViewer.feedList.dataProvider = feedsPanelViewerProxy.feedsCollectionVO.collection;
                         setDetail(feedsPanelViewerProxy.selectedFeed);
                         feedsPanelViewer.title = feedsPanelViewerProxy.panelTitle;

                    break;
               }
          }
     }
}

Proxy:

package com.elad.TDDPureMVC.model
{
     import com.elad.TDDPureMVC.model.vo.FeedVO;
     import com.elad.TDDPureMVC.model.vo.FeedsCollectionVO;

     import mx.rpc.events.FaultEvent;
     import mx.rpc.events.ResultEvent;
     import mx.rpc.http.HTTPService;

     import org.puremvc.as3.interfaces.IProxy;
     import org.puremvc.as3.patterns.proxy.Proxy;

     public class FeedsPanelViewerProxy extends Proxy implements IProxy
     {

          public static const NAME:String = "FeedsPanelViewerProxy";
          public static const READ_ADOBE_FEEDS_SUCCESS:String = 'readAdobeFeedsSuccess';
          public static const READ_ADOBE_FEEDS_FAILED:String = 'readAdobeFeedsFailed';
          public var service:HTTPService;

          public function FeedsPanelViewerProxy()
          {
               super(NAME, new FeedsCollectionVO() );

               service = new HTTPService();
            service.url = "http://rss.adobe.com/en/resources_flex.rss";
            service.resultFormat = "e4x";
               service.addEventListener( FaultEvent.FAULT, onFault );
               service.addEventListener( ResultEvent.RESULT, onResult );
          }

          public function getAdobeFeeds():void
          {
               service.send();
          }

          // Cast data object with implicit getter
          public function get feedsCollectionVO():FeedsCollectionVO
          {
               return data.feedsCollectionVO as FeedsCollectionVO;
          }

          public function get selectedFeed():FeedVO
          {
               return (data.feedsCollectionVO as FeedsCollectionVO).collection.getItemAt(0) as FeedVO;
          }

          public function get panelTitle():String
          {
               return data.panelTitle as String;
          }

          private function onResult( result:ResultEvent ) : void
          {
               var feed:FeedVO;
               var item:Object;
               var len:int = result.result[0].channel.item.length();
               var collection:FeedsCollectionVO = new FeedsCollectionVO;
               var dataObject:Object = new Object();

               for (var i:int=0; i<len; i++)
               {
                    feed = new FeedVO();
                    item = result.result[0].channel.item[i];

                    feed.author = item.author;
                    feed.category = item.category;
                    feed.description = item.description;
                    feed.link = item.link;
                    feed.pubdate = item.pubdate;
                    feed.title = item.title;

                    collection.addItem(feed);
               }

               dataObject.feedsCollectionVO = collection;
               dataObject.panelTitle = String(result.result.*[0].*[0]);

               setData(dataObject);
               sendNotification(READ_ADOBE_FEEDS_SUCCESS);
          }

          private function onFault( event:FaultEvent) : void
          {
               sendNotification( READ_ADOBE_FEEDS_FAILED, event.fault.faultString );
          }

     }
}

To test the PureMVC framework I will be using puremvc-flexunit-testing. Our test calls the registerObserver method used for listening for a PureMVC notification on a proxy. We pass the PureMVC view, proxy and information for the AddSync such as the method to send a success response and timeout.

package flexUnitTests.proxies
{
     import com.andculture.puremvcflexunittesting.PureMVCNotificationEvent;
     import com.andculture.puremvcflexunittesting.PureMVCTestCase;
     import com.elad.TDDPureMVC.model.FeedsPanelViewerProxy;

     import flexunit.framework.Assert;

     import org.puremvc.as3.core.View;
     import org.puremvc.as3.interfaces.IView;

     public class ReadAdobeFeedsTestCase extends PureMVCTestCase
     {
          override public function setUp():void
          {
               var facade:ApplicationFacade = ApplicationFacade.getInstance();
               facade.registerProxy( new FeedsPanelViewerProxy() );
          }

          private function get proxy():FeedsPanelViewerProxy
          {
               var retVal:FeedsPanelViewerProxy = ApplicationFacade.getInstance().retrieveProxy(FeedsPanelViewerProxy.NAME) as FeedsPanelViewerProxy;
               return retVal;
          }

          private function get view():IView
          {
               return View.getInstance();
          }

          public function ReadAdobeFeedsTestCase(methodName:String=null)
          {
               super(methodName);
          }

          public function testReadAdobeFeedsEvent():void
          {
               registerObserver(this.view, this.proxy, FeedsPanelViewerProxy.READ_ADOBE_FEEDS_SUCCESS, handleResponse, 300);
               this.proxy.getAdobeFeeds();
          }

          private function handleResponse(e:PureMVCNotificationEvent):void
          {
               Assert.assertEquals("Feed title is incorrect", proxy.panelTitle, "Flex News");
          }
     }
}

TDD and Asynchronous Tests with Cairngorm applications

Model-View-Controller (MVC) frameworks such as PureMVC or Cairngorm and TDD makes a good marriage. MVC framework works very well with TDD since the application logic is separated from the front view and the data. TDD allows us to create Test Cases on the logic only, as well as create a test case for the presentation layer.

With that said, Cairngorm by default doesn’t separate the view, behavior and model in each container. In order to utilize TDD it’s recommended to use the Presentation model pattern. Few blogs back I covered Cairngorm and the presentation model so in this blog post I am going to continue by showing you how to use TDD and FlexUnit to test your user stories.

The application I am using is simple application with only three business rules:

1. Connect to adobe feeds and retrieve the latest feeds related to Flex.
2. Display these results in a list.
3. Upon user clicking an item on the list display detail information.

Take a look and download the source code:
CairngormPM and TDD

So far you can easily figure out how to create the User Story for item #1, To test Asynchronous tests in FlexUnit we can use the “addAsync” method. Here’s an example of calling the Flex News feeds.

package flexUnitTests
{
    import flexunit.framework.TestCase;

    import mx.rpc.events.ResultEvent;
    import mx.rpc.http.HTTPService;

    public class ServiceTestCase extends TestCase
    {
        private var service:HTTPService;

        public function ServiceTestCase(methodName:String=null)
        {
            super(methodName);
        }

        override public function setUp():void
        {
            service = new HTTPService();
            service.url = "http://rss.adobe.com/en/resources_flex.rss";
            service.resultFormat = "e4x";
        }

        override public function tearDown():void
        {
            service = null;
        }

        public function testServiceCall():void
        {
            service.addEventListener(ResultEvent.RESULT, addAsync(serviceResultEventHandler, 2000, {expectedResults: "Flex News"}, failFunction));
            service.send();
        }

        private function serviceResultEventHandler(event:ResultEvent, data:Object):void
        {
            var val:String = event.result[0].channel.title;
            assertTrue("Unable to retrieve Flex News feeds", val, data.expectedResults);
        }

        private function failFunction(data:Object):void
        {
            fail("Unable to connect to Flex News feeds");
        }
    }
}

Essentially, we should create a test case for each Cairngorm user gesture. So the process is as follow:

1. Understand what the purpose of the user gesture and what model get effected
2. Dispatch the event and watch changes in the model.

Additionally, it’s recommended to create FlexMonkey view tests to ensure the application view is changing according to our requirement and will test the container.

I am not going to explain the entire code, let me cover one test case and you can figure out the rest;

The ReadAdobeFeedsTestCase this test case is based on testing ReadAdobeFeedsEvent. Each Cairngorm Event-Command is based on a user gesture and you need to understand what that user gesture means.

In our case we dispatch the event and on result we place the collection in the model, so we can dispatch the event and make sure the results reaches the model. Here’s the watcher:

watcherInstance = ChangeWatcher.watch(modelLocator.feedsPanelViewerPM,["feedsCollection"],
addAsync(itemsChanged, 2000, {compareResults: 0}, failFunc));
new ReadAdobeFeedsEvent().dispatch();

And once a change is made to the model the binding method will dispatch an event automatically and we can listen to that change and compare the result in the model:

private function itemsChanged(event:Event, data:Object):void
        {
            var len:int = modelLocator.feedsPanelViewerPM.feedsCollection.collection.length;
            Assert.assertTrue("Collection is empty", len>data.compareResults);
        }

Take a look at the complete code:

package flexUnitTests.events
{
    import com.elad.application.events.ReadAdobeFeedsEvent;
    import com.elad.application.model.ModelLocator;

    import flash.events.Event;

    import flexunit.framework.Assert;
    import flexunit.framework.TestCase;

    import mx.binding.utils.ChangeWatcher;

    public class ReadAdobeFeedsTestCase extends TestCase
    {

        [Bindable]
        private var modelLocator:ModelLocator = ModelLocator.getInstance();

        private var watcherInstance:ChangeWatcher;

        public function ReadAdobeFeedsTestCase(methodName:String=null)
        {
            super(methodName);
        }

        public function testReadAdobeFeedsEvent():void
        {
            watcherInstance = ChangeWatcher.watch(modelLocator.feedsPanelViewerPM,["feedsCollection"],
                addAsync(itemsChanged, 2000, {compareResults: 0}, failFunc));

            new ReadAdobeFeedsEvent().dispatch();
        }

        private function itemsChanged(event:Event, data:Object):void
        {
            var len:int = modelLocator.feedsPanelViewerPM.feedsCollection.collection.length;
            Assert.assertTrue("Collection is empty", len>data.compareResults);
        }

        private function failFunc(data:Object):void
        {
            fail("Couldn't connect to Adobe feeds and update application model");
        }
    }
}

We have to make some changes in the TestRunner.mxml container.
The reason is that we need to add the same references as we added in our main application to the singleton classes: Front Controller and Service Locator. Otherwise the application will not be able to map between the event and commands as well as lack the ability to make service calls.

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
	xmlns:flexunit="flexunit.flexui.*"
	xmlns:business="com.elad.application.business.*"
	xmlns:control="com.elad.application.control.*"
	creationComplete="onCreationComplete()">

	<mx:Script>
		<![CDATA[
			import flexUnitTests.GesturesTestSuite;

			import flexUnitTests.events.ReadAdobeFeedsTestCase;
			import flexUnitTests.events.UserSelectedFeedTestCase;
			import flexunit.framework.TestSuite;

			private function onCreationComplete():void
			{
				testRunner.test = currentRunTestSuite();
				testRunner.startTest();
			}

			public function currentRunTestSuite():TestSuite
			{
				var testsToRun:TestSuite = new TestSuite();

				testsToRun.addTest(GesturesTestSuite.suite());
				testsToRun.addTest(new UserSelectedFeedTestCase("testUserSelectedFeedEvent"));
				testsToRun.addTest(new ReadAdobeFeedsTestCase("testReadAdobeFeedsEvent"));
				return testsToRun;
			}

		]]>
	</mx:Script>

	<control:testController />
	<business:Services />
	<flexunit:TestRunnerBase id="testRunner"/>

</mx:Application>

download the source code

I will published a post soon on creating Test Suites and Test Cases for PureMVC, which is much easier than Cairngorm applications since the presentation containers are already split from the data and logic.

Test Driven Development (TDD) best practices with FlexUnit

FlexUnit moved to Adobe Open Source and it is similar in functionality to JUnit, it allows us to create test suite and test unit. Adobe are showing their commitment to TDD as best practices for dynamic development cycles. In fact, Flex 4 has built-in menu to generate Unit Testing and Test Suite as well as plugin to view results, until Flex 4 is out here’s an example of creating a test suite and test unit using best practices in an easy to understand example.

The TDD process consists of the following steps:

  • Add test
  • Write failed unit test
  • Write code
  • Test Passed
  • Refactor
  • Repeat

First create the scaffolding with the test runner, test suite and test case than start the TDD process listed above. The complete working example can be seen here.

Let’s take a look at the classes:
the test suite holds all the test cases:

/*

 Copyright (c) 2008 Elrom LLC. All Rights Reserved

 @author   Elad Elrom
 @contact  elad.ny at gmail.com
 @project  Project Name

 @internal

 */
package flexUnitTests
{
	import flexunit.framework.TestSuite;

	public class TestSuiteClass extends TestSuite
	{
		/**
		 * Class constructor.  If you provide a contstructor in a <code>TestCase</code> subclass,
		 * you should ensure that this constructor is called.
		 *
		 * @param param	The name of the test method to be called in the test run.
		 *
		 */
		public function TestSuiteClass(param:Object=null)
		{
			super(param);
		}

		/**
		 * Holds the test to be run.
		 *
		 * @return new instance of the <code>TestSuite</code>
		 *
		 */
		public static function suite():TestSuite
		{
			var newTestSuite:TestSuite = new TestSuite();
			return newTestSuite;
		}
	}
}

The test case that holds the tests for the logic methods:

/*

 Copyright (c) 2008 Elrom LLC. All Rights Reserved

 @author   Elad Elrom
 @contact  elad.ny at gmail.com
 @project  Project Name

 @internal

 */
package flexUnitTests
{
    import com.elad.view.LogicClass;
    import flexunit.framework.TestCase;

    public class TestCaseClass extends TestCase
    {
		/**
		 * A contstructor to pass the method name.
		 *
		 * @param methodName	The name of the test method to be called in the test run.
		 *
		 */
		public function TestCaseClass(methodName:String=null)
		{
			super(methodName);
		}

		/**
		 * First test method to do something
		 *
		 */
		public function testFirstMethod():void
		{
		    var logic:LogicClass = new LogicClass();
		    assertEquals( "Expecting zero here", 0, logic.firstMethod() );
		}

		/**
		 * Second test method to do something
		 *
		 */
		public function testNextMethod():void
		{
		    var logic:LogicClass = new LogicClass();
		    assertTrue( "Expecting true here", logic.NextMethod()==50 );
		}
    }
}

And here’s our logic class:

/*

 Copyright (c) 2008 Elrom LLC. All Rights Reserved

 @author   Elad Elrom
 @contact  elad.ny at gmail.com
 @project  Project Name

 @internal

 */
package com.elad.view
{
    public class LogicClass
    {
		public function LogicClass()
		{
			// contstructor
        }

       public function firstMethod():Number
       {
             return 0;
       }

       public function NextMethod():Number
       {
       		return 50;
       }
    }
}

Last we need to have a test runner component. Create an instance of the test suite and add all the case unit we would like to run:

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
	xmlns:flexunit="flexunit.flexui.*"
	creationComplete="onCreationComplete()"
	layout="absolute">

	<mx:Script>
		<![CDATA[

			import flexUnitTests.TestCaseClass;
			import flexUnitTests.TestSuiteClass;
			import flexunit.framework.TestSuite;

			private function onCreationComplete():void
			{
				testRunner.test = currentRunTestSuite();
				testRunner.startTest();
			}

			public function currentRunTestSuite():TestSuite
			{
				var testsToRun:TestSuite = new TestSuite();

				testsToRun.addTest(TestSuiteClass.suite());
				testsToRun.addTest(new TestCaseClass("testFirstMethod"));
				testsToRun.addTest(new TestCaseClass("testNextMethod"));

				return testsToRun;
			}

		]]>
	</mx:Script>

	<flexunit:TestRunnerBase id="testRunner"/>

</mx:Application>

At this point you can add custom setUp() and tearDown() stubs as well as other test methods and test suites. The complete application can be seen and downloaded (right click ‘view source’), click the image below:

Result