Categories
Articles

Basic Parsley MVC Flash Builder Actionscript Project

This is a basic model view controller example of a Parsley Actionscript project created in Flash Builder. I posted in this blog a minimalist example of using Parsley without implementing model view controller. See Parsley Hello World For A Flash Builder Actionscript Project.. In that post I explained the messaging in Parsley and will not repeat that explanation in this post other than to point them out where used in the model view controller implementation.

This model view controller example is as basic as I can make it while best trying to show the decoupling magic in Parsley. This is accomplished using the Parsley messaging and injection. I attempted to keep the view decoupled from the controller and model. The model and the controller could be decoupled from each other. How far you go with this depends on how many messages you want to design. To keep simple I did have the controller include the model and directly call model methods. This could be decoupled with a Parsley message.

This example follows a few techniques in a referenced example found in the Spicefactory Parsley Developer Manual in chapter 1 under “Other Resources”. This example is found at BloggingLemon. It was written in July 2009. There is a live demo and you can view the source code. Unfortunately the article has no explanations. It did provide me a good template for the Parsley bootstrapping. I tried to streamline that example even further and document what I know here.

You can build this with the free Flex SDK by using the code in the src folder and be sure to include a path to the Parsley and Spicelib library. I have included the Flash Builder 4 project file here you can download.

[ad name=”Google Adsense”]
Application Class – ParsleyFramework_MVC_AS.as

This is the bootstrap application class. One of the issues in Flash using the XML Parsley configuration file are classes not tied to the view. These classes need to be compiled early so that Parsley can reflect on them. In our case the ApplicationModel and ApplicationController classes fall into this category. There are three techniques to include them for Parsley and I repeated the Parsley Developer’s Manual notes in the comments on line 35 – 39. I borrowed the first technique also used in the BloggingLemon example which is considered by some a hack. Line 41 shows including these classes in an array that has no other use in this class and seems simple enough but should be clearly documented so they are not removed as orphaned code.

Lines 53 – 60 configure the Parsley log messaging useful for debugging Parsley. Here we are suppressing the Parsley messaging so you can view the trace statements I added to help follow the application model view controller and Parsely messaging.

Lines 89 and 90 of the initApp method load the Parsley xml configuration file shown later in this post.

Lines 100 and 103 of the contextEventInitializedHandler method create Parsley managed objects for the two views in this application. The input view allows you to enter and send a message. The output view is a Flash TextField that shows all the messages in reverse entry order. Actually the ApplicationModel maintains the reverse order and the view only displays the model.

The trace statements will show you the flow of the class as it bootstraps.

package
{
	import controllers.ApplicationController;
	
	import flash.display.Sprite;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.events.Event;
	
	import models.ApplicationModel;
	
	import org.spicefactory.lib.flash.logging.Appender;
	import org.spicefactory.lib.flash.logging.FlashLogFactory;
	import org.spicefactory.lib.flash.logging.LogLevel;
	import org.spicefactory.lib.flash.logging.impl.DefaultLogFactory;
	import org.spicefactory.lib.flash.logging.impl.TraceAppender;
	import org.spicefactory.lib.logging.LogContext;
	import org.spicefactory.lib.logging.Logger;
	import org.spicefactory.parsley.core.context.Context;
	import org.spicefactory.parsley.core.events.ContextEvent;
	import org.spicefactory.parsley.flash.logging.FlashLoggingXmlSupport;
	import org.spicefactory.parsley.xml.XmlContextBuilder;
	
	import views.InputView;
	import views.OutputView;

	[SWF(frameRate="30", width="800", height="650", backgroundColor="0x666666")]	
	
	public class ParsleyFramework_MVC_AS extends Sprite
	{
		/**
		 * Hack to force compiling of classes configured in ParsleyConfiguration.xml 
		 * not used in this class so Parsley can reflect on them. 
		 * 
		 * Alternatives to this hack include either 
		 * compiling them into an SWC (with compc you can include 
		 * whole source folders into the SWC) and then include the whole SWC into your SWF 
		 * with the -include-libraries option of the mxmlc compiler.
		 * or include individual classes with the -includes option of the mxmlc compiler. 
		 * */
		protected var classImporter:Array = [ApplicationModel, ApplicationController];
		/**
		 * This app's context for Parsley.
		 * */
		protected var mainContext:Context;
		/**
		 * Application bootstrap class.
		 * */
		public function ParsleyFramework_MVC_AS()
		{
			super();
			trace("INIT: ParsleyFramework_MVC_AS()");
			var factory:FlashLogFactory = new DefaultLogFactory();
			// Spicefactory warning level for logging.
			factory.setRootLogLevel(LogLevel.WARN);
			var traceApp:Appender = new TraceAppender();
			// Suppress SpiceFactory lib tracing.
			traceApp.threshold = LogLevel.OFF;
			factory.addAppender(traceApp);
			LogContext.factory = factory;
			if (stage == null) 
			{
				addEventListener(Event.ADDED_TO_STAGE, addedToStageEventHandler);
			}
			else 
			{
				initApp();
			}
		}
		/**
		 * Handler for ADDED_TO_STAGE EVENT
		 */
		protected function addedToStageEventHandler(event:Event):void
		{
			trace("INIT: ParsleyFramework_MVC_AS.addedToStageEventHandler(...)");
			removeEventListener(Event.ADDED_TO_STAGE, addedToStageEventHandler);
			initApp();
		} 
		/**
		 * Initialize the stage and load Parsley configuration.
		 */
		protected function initApp():void
		{            
			trace("INIT: ParsleyFramework_MVC_AS.initApp()");
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
			FlashLoggingXmlSupport.initialize();
			// INITIALIZE CONTEXT
			mainContext = XmlContextBuilder.build("ParsleyConfiguration.xml");
			mainContext.addEventListener(ContextEvent.INITIALIZED, contextEventInitializedHandler); 
		}
		/**
		 * Handler for Parsley ContextEvent.INITIALIZED event.
		 */
		protected function contextEventInitializedHandler(event:ContextEvent):void
		{    
			trace("INIT: ParsleyFramework_MVC_AS.contextEventInitializedHandler(...)");
			mainContext.removeEventListener(ContextEvent.INITIALIZED, contextEventInitializedHandler);
			// Add in the views.
			var inputView:InputView = mainContext.getObjectByType(InputView) as InputView;
			addChild(inputView);  
			inputView.y = 50;
			var outputView:OutputView = mainContext.getObjectByType(OutputView) as OutputView;
			addChild(outputView);  
			outputView.y = inputView.y + inputView.height + 5;
		}        
	}
}

Parsley Configuration XML File – ParsleyConfiguration.xml
For Parsley to look for the Parsley metatags and other management tasks, it needs to know which classes to search. Loading an XML configuration file is how that is done. Here you see the model, controller and two views referenced by their package identification.

<?xml version="1.0" encoding="UTF-8"?>
<objects 
    xmlns="http://www.spicefactory.org/parsley"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.spicefactory.org/parsley 
        http://www.spicefactory.org/parsley/schema/2.3/parsley-core.xsd"
    >
     <!-- Objects managed by Parsley -->
 	<object type="models.ApplicationModel" />
 	<object type="controllers.ApplicationController" />
     
	<object type="views.InputView" />
	<object type="views.OutputView"  /> 
</objects>

[ad name=”Google Adsense”]
The Input View – InputView.as
This is an input TextField and an extended SimpleButton to take any typed message and send a Parsley message.

Line 102 is where a message is dispatched for Parsley to broadcast. Lines 37 and 38 show the Parsley injection for the messaging method.

There is no reference to a controller or a model in this view. The message on line 102 is dispatched for the chosen controller to handle. That choice is made in the controller.

package views
{
	import events.SendTextMessageEvent;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.text.TextField;
	import flash.text.TextFieldType;
	import flash.text.TextFormat;
	import flashx.textLayout.formats.TextAlign;
	import ui.simple.QuickButton;
	/**
	 * Demonstrates UI components sending messages confined to this view while
	 * using Parsley to send the SendTextMessageEvent outside for a controller to handle.
	 * */
	public class InputView extends Sprite
	{
		/**
		 * Output TextField component.
		 * */
		private var input_tf:TextField;
		/**
		 * TextFormat for input_tf.
		 * */
		private var tfFormat:TextFormat;
		/**
		 * The send button
		 * */
		private var sendButton:QuickButton;
		/**
		 * The off button
		 * */
		private var offButton:QuickButton;
		/**
		 * Parsley injected message dispatcher
		 * */
		[MessageDispatcher]
		public var dispatcher:Function;
		/**
		 * Constructor
		 * */
		public function InputView()
		{
			trace("VIEW: InputView()");
			super();
			createChildren();
		}
		/**
		 * Parsley calls automatically after context parsing.
		 */
		[Init]
		public function parsleyInit():void
		{
			trace("VIEW: InputView.parsleyInit()");
		}
		/**
		 * Build the UI for this display object.
		 */
		public function createChildren():void
		{
			trace("VIEW: InputView.createChildren()");
			// TextFormat
			tfFormat = new TextFormat();
			tfFormat.align = TextAlign.LEFT;
			tfFormat.bold = true;
			tfFormat.font = "_typewriter";
			// Input TextField.
			input_tf = new TextField();
			input_tf.border = true; 
			input_tf.multiline = false;
			input_tf.type = TextFieldType.INPUT;
			input_tf.background = true;
			input_tf.width = 600;
			input_tf.height = 20;
			input_tf.addEventListener(Event.CHANGE, input_tf_changeHandler);
			addChild(input_tf);	
			// Create QuickButton and add to display list.
			sendButton = new QuickButton("Send",60);
			sendButton.addEventListener(MouseEvent.CLICK, sendButtonClickHandler);
			addChild(sendButton);	
			input_tf.width -= sendButton.width + 10;
			sendButton.x = input_tf.x + input_tf.width + 10;
		}
		/**
		 * Handler for input_tf_changeHandler Event.CHANGE. To maintain format while changing input
		 * text.
		 * */
		private function input_tf_changeHandler(e:Event):void
		{
			//trace("VIEW: InputView.input_tf_changeHandler(...)");
			input_tf.setTextFormat(tfFormat);
		}
		/**
		 * Handler for sendButton MouseEvent.CLICK
		 * */
		private function sendButtonClickHandler(e:MouseEvent):void
		{
			trace("VIEW: InputView.sendButtonClickHandler(...)");
			// There is text to send
			if (input_tf.length > 0)
			{
				dispatcher( new SendTextMessageEvent(SendTextMessageEvent.SEND, input_tf.text) );
				input_tf.text = "";
				stage.focus = input_tf;
			}
		}
	}
}

The Output View – OutputView.as

As in the InputView there is no reference to a controller or a model. Line 68 shows using Parsley to pick up a message. Of course in a model-view-controller implementation the model should dispatch this message when the model updates.

package views
{
	import events.SendTextMessageEvent;
	import events.SentTextMessagesUpdateEvent;
	
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	import flash.text.TextField;
	import flash.text.TextFieldType;
	import flash.text.TextFormat;
	
	import flashx.textLayout.formats.TextAlign;

	/**
	 * Simple output view. Demonstrates receiving Parsley managed messages and updating from model changes.
	 * */
	public class OutputView extends Sprite
	{
		/**
		 * Output TextField component.
		 * */
		private var output_tf:TextField;
		/**
		 * TextFormat for output_tf.
		 * */
		private var tfFormat:TextFormat;
		/**
		 * Constructor
		 * */
		public function OutputView()
		{
			trace("VIEW: OutputView()");
			super();
			createChildren();
		}
		/**
		 * Parsley calls automatically after context parsing.
		 */
		[Init]
		public function parsleyInit():void
		{
			trace("VIEW: OutputView.parsleyInit()");
		}
		/**
		 * Build the UI for this display object.
		 */
		public function createChildren():void
		{
			trace("VIEW: OutputView.createChildren()");
			// TextFormat
			tfFormat = new TextFormat();
			tfFormat.align = TextAlign.LEFT;
			tfFormat.bold = true;
			tfFormat.font = "_typewriter";
			// TextField.
			output_tf = new TextField();
			output_tf.border = true; 
			output_tf.multiline = true;
			output_tf.background = true;
			output_tf.width = 600;
			output_tf.height = 100;
			addChild(output_tf);
		}
		/**
		 * Parsley event handler. Listening for SentTextMessagesUpdateEvent.UPDATE type. Shows using a selector.
		 * Other Parsley managed views can do the same.
		 */
		[MessageHandler(selector="event.SentTextMessagesUpdateEvent.UPDATED")]
		public function sentTextMessagesUpdateEventHandler(event:SentTextMessagesUpdateEvent):void
		{
			trace("VIEW: OutputView.sentTextMessagesUpdateEventHandler(...) - event.type: " + event.type);
			output_tf.text = event.sentTextMessages;
			output_tf.setTextFormat(tfFormat);
		} 
	}
}

The Model – ApplicationModel.as

On line 12 of the model the one data element, sentTextMessages, holds all the text messages sent in the application. The addSentTextMessage method is where the model is updated with new text messages and where sentTextMessages is maintained. The text messages in sentTextMessages are kept in “last in” order.

Line 30 notifies the Parsley framework of changes in sentTextMessages. The wiring we applied in the Parsley framework incudes the views and the views have handlers to receive the message. In our case it is the OutputView which simply displays the sentTextMessages value as is.

package models
{
	import events.SentTextMessagesUpdateEvent;
	/**
	 * The model responsible for application data.
	 * */
	public class ApplicationModel
	{
		/**
		 * All text messages sent separated by new line \n.
		 * */
		private var sentTextMessages:String = "";
		/**
		 * Parsley injected message dispatcher
		 * */
		[MessageDispatcher]
		public var dispatcher:Function;
		/**
		 * Updates the sentTextMessages property and broadcasts SentTextMessagesUpdateEvent.
		 * @param messageText A text message sent.
		 * */
		public function addSentTextMessage(messageText:String):void
		{
			trace("MODEL: ApplicationModel.addSentTextMessage(...)");
			if (sentTextMessages.length > 0)
			{
				messageText += "\n";
			}
			sentTextMessages = messageText + sentTextMessages;
			dispatcher( new SentTextMessagesUpdateEvent(SentTextMessagesUpdateEvent.UPDATED, sentTextMessages) );
		}
	}
}

[ad name=”Google Adsense”]
The Controller– ApplicationController.as

Lines 34 and 35 uses the Parsley messaging to listen for view messages and in this case the event.SendTextMessageEvent.SEND message. Views do not need to couple to this controller. Neither does the controller need to know anything about the views.

Line 38 shows the updating of the model. However you could replace this with Parsley messaging to decouple the controller from the model. I did not in order to reduce the number of messages for the example.

package controllers
{
	import events.SendTextMessageEvent;
	
	import models.ApplicationModel;

	/**
	 * The controller responsible for application level control.
	 * */
	public class ApplicationController
	{
		/**
		 * The model injected by Parsley.
		 * */
		[Inject]
		public var model:ApplicationModel;
		/**
		 * Parsley injected message dispatcher
		 * */
		[MessageDispatcher]
		public var dispatcher:Function;
		/**
		 * Parsley calls automatically after context parsing.
		 */
		[Init]
		public function parsleyInit():void
		{
			trace("CONTROLLER: ApplicationController.parsleyInit()");
		}
		/**
		 * Parsley event handler. Listening for SendTextMessageEvent.SEND type. Shows using a selector.
		 * Other Parsley managed views can do the same.
		 */
		[MessageHandler(selector="event.SendTextMessageEvent.SEND")]
		public function sendTextMessageEventHandler(event:SendTextMessageEvent):void
		{
			trace("CONTROLLER: ApplicationController.sendTextMessageEventHandler(...) - event.type: " + event.type);
			model.addSentTextMessage(event.messageText);
		} 
	}
}

The SendTextMessageEvent– SendTextMessageEvent.as

This is a custom Actionscript event. The purpose is to carry a new text message.

package events
{
	import flash.events.Event;
	/**
	 * Event for demonstrating Parsley. View sending a new text message.
	 * */
	public class SendTextMessageEvent extends Event
	{
		/**
		 * The event send type.
		 * */
		public static const SEND:String = "event.SendTextMessageEvent.SEND";
		/**
		 * The text sent.
		 * */
		public var messageText:String;
		/**
		 * Constructor
		 * @param messageText The text sent.
		 * */
		public function SendTextMessageEvent(type:String, messageText:String, bubbles:Boolean=false, cancelable:Boolean=false)
		{
			super(type, bubbles, cancelable);
			this.messageText = messageText;
			trace ("EVENT: SendTextMessageEvent(...) type: " + type);
		}
		override public function clone():Event
		{
			return new SendTextMessageEvent(type, messageText, bubbles, cancelable);
		}
	}
}

The SentTextMessagesUpdateEvent – SentTextMessagesUpdateEvent .as

Another custom Actionscript event. The model’s sentTextMessages property is carried in this event.

package events
{
	import flash.events.Event;
	/**
	 * Event for demonstrating Parsley with mvc. Model notification of sentTextMessages updated. 
	 * The event does not need to be specific to the model so references to the model in
	 * documentation is for helping in tracing the Parsley mvc being demonstrated.
	 * */
	public class SentTextMessagesUpdateEvent extends Event
	{
		/**
		 * The event updated type.
		 * */
		public static const UPDATED:String = "event.SentTextMessagesUpdateEvent.UPDATED";
		/**
		 * The model value of all the sent text messages.
		 * */
		public var sentTextMessages:String;
		/**
		 * Constructor
		 * @param sentTextMessages The model's value of the sentTextMessages.
		 * */
		public function SentTextMessagesUpdateEvent(type:String, sentTextMessages:String, bubbles:Boolean=false, cancelable:Boolean=false)
		{
			super(type, bubbles, cancelable);
			this.sentTextMessages = sentTextMessages;
			trace ("EVENT: SentTextMessagesUpdateEvent(...) type: " + type);
		}
		override public function clone():Event
		{
			return new SentTextMessagesUpdateEvent(type, sentTextMessages, bubbles, cancelable);
		}
	}
}

The QuickButton- QuickButton.as
This is just an Actionscript SimpleButton to use for the demo.

package ui.simple
{
	import flash.display.DisplayObject;
	import flash.display.Shape;
	import flash.display.SimpleButton;
	import flash.events.MouseEvent;
	public class QuickButton extends SimpleButton
	{
		/**
		 * The up state background color;
		 * */
		private var upColor:uint   = 0xFFCC00;
		/**
		 * The over state background color;
		 * */
		private var overColor:uint = 0xCCFF00;
		/**
		 * The down state background color;
		 * */
		private var downColor:uint = 0x00CCFF;
		/**
		 * Width.
		 * */
		private var buttonWidth:Number;;
		/**
		 * Label.
		 * */
		private var label:String;
		/**
		 * Constructor.
		 * @param label The caption for button.
		 * @param buttonWidth Width of the button. Height is 1/3 of buttonWidth
		 * */
		public function QuickButton(label:String = "Button", buttonWidth:Number = 80)
		{
			trace("UI: QuickButton() - label: " + label);
			this.label = label;
			this.buttonWidth = buttonWidth;
			downState      = new QuickButtonDisplayShape(label, downColor, buttonWidth);
			overState      = new QuickButtonDisplayShape(label, overColor, buttonWidth);
			upState        = new QuickButtonDisplayShape(label, upColor, buttonWidth);
			hitTestState   = new QuickButtonDisplayShape(label, upColor, buttonWidth);
			useHandCursor  = true;
		}
	}
}

The QuickButton Skin – QuickButtonDisplayShape.as
How I skinned the QuickButton.

package ui.simple
{
	import flash.display.Sprite;
	import flash.text.TextField;
	import flash.text.TextFieldAutoSize;
	import flash.text.TextFormat;
	import flashx.textLayout.formats.TextAlign;
	/**
	 * Rounded button with text label. Width and height and not margins or padding.
	 * */
	public class QuickButtonDisplayShape extends Sprite
	{
		/**
		 * Background color.
		 * */
		private var bgColor:uint;
		/**
		 * Width.
		 * */
		private var buttonWidth:Number;
		/**
		 * Height.
		 * */
		private var buttonHeight:Number;
		/**
		 * Label TextField component.
		 * */
		private var tf:TextField;
		/**
		 * Left padding for tf inside the button shape.
		 * */
		private const TF_LEFT_PADDING:Number = 6;
		/**
		 * Right padding for tf inside the button shape.
		 * */
		private const TF_RIGHT_PADDING:Number = 6;
		/**
		 * Ratio of button height to the buttonWidth.
		 * */
		private const BUTTON_HEIGHT_RATIO:Number = 1/3;
		/**
		 * Constructor.
		 * @param label The caption for button.
		 * @param bgColor Color for the button background.
		 * @param buttonWidth Width of the button. Height is 1/3 of buttonWidth
		 * */
		public function QuickButtonDisplayShape(label:String,bgColor:Number, buttonWidth:Number)
		{
			// Consume parameters
			this.bgColor = bgColor;
			this.buttonWidth = buttonWidth;
			this.buttonHeight = buttonWidth * BUTTON_HEIGHT_RATIO;
			// Draw button graphics.
			draw();
			// TextField for the button caption.
			tf = new TextField();
			var tfFormat:TextFormat = new TextFormat();
			tf.text = label;
			// Format for centering.
			tfFormat.align = TextAlign.CENTER;
			tfFormat.bold = true;
			tfFormat.font = "_sans";
			//tf.border = true; // Design guide for layout.
			tf.setTextFormat(tfFormat);
			// Position and size the caption.
			tf.x = TF_LEFT_PADDING;
			tf.width = buttonWidth - (TF_LEFT_PADDING + TF_RIGHT_PADDING);
			tf.height = tf.textHeight + 2;
			tf.y = Math.max(0, ( buttonHeight - (tf.textHeight + 4)) / 2);
			// Add caption.
			addChild(tf);
		}
		/**
		 * Draw graphics.
		 * */
		private function draw():void 
		{
			graphics.beginFill(bgColor);
			graphics.drawRoundRect(0, 0, buttonWidth, buttonHeight, 20, 20);
			graphics.endFill();
		}
	}
}

Categories
Articles

Parsley Hello World For A Flash Builder Actionscript Project


I had reasonable success using the SpiceFactory Parsley framework for Flex and AIR projects. I posted a basic Flex Example in this blog. I also wanted to use it for Actionscript projects in Flash Builder. This is as minimalist example as I could make. It shows how to configure the Actionscript project for Parsley, how to wire in your view and how to access the Parsley messaging framework.

[UPDATE] I posted a second example that includes a minimalist model view controller with Parsley: Basic Parsley MVC Flash Builder Actionscript Project.

The only basic example for Flash I could find is referenced from the Spicefactory Parsley Developer Manual in chapter 1 under “Other Resources”. This example is found at BloggingLemon. It was written in July 2009. There is a live demo and you can view the source code. Unfortunately the article has no explanations. It did provide me a good template for the Parsley bootstrapping.

The BloggingLemon article attempts to show using model-view-controller with Parsley. I stripped that out so you have a real basic example that, to my search efforts, is not available on the web or at the SpiceFactory web site.

You can build this with the free Flex SDK by using the code in the src folder and be sure to include a path to the Parsley and Spicelib library. I have included the Flash Builder 4 project file here you can download.

[ad name=”Google Adsense”]
Application Class – ParsleyFramework_HelloWorld_AS.as
This is the bootstrap application class. The constructor lines 33 – 40 configure the Parsley log messaging useful for debugging Parsley. Here we are suppressing the Parsley messaging so you can view the trace statements I added to help follow the application and Parsely messaging.

Lines 69 and 70 of the initApp method load the Parsley xml configuration file shown later in this post.

Lines 79 an 81 of the contextEventInitializedHandler method create Parsley managed objects for the two views in this application. The input view is two buttons labeled on and off. The output view is a Flash TextField.

The trace statements will show you the flow of the class.

package
{
	import flash.display.Sprite;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.events.Event;
	import org.spicefactory.lib.flash.logging.Appender;
	import org.spicefactory.lib.flash.logging.FlashLogFactory;
	import org.spicefactory.lib.flash.logging.LogLevel;
	import org.spicefactory.lib.flash.logging.impl.DefaultLogFactory;
	import org.spicefactory.lib.flash.logging.impl.TraceAppender;
	import org.spicefactory.lib.logging.LogContext;
	import org.spicefactory.lib.logging.Logger;
	import org.spicefactory.parsley.core.context.Context;
	import org.spicefactory.parsley.core.events.ContextEvent;
	import org.spicefactory.parsley.flash.logging.FlashLoggingXmlSupport;
	import org.spicefactory.parsley.xml.XmlContextBuilder;
	import views.InputView;
	import views.OutputView;
	[SWF(frameRate="30", width="800", height="650", backgroundColor="0x666666")]	
	public class ParsleyFramework_HelloWorld_AS extends Sprite
	{
		/**
		 * This app's context for Parsley.
		 * */
		protected var _mainContext:Context;
		/**
		 * Application bootstrap class.
		 * */
		public function ParsleyFramework_HelloWorld_AS()
		{
			super();
			var factory:FlashLogFactory = new DefaultLogFactory();
			// Spicefactory warning level for logging.
			factory.setRootLogLevel(LogLevel.WARN);
			var traceApp:Appender = new TraceAppender();
			// Suppress SpiceFactory lib tracing.
			traceApp.threshold = LogLevel.OFF;
			factory.addAppender(traceApp);
			LogContext.factory = factory;
			if (stage == null) 
			{
				addEventListener(Event.ADDED_TO_STAGE, addedToStageEventHandler);
			}
			else 
			{
				initApp();
			}
		}
		/**
		 * Handler for ADDED_TO_STAGE EVENT
		 */
		protected function addedToStageEventHandler(event:Event):void
		{
			trace("INIT: ParsleyFramework_HelloWorld01_AS.addedToStageEventHandler(...)");
			removeEventListener(Event.ADDED_TO_STAGE, addedToStageEventHandler);
			initApp();
		} 
		/**
		 * Initialize the stage and load Parsley configuration.
		 */
		protected function initApp():void
		{            
			trace("INIT: ParsleyFramework_HelloWorld01_AS.initApp()");
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
			FlashLoggingXmlSupport.initialize();
			// INITIALIZE CONTEXT
			_mainContext = XmlContextBuilder.build("ParsleyConfiguration.xml");
			_mainContext.addEventListener(ContextEvent.INITIALIZED, contextEventInitializedHandler); 
		}
		/**
		 * Handler for Parsley ContextEvent.INITIALIZED event.
		 */
		protected function contextEventInitializedHandler(event:ContextEvent):void
		{    
			trace("INIT: ParsleyFramework_HelloWorld01_AS.contextEventInitializedHandler(...)");
			_mainContext.removeEventListener(ContextEvent.INITIALIZED, contextEventInitializedHandler);
			var inputView:InputView = _mainContext.getObjectByType(InputView) as InputView;
			addChild(inputView);  
			var outputView:OutputView = _mainContext.getObjectByType(OutputView) as OutputView;
			addChild(outputView);  
			outputView.y = inputView.y + inputView.height + 5;
		}        
	}
}

Parsley Configuration XML File – ParsleyConfiguration.xml
For Parsley to look for the Parsley metatags and other management tasks, it needs to know which classes to search. Loading an XML configuration file is how that is done.

Lines 11 and 12 show how to wire in the two classes we are using.

<?xml version="1.0" encoding="UTF-8"?>
<objects 
    xmlns="http://www.spicefactory.org/parsley"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.spicefactory.org/parsley 
        http://www.spicefactory.org/parsley/schema/2.3/parsley-core.xsd"
    >
    <!-- Classes managed by Parsley -->
	<object type="views.InputView"  />
	<object type="views.OutputView"  />
</objects>

The Input View – InputView.as
This input view is two buttons labeled on and off. The UI classes for the buttons is shown later in this post.

The key items to see here are the Parsley metatags. The first is line 25 where [MessageDispatcher] defines this class as a dispatcher of Parsley messages. Line 26 follows with the name of the function for message sending. Lines 65 and 73 show this dispatcher function in action.

Line 39 shows the [Init] metatag. There may be times that you need to wait until Parsley is fully configured before adding display objects or performing other class initialization tasks. The [Init] metatag defines the function that Parsley will call when it is fully configured. An example is included here for demonstration purposes but we have no need for it other than to display a trace message for you to follow in your output console.

You can appreciate the two buttons sending their messages within the InputView and then the InputView dispatching messages to Parsley for other objects to handle. InputView is decoupled from the overall application and can be easily inserted into another application without a concern about the application messaging framework.

I made the InputView class so it would also handle the messages is gives to Parsley. You see this on lines 79, 89 and 97 with the [MessageHandler] metatags. These lines indentify functions that will act as Parsley message handlers. These are the methods that follow the [MessageHandler] metatags on lines 80, 90 and 98.

The message selection works by comparing the event in the [MessageHandler] method’s argument. A further feature can target the message using a selector. You see this with lines 89 and 89 where the event’s type becomes a filter for calling the method. You will see these event types later in the OnOffEvent class which is a common custom Actionscript event class.

package views
{
	import events.OnOffEvent;
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	import ui.simple.QuickButton;
	/**
	 * Demonstrates UI components sending messages confined to this view while
	 * using Parsley to send messages outside. This view also
	 * handles the Parsley messages it dispatches.
	 * */
	public class InputView extends Sprite
	{
		/**
		 * The on button
		 * */
		private var onButton:QuickButton;
		/**
		 * The off button
		 * */
		private var offButton:QuickButton;
		/**
		 * Parsley injected message dispatcher
		 * */
		[MessageDispatcher]
		public var dispatcher:Function;
		/**
		 * Constructor
		 * */
		public function InputView()
		{
			trace("VIEW: InputView()");
			super();
			createChildren();
		}
		/**
		 * Parsley calls automatically after context parsing.
		 */
		[Init]
		public function parsleyInit():void
		{
			trace("VIEW: InputView.parsleyInit()");
		}
		/**
		 * Build the UI for this display object.
		 */
		public function createChildren():void
		{
			trace("VIEW: InputView.createChildren()");
			// Create two QuickButtons and add to display list.
			onButton = new QuickButton("On");
			onButton.addEventListener(MouseEvent.CLICK, onButtonClickHandler);
			addChild(onButton);	
			offButton = new QuickButton("Off");
			offButton.addEventListener(MouseEvent.CLICK, offButtonClickHandler);
			offButton.x = onButton.x + onButton.width + 10;
			addChild(offButton);
		}
		/**
		 * Handler for onButton MouseEvent.CLICK
		 * */
		private function onButtonClickHandler(e:MouseEvent):void
		{
			trace("VIEW: InputView.onButtonClickHandler(...)");
			dispatcher( new OnOffEvent(OnOffEvent.ON) );
		}
		/**
		 * Handler for offButton MouseEvent.CLICK
		 * */
		private function offButtonClickHandler(e:MouseEvent):void
		{
			trace("VIEW: InputView.offButtonClickHandler(...)");
			dispatcher( new OnOffEvent(OnOffEvent.OFF) );
		}
		/**
		 * Parsley event handler. Listening for OnOffEvent all types. 
		 * Other Parsley managed views can do the same.
		 */
		[MessageHandler]
		public function offOnEventHandler(event:OnOffEvent):void
		{
			trace("VIEW: InputView.offOnEventHandler(...) - event.type: " + event.type);
		} 
		/**
		 * Parsley event handler. Listening for OnOffEvent ON type. Shows using a selector.
		 * Other Parsley managed views can do the same.
		 */
		[MessageHandler(selector="event.OnOffEvent.OFF")]
		public function offEventHandler(event:OnOffEvent):void
		{
			trace("VIEW: InputView.offEventHandler(...)");
		} 
		/**
		 * Parsley event handler. Listening for OnOffEvent all types. Shows using a selector.
		 * Other Parsley managed views can do the same.
		 */
		[MessageHandler(selector="event.OnOffEvent.ON")]
		public function onEventHandler(event:OnOffEvent):void
		{
			trace("VIEW: InputView.onEventHandler(...)");
		} 
	}
}

[ad name=”Google Adsense”]
The Output View – OutputView.as
The OutputView class receives Parsley messages in the same way the InputView class does. In fact they are both receiving the same Parsley messages on line 64, 73 and 82. The one difference is that the view is updated.

The messaging is the InputView buttons result in Parsley messages that the OutputView receives.

You also see the [Init] metatag on line 36 to further demonstrate Parsley providing its ready message to managed objects.

package views
{
	import events.OnOffEvent;
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	import flash.text.TextField;
	import flash.text.TextFieldType;
	import flash.text.TextFormat;
	import flashx.textLayout.formats.TextAlign;
	/**
	 * Simple output view. Demonstrates receiving Parsley managed messages.
	 * */
	public class OutputView extends Sprite
	{
		/**
		 * Output TextField component.
		 * */
		private var tf:TextField;
		/**
		 * TextFormat for tf.
		 * */
		private var tfFormat:TextFormat;
		/**
		 * Constructor
		 * */
		public function OutputView()
		{
			trace("VIEW: OutputView()");
			super();
			createChildren();
		}
		/**
		 * Parsley calls automatically after context parsing.
		 */
		[Init]
		public function parsleyInit():void
		{
			trace("VIEW: OutputView.parsleyInit()");
		}
		/**
		 * Build the UI for this display object.
		 */
		public function createChildren():void
		{
			trace("VIEW: OutputView.createChildren()");
			// TextFormat
			tfFormat = new TextFormat();
			tfFormat.align = TextAlign.LEFT;
			tfFormat.bold = true;
			tfFormat.font = "_typewriter";
			// TextField.
			tf = new TextField();
			tf.border = true; 
			tf.multiline = true;
			tf.background = true;
			tf.width = 600;
			tf.height = 400;
			addChild(tf);	
		}
		/**
		 * Parsley event handler. Listening for OnOffEvent all types. 
		 * Other Parsley managed views can do the same.
		 */
		[MessageHandler]
		public function offOnEventHandler(event:OnOffEvent):void
		{
			addText("VIEW: OutputView.offOnEventHandler(...) - event.type: " + event.type);
		} 
		/**
		 * Parsley event handler. Listening for OnOffEvent ON type. Shows using a selector.
		 * Other Parsley managed views can do the same.
		 */
		[MessageHandler(selector="event.OnOffEvent.OFF")]
		public function offEventHandler(event:OnOffEvent):void
		{
			addText("VIEW: OutputView.offEventHandler(...)");
		} 
		/**
		 * Parsley event handler. Listening for OnOffEvent all types. Shows using a selector.
		 * Other Parsley managed views can do the same.
		 */
		[MessageHandler(selector="event.OnOffEvent.ON")]
		public function onEventHandler(event:OnOffEvent):void
		{
			addText("VIEW: OutputView.onEventHandler(...)");
		} 
		/**
		 * Appends to tf and adds to Flash trace output.
		 * @param message Text to append
		 * */
		private function addText(message:String):void
		{
			trace(message);
			tf.appendText(message + "\n");
			tf.setTextFormat(tfFormat);
		}
	}
}

[ad name=”Google Adsense”]
The OnOffEvent – OnOffEvent.as
This is a typical Actionscript custom event class. The clone method is optional for Parsley messaging. However you might need to use the event for both Parsley and Flash messaging so it is no bother to include it for consistency.

Lines 9 and 10 show the event types. The string descriptors are used as selectors in two of the [MessageHandler] metatag methods in both the InputView and OutputView. Since these need to be hardwired in the selector for the [MessageHandler] metatag, you must manually manage any changes to the string descriptors throughout the application. An alternative is to create two separate events.

Data can be sent with the event via Parsley in the same way you are accustomed with Actionscript custom events.

package events
{
	import flash.events.Event;
	/**
	 * Event for demonstrating Parsley. Simply reports an on or off state event.
	 * */
	public class OnOffEvent extends Event
	{
		public static const ON:String = "event.OnOffEvent.ON";
		public static const OFF:String = "event.OnOffEvent.OFF";
		public function OnOffEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)
		{
			super(type, bubbles, cancelable);
			trace ("EVENT: OnOffEvent(...) type: " + type);
		}
		override public function clone():Event
		{
			return new OnOffEvent(type, bubbles, cancelable);
		}
	}
}

The QuickButton- QuickButton.as
This is just an Actionscript SimpleButton to use for the demo.

package ui.simple
{
	import flash.display.DisplayObject;
	import flash.display.Shape;
	import flash.display.SimpleButton;
	import flash.events.MouseEvent;
	public class QuickButton extends SimpleButton
	{
		/**
		 * The up state background color;
		 * */
		private var upColor:uint   = 0xFFCC00;
		/**
		 * The over state background color;
		 * */
		private var overColor:uint = 0xCCFF00;
		/**
		 * The down state background color;
		 * */
		private var downColor:uint = 0x00CCFF;
		/**
		 * Width.
		 * */
		private var buttonWidth:Number;;
		/**
		 * Label.
		 * */
		private var label:String;
		/**
		 * Constructor.
		 * @param label The caption for button.
		 * @param buttonWidth Width of the button. Height is 1/3 of buttonWidth
		 * */
		public function QuickButton(label:String = "Button", buttonWidth:Number = 80)
		{
			trace("UI: QuickButton() - label: " + label);
			this.label = label;
			this.buttonWidth = buttonWidth;
			downState      = new QuickButtonDisplayShape(label, downColor, buttonWidth);
			overState      = new QuickButtonDisplayShape(label, overColor, buttonWidth);
			upState        = new QuickButtonDisplayShape(label, upColor, buttonWidth);
			hitTestState   = new QuickButtonDisplayShape(label, upColor, buttonWidth);
			useHandCursor  = true;
		}
	}
}

The QuickButton Skin – QuickButtonDisplayShape.as
How I skinned the QuickButton.

package ui.simple
{
	import flash.display.Sprite;
	import flash.text.TextField;
	import flash.text.TextFieldAutoSize;
	import flash.text.TextFormat;
	import flashx.textLayout.formats.TextAlign;
	/**
	 * Rounded button with text label. Width and height and not margins or padding.
	 * */
	public class QuickButtonDisplayShape extends Sprite
	{
		/**
		 * Background color.
		 * */
		private var bgColor:uint;
		/**
		 * Width.
		 * */
		private var buttonWidth:Number;
		/**
		 * Height.
		 * */
		private var buttonHeight:Number;
		/**
		 * Label TextField component.
		 * */
		private var tf:TextField;
		/**
		 * Left padding for tf inside the button shape.
		 * */
		private const TF_LEFT_PADDING:Number = 6;
		/**
		 * Right padding for tf inside the button shape.
		 * */
		private const TF_RIGHT_PADDING:Number = 6;
		/**
		 * Ratio of button height to the buttonWidth.
		 * */
		private const BUTTON_HEIGHT_RATIO:Number = 1/3;
		/**
		 * Constructor.
		 * @param label The caption for button.
		 * @param bgColor Color for the button background.
		 * @param buttonWidth Width of the button. Height is 1/3 of buttonWidth
		 * */
		public function QuickButtonDisplayShape(label:String,bgColor:Number, buttonWidth:Number)
		{
			// Consume parameters
			this.bgColor = bgColor;
			this.buttonWidth = buttonWidth;
			this.buttonHeight = buttonWidth * BUTTON_HEIGHT_RATIO;
			// Draw button graphics.
			draw();
			// TextField for the button caption.
			tf = new TextField();
			var tfFormat:TextFormat = new TextFormat();
			tf.text = label;
			// Format for centering.
			tfFormat.align = TextAlign.CENTER;
			tfFormat.bold = true;
			tfFormat.font = "_sans";
			//tf.border = true; // Design guide for layout.
			tf.setTextFormat(tfFormat);
			// Position and size the caption.
			tf.x = TF_LEFT_PADDING;
			tf.width = buttonWidth - (TF_LEFT_PADDING + TF_RIGHT_PADDING);
			tf.height = tf.textHeight + 2;
			tf.y = Math.max(0, ( buttonHeight - (tf.textHeight + 4)) / 2);
			// Add caption.
			addChild(tf);
		}
		/**
		 * Draw graphics.
		 * */
		private function draw():void 
		{
			graphics.beginFill(bgColor);
			graphics.drawRoundRect(0, 0, buttonWidth, buttonHeight, 20, 20);
			graphics.endFill();
		}
	}
}

Categories
Articles

Read Flash SWF Header in AIR with Parsley Framework

I was asked to look for open source tools to read the basic information contained in a Flash swf file. Most of the items you find are for reverse engineering swf files. In the search process I found two sources to parse the swf file header.

The first was a php script written by Carlos Falo Hervá. You need to scroll down to the end of the post to find his work. This works just fine if you want to run off a server.

The second is written in Actionscript for Flash CS5. Only the author’s handle, jared, and not the author’s name is not available at the site. The download works nice and you can run from the author’s web page. You need type or paste in a url for the swf file name. The code is all dropped in on the first frame.

This site also provided a nice link to the Adobe SWF file version 10 specifications if you care to know. Page 25 has the key information on the SWF header. This is also repeated in the SWFHeaderParser parser class presented in this post.

I took the code for the second example and created a drag and drop version Adobe Air. First I separated the code into two Actionscript classes. One for parsing and one for loading swf file bytes. These classes are named respectively SWFHeaderParser and SWFHeaderLoader. I added a value object, SWFHeader_vo, to pass around the values.

I glued those items together in an Air project using the Parsley framework. This gave me a chance to use Parsley DynamicObject for the SWFHeaderLoader class that loads the SWF file for the parser.

The result is a fully decoupled version that serves as a super example of model-view-controller using Parsley for an Air app.

Download files:
Here is the Flex project if you want to explore the code and the Air installer in case you just want the tool.

[ad name=”Google Adsense”]

SWF Parser – SWFHeaderParser.as
The parser class is first as that is what you might want to integrate into your code. Note it returns the SWFHeader_vo class. The comments are scant in this class since I did write the code and only converted it into a class, returned a value object and removed code not specific to parsing the bytes. The value object is created and populated on lines 85 – 95.

package swf
{
	import flash.geom.Rectangle;
	import flash.utils.ByteArray;
	import flash.utils.Endian;
	import vo.SWFHeader_vo;
	/**
	 * SWF Header parsing. Code adapted from http://simplistika.com/parsing-reading-swf-header/.
	 * */
	public class SWFHeaderParser
	{
		private var xByte : uint;
		private var xNBits : int;
		private var xOffset : int;
		public function SWFHeaderParser()
		{
		}
		/**
		 * Get a SWFHeader_vo object from a ByteArray
		 * @param swfByteArray Any byte array, but expecting a SWF binary file or header from SWF file.
		 * @return Parsed values in SWFHeader_vo object.
		 * */
		public function parseBytes(swfByteArray : ByteArray) : SWFHeader_vo
		{
			return fParse(swfByteArray);
		}
		/**
		 * Original code from http://simplistika.com/parsing-reading-swf-header/ changed to 
		 * return SWFHeader_vo object.
		 * */
		private function fParse(v : ByteArray) : SWFHeader_vo
		{
			/*
			Field		Type 	Comment
								Signature byte
			Signature	UI8		"F" indicates uncompressed
								"C" indicates compressed (SWF 6 and later only)
			Signature	UI8		Signature byte always "W"
			Signature	UI8		Signature byte always "S"
			
			Version		UI8		Single byte file version
			FileLength	UI32	Length of entire file in bytes
			FrameSize	RECT	Frame size in twips
			FrameRate	UI16	Frame delay in 8.8 fixed number of frames per second
			FrameCount	UI16	Total number of frames in file
			The header begins with a three-byte signature of either 0×46, 0×57, 0×53 ("FWS"); or 0×43, 0×57, 0×53 (“CWS”). An FWS signature indicates an uncompressed SWF file; CWS indicates that the entire file after the first 8 bytes (that is, after the FileLength field) was compressed by using the ZLIB open standard. The data format that the ZLIB library uses is described by Request for Comments (RFCs) documents 1950 to 1952. CWS file compression is permitted in SWF 6 or later only.
			
			A one-byte version number follows the signature. 
			The version number is not an ASCII character, 
			but an 8-bit number. For example, for SWF 4, the version byte is 0×04, not the 
			ASCII character “4? (0×34). The FileLength field is the total length of the 
			SWF file, including the header. If this is an uncompressed SWF file (FWS signature), 
			the FileLength field should exactly match the file size. If this is a compressed SWF file 
			(CWS signature), the FileLength field indicates the total length of the file after decompression, 
			and thus generally does not match the file size. Having the uncompressed size available can make 
			the decompression process more efficient. The FrameSize field defines the width and height 
			of the on-screen display. This field is stored as a RECT structure, meaning that its size 
			may vary according to the number of bits needed to encode the coordinates. The FrameSize 
			RECT always has Xmin and Ymin value of 0; the Xmax and Ymax members define the width and 
			height. The FrameRate is the desired playback rate in frames per second.
			
			Source: http://simplistika.com/parsing-reading-swf-header/
			
			*/
			var vFormat : String;
			var vSwfVersion : int;
			var vFileLength : int;
			var vFrameRate : int;
			var vTotalFrames : int;
			var vFrameSize : Rectangle;
			v.endian = Endian.LITTLE_ENDIAN;
			vFormat = v.readUTFBytes(3);
			vSwfVersion = v.readByte();
			vFileLength = v.readUnsignedInt();
			v.readBytes(v);
			v.length -= 8;
			if (vFormat == "CWS")
				v.uncompress();
			v.position = 0;
			vFrameSize = new Rectangle();
			vFrameSize.left = xfReadNBits(v, true) / 20;
			vFrameSize.right = xfReadNBits(v) / 20;
			vFrameSize.top = xfReadNBits(v) / 20;
			vFrameSize.bottom = xfReadNBits(v) / 20;
			vFrameRate = v.readUnsignedByte() / 256 + v.readUnsignedByte();
			vTotalFrames = v.readUnsignedShort();
			var swfHeader_vo:SWFHeader_vo = new SWFHeader_vo();
			swfHeader_vo.format = vFormat;
			swfHeader_vo.swfVersion = vSwfVersion;
			swfHeader_vo.sizeUncompressed = vFileLength;
			swfHeader_vo.width = vFrameSize.width;
			swfHeader_vo.height =vFrameSize.height;
			swfHeader_vo.frameRate = vFrameRate;
			swfHeader_vo.totalFrames = vTotalFrames;
			return swfHeader_vo;
		}
		/**
		 * Original code from http://simplistika.com/parsing-reading-swf-header/.
		 * */
		private function xfReadNBits(v : ByteArray, vStart : Boolean = false) : uint
		{
			var n : uint;
			if (vStart)
			{
				xByte = v.readUnsignedByte();
				xNBits = xByte >> 3;
				xOffset = 3;
			}
			n = xByte << (32 - xOffset) >> (32 - xNBits);
			xOffset -= xNBits;
			while (xOffset < 0)
			{
				xByte = v.readUnsignedByte();
				n |= (xOffset < -8) ? (xByte << (-xOffset - 8)) : (xByte >> (-xOffset - 8));
				xOffset += 8;
			}
			return n;
		}
	}
}

SWF Loader – SWFHeaderLoader.as
This is my class to load the SWF and it uses Parsley messaging. If you do not intend to use Parsley, you need to add your own messaging.

Most of this is standard URLLoader coding.

The SWFHeaderParser is called on line 62. On line 63, the actual bytes are not gotten from the SWFHeaderParser. So it can be a bit misleading that that is information found in the SWF header. The SWF header only contains the uncompressed file size.

package swf
{
	import events.SWFHeaderLoaderCompleteEvent;
	import events.SWFHeaderLoaderErrorEvent;
	import events.SWFHeaderLoaderProgressEvent;
	import flash.events.Event;
	import flash.events.HTTPStatusEvent;
	import flash.events.IOErrorEvent;
	import flash.events.ProgressEvent;
	import flash.events.SecurityErrorEvent;
	import flash.net.URLLoader;
	import flash.net.URLLoaderDataFormat;
	import flash.net.URLRequest;
	import models.SWFHeaderModel;
	import vo.SWFHeader_vo;
	/**
	 * Byte loader for a SWF file.
	 * */
	public class SWFHeaderLoader 
	{
		/**
		 * Has a URLLoader
		 * */
		private var urlLoader:URLLoader;
		/**
		 * Model
		 * */
		[Inject]
		public var model:SWFHeaderModel;
		/**
		 * Receives Parsley messages.
		 * */
		[MessageDispatcher]
		public var dispatcher:Function;
		/**
		 * Load the bytes from a file.
		 * */
		public function load(url:String):void
		{
			urlLoader = new URLLoader();
			urlLoader.addEventListener(Event.COMPLETE, urlLoader_completeEventHandler);
			urlLoader.addEventListener(ProgressEvent.PROGRESS, urlLoader_progressHandler);
			urlLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, urlLoader_securityErrorHandler);
			urlLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, urlLoader_httpStatusHandler);
			urlLoader.addEventListener(IOErrorEvent.IO_ERROR, urlLoader_ioErrorHandler);
			urlLoader.dataFormat = URLLoaderDataFormat.BINARY;
			urlLoader.load( new URLRequest(url) );
		}
		/**
		 * Handler for the urlLoader ProgressEvent.PROGRESS event.
		 * */
		private function urlLoader_progressHandler(event:ProgressEvent):void 
		{
			dispatcher(new SWFHeaderLoaderProgressEvent(SWFHeaderLoaderProgressEvent.PROGRESS, event.bytesLoaded, event.bytesTotal));
		}
		/**
		 * Handler for the urlLoader Event.COMPLETE event.
		 * */
		private function urlLoader_completeEventHandler(event:Event):void
		{
			var swfHeader_vo:SWFHeader_vo = new SWFHeader_vo();
			swfHeader_vo  = new SWFHeaderParser().parseBytes(urlLoader.data);
			swfHeader_vo.sizeCompressed = urlLoader.bytesLoaded;
			dispatcher(new SWFHeaderLoaderCompleteEvent(SWFHeaderLoaderCompleteEvent.COMPLETE, swfHeader_vo));
		}
		/**
		 * Handler for the urlLoader SecurityErrorEvent.SECURITY_ERROR event.
		 * */
		private function urlLoader_securityErrorHandler(event:SecurityErrorEvent):void 
		{
			dispatcher(new SWFHeaderLoaderErrorEvent(SWFHeaderLoaderErrorEvent.SECURITY_ERROR));
		}
		/**
		 * Handler for the urlLoader HTTPStatusEvent.HTTP_STATUS event.
		 * */
		private function urlLoader_httpStatusHandler(event:HTTPStatusEvent):void 
		{
			if (event.status != 200 && event.status != 0)
			{
				dispatcher(new SWFHeaderLoaderErrorEvent(SWFHeaderLoaderErrorEvent.HTTP_ERROR));
			}
		}
		/**
		 * Handler for the urlLoader IOErrorEvent.IO_ERROR event.
		 * */
		private function urlLoader_ioErrorHandler(event:IOErrorEvent):void 
		{
			dispatcher(new SWFHeaderLoaderErrorEvent(SWFHeaderLoaderErrorEvent.IO_ERROR));
		}
	}
}

SWF Header Value Object – SWFHeader_vo.as
The value object I use in the SWFHeaderLoader and SWFHeaderParser.

package vo
{
	/**
	 * Defines the values in the SWF file header record needed for app
	 * */
	[Bindable]
	public class SWFHeader_vo
	{
		public var format:String;			//	3 characters.
											//  First character:
											//  	"F" indicates uncompressed.
											// 		"C" indicates compressed (SWF 6 and later only).
											//  Second character always "W".
											//  Third character always "S".
		public var swfVersion:Number;		//	The swf version.
		public var frameRate:Number;		//	Frame rate.
		public var totalFrames:Number;		//	Number of frames on main timeline.
		public var width:Number;			//	Stage width.
		public var height:Number;			//	Stage height.
		public var sizeUncompressed:Number;	//	File size before compression.
		public var sizeCompressed:Number;	//	Actual file size.
	}
}

[ad name=”Google Adsense”]
The remainder code listings are the wiring into a Parsley Framework and published as an Adobe Air project.

Application Class – SWFHeader.mxml

This is the application mxml file. Gotta love how Parsley helps make these application mxml files minimal.

<?xml version="1.0" encoding="utf-8"?>
<!--- 
Application container
-->
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009" 
					   xmlns:s="library://ns.adobe.com/flex/spark" 
					   xmlns:mx="library://ns.adobe.com/flex/mx"
					   xmlns:parsley="http://www.spicefactory.org/parsley" 
					   xmlns:views="views.*"
					   height = "450"
					   backgroundAlpha="0"
					   showStatusBar="false" 					>
	<fx:Declarations>
		<!-- Place non-visual elements (e.g., services, value objects) here -->
		<parsley:ContextBuilder config="ParsleyConfiguration"  />
		<parsley:Configure/>
	</fx:Declarations>
	<views:Main  />
</s:WindowedApplication>

Parsley Configuration File – ParsleyConfiguration.mxml
My first example that uses a Parsley DynamicObject. It is the SWFHeaderLoader class. I had a lot of silent failures before I could get this to stick. The result is that Parsley manages objects made from SWFHeaderLoader so we can have Parsley messaging and insertions. Only tried it with one object however.

<?xml version="1.0" encoding="utf-8"?>
<!--- 
Parsley framework configuration file
-->
<Objects 
	xmlns:fx="http://ns.adobe.com/mxml/2009"
	xmlns="http://www.spicefactory.org/parsley"
	xmlns:spicefactory="http://www.spicefactory.org/parsley"
	xmlns:models="models.*" 
	xmlns:controllers="controllers.*" 
	xmlns:s="library://ns.adobe.com/flex/spark" 
	>
	<fx:Script>
		<![CDATA[
			// Required for DynamicObject SWFHeaderLoader.
			import swf.*; 
		]]>
	</fx:Script>
	<fx:Declarations>
		<!--
		Parsley defined objects.
		-->
		<spicefactory:DynamicObject type = "{SWFHeaderLoader}" />
		<models:ApplicationModel/>
		<models:SWFHeaderModel/>
		<controllers:ApplicationController/>
	</fx:Declarations>
</Objects>

Air Application Descriptor File – SWFHeader-app.xml
Key here is that this an Air 2.0 project indicated on line 2 and line 8.

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<application xmlns="http://ns.adobe.com/air/application/2.0">

<!-- Adobe AIR Application Descriptor File Template.

	Specifies parameters for identifying, installing, and launching AIR applications.

	xmlns - The Adobe AIR namespace: http://ns.adobe.com/air/application/2.0
			The last segment of the namespace specifies the version 
			of the AIR runtime required for this application to run.
			
	minimumPatchLevel - The minimum patch level of the AIR runtime required to run 
			the application. Optional.
-->

	<!-- The application identifier string, unique to this application. Required. -->
	<id>SwfHeader</id>

	<!-- Used as the filename for the application. Required. -->
	<filename>SWFHeaderInspector</filename>

	<!-- The name that is displayed in the AIR application installer. 
	     May have multiple values for each language. See samples or xsd schema file. Optional. -->
	<name>SWF Header Inspector</name>

	<!-- An application version designator (such as "v1", "2.5", or "Alpha 1"). Required. -->
	<version>v0.01</version>

	<!-- Description, displayed in the AIR application installer.
	     May have multiple values for each language. See samples or xsd schema file. Optional. -->
	<!-- <description></description> -->

	<!-- Copyright information. Optional -->
	<!-- <copyright></copyright> -->

	<!-- Publisher ID. Used if you're updating an application created prior to 1.5.3 -->
	<!-- <publisherID></publisherID> -->

	<!-- Settings for the application's initial window. Required. -->
	<initialWindow>
		<!-- The main SWF or HTML file of the application. Required. -->
		<!-- Note: In Flash Builder, the SWF reference is set automatically. -->
		<content>[This value will be overwritten by Flash Builder in the output app.xml]</content>
		
		<!-- The title of the main window. Optional. -->
		<!-- <title></title> -->

		<!-- The type of system chrome to use (either "standard" or "none"). Optional. Default standard. -->
		<systemChrome>none</systemChrome>

		<!-- Whether the window is transparent. Only applicable when systemChrome is none. Optional. Default false. -->
		<transparent>true</transparent>

		<!-- Whether the window is initially visible. Optional. Default false. -->
		<!-- <visible></visible> -->

		<!-- Whether the user can minimize the window. Optional. Default true. -->
		<!-- <minimizable></minimizable> -->

		<!-- Whether the user can maximize the window. Optional. Default true. -->
		<!-- <maximizable></maximizable> -->

		<!-- Whether the user can resize the window. Optional. Default true. -->
		<!-- <resizable></resizable> -->

		<!-- The window's initial width. Optional. -->
		<!-- <width></width> -->

		<!-- The window's initial height. Optional. -->
		<!-- <height></height> -->

		<!-- The window's initial x position. Optional. -->
		<!-- <x></x> -->

		<!-- The window's initial y position. Optional. -->
		<!-- <y></y> -->

		<!-- The window's minimum size, specified as a width/height pair, such as "400 200". Optional. -->
		<!-- <minSize></minSize> -->

		<!-- The window's initial maximum size, specified as a width/height pair, such as "1600 1200". Optional. -->
		<!-- <maxSize></maxSize> -->
	</initialWindow>

	<!-- The subpath of the standard default installation location to use. Optional. -->
	<!-- <installFolder></installFolder> -->

	<!-- The subpath of the Programs menu to use. (Ignored on operating systems without a Programs menu.) Optional. -->
	<!-- <programMenuFolder></programMenuFolder> -->

	<!-- The icon the system uses for the application. For at least one resolution,
		 specify the path to a PNG file included in the AIR package. Optional. -->
	<!-- <icon>
		<image16x16></image16x16>
		<image32x32></image32x32>
		<image48x48></image48x48>
		<image128x128></image128x128>
	</icon> -->

	<!-- Whether the application handles the update when a user double-clicks an update version
	of the AIR file (true), or the default AIR application installer handles the update (false).
	Optional. Default false. -->
	<!-- <customUpdateUI></customUpdateUI> -->
	
	<!-- Whether the application can be launched when the user clicks a link in a web browser.
	Optional. Default false. -->
	<!-- <allowBrowserInvocation></allowBrowserInvocation> -->

	<!-- Listing of file types for which the application can register. Optional. -->
	<!-- <fileTypes> -->

		<!-- Defines one file type. Optional. -->
		<!-- <fileType> -->

			<!-- The name that the system displays for the registered file type. Required. -->
			<!-- <name></name> -->

			<!-- The extension to register. Required. -->
			<!-- <extension></extension> -->
			
			<!-- The description of the file type. Optional. -->
			<!-- <description></description> -->
			
			<!-- The MIME content type. -->
			<!-- <contentType></contentType> -->
			
			<!-- The icon to display for the file type. Optional. -->
			<!-- <icon>
				<image16x16></image16x16>
				<image32x32></image32x32>
				<image48x48></image48x48>
				<image128x128></image128x128>
			</icon> -->
			
		<!-- </fileType> -->
	<!-- </fileTypes> -->

</xml>

Main.mxml
The layout is a header and a body. The header is set up to be the move area for the window. he header is the top_hg HGroup on line 60. The body is the SWFHeaderBasic component on line 62. Here you could swap another body view easily.

<?xml version="1.0" encoding="utf-8"?>
<!--- 
Main view
-->
<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/mx" 
		 xmlns:sf="http://www.spicefactory.org/parsley"
		 
		 width="100%" height="100%"
		 xmlns:views="views.*" 
		 xmlns:ui="ui.*"
		 >
	<fx:Script>
		<![CDATA[
			import ui.ApplicationCloseButtonSkin;
			/**
			 * Called once Parsley framework has reflected.
			 * */
			[Init]
			public function parsleyInit():void
			{
				top_hg.addEventListener(MouseEvent.MOUSE_DOWN, top_hg_onMouseDown);
			}
			/**
			 * Handler for top_hg MouseEvent.MOUSE_DOWN. Handles OS window move.
			 * */
			private function top_hg_onMouseDown( evt:MouseEvent):void 
			{ 
				stage.nativeWindow.startMove(); 
			}
		]]>
	</fx:Script>
	<fx:Declarations>
		<sf:Configure/>
	</fx:Declarations>
	<fx:Style>
		#appName_lbl {
			font-weight:bold;
			color: #291F65;
			font-size:18px;
		}
		#version_lbl {
			font-weight:bold;
			color: #291F65;
			font-size:10px;
		}
	</fx:Style>
	<s:VGroup width="100%" height="100%">
	<s:BorderContainer width="100%" height="100%"
					   cornerRadius="20" borderWeight="3" 
					   borderColor="0x000000" dropShadowVisible="true"
					   backgroundColor="#858282">
		<s:VGroup  verticalAlign="middle" width="100%" height="100%"
			paddingLeft="6" paddingRight="6" paddingBottom="12" 
				>
			<!--- 
			Appplication header and drag area.
			--> 			
			<s:HGroup id = "top_hg" width="100%"  verticalAlign="middle" paddingTop="12"  >
				<s:HGroup  paddingLeft="5" verticalAlign="middle" horizontalAlign="left" width="100%"  gap="30">
					<mx:Image source="@Embed(source='../assets/Adobe-swf_icon_40x40_published.png')" />
					<s:Label id = "appName_lbl" text = "SWF Header Inspection Tool"/>
				</s:HGroup>	
				<s:HGroup  paddingRight="12" verticalAlign="middle" horizontalAlign="right" width="100%"  >
					<ui:ApplicationCloseButton  
						click="NativeApplication.nativeApplication.exit()" skinClass="ui.ApplicationCloseButtonSkin"/>
					
				</s:HGroup>			
		
			</s:HGroup>
			<views:SWFHeaderBasic    width="100%" height="100%" />	
			<s:Label id = "version_lbl" text = "Version 1.00"/>
		</s:VGroup>

	</s:BorderContainer>
	</s:VGroup>	
</s:Group>

SWFHeaderBasic.mxml
Body section for the application. You can easily plug in your own view to replace this one.

<?xml version="1.0" encoding="utf-8"?>
<!--- 
Basic view.
-->
<s:Group xmlns:fx="http://ns.adobe.com/mxml/2009" 
		 xmlns:s="library://ns.adobe.com/flex/spark" 
		 xmlns:sf="http://www.spicefactory.org/parsley"
		 xmlns:mx="library://ns.adobe.com/flex/mx" 
		 >
	<fx:Script>
		<![CDATA[
			import events.LoadSwfUrlHeaderRequestEvent;
			import models.SWFHeaderModel;
			import mx.binding.utils.BindingUtils;
			import mx.binding.utils.ChangeWatcher;
			import mx.events.FlexEvent;
			/**
			 * File name extracted from url or path.
			 * */
			[Bindable]
			private var fileName:String = '';
			[Inject]
			[Bindable]
			public var model:SWFHeaderModel;

			[MessageDispatcher]
			public var dispatcher:Function;
			/**
			 * Called once Parsley framework has reflected.
			 * */
			[Init]
			public function parsleyInit():void
			{
				// Bind the model changes from bytesLoaded in order to call a method.
				ChangeWatcher.watch(model, "bytesLoaded", updateProgressBar);
				addEventListener(NativeDragEvent.NATIVE_DRAG_ENTER, onDragEnterHandler);
				addEventListener(NativeDragEvent.NATIVE_DRAG_DROP, onDragDropHandler);
			}
			/**
			 * Handler for the load_btn Mouse.Click event.
			 * */
			protected function load_btn_clickHandler(event:MouseEvent):void
			{
				fileName = url_ti.text.substr(   url_ti.text.lastIndexOf( '/' ) + 1);
				dispatcher(new LoadSwfUrlHeaderRequestEvent (LoadSwfUrlHeaderRequestEvent.LOAD, url_ti.text));
			}
			/**
			 * Handler for the load_btn Mouse.Click event.
			 * */
			private function updateProgressBar(bytesLoaded:Number):void
			{
				//trace (className + ".updateProgressBar(...) - model.bytesLoaded: " + model.bytesLoaded);	
				//trace (className + ".updateProgressBar(...) - model.bytesTotal: " + model.bytesTotal);	
				bar.setProgress(model.bytesLoaded,model.bytesTotal);
				var pct:Number = Math.round((model.bytesLoaded/model.bytesTotal) * 100);
				bar.label= "Current Progress" + " " + pct + "%";
			}
			/**
			 * Handler for the NativeDragEvent.NATIVE_DRAG_ENTER event.
			 * */
			private function onDragEnterHandler(e:NativeDragEvent):void
			{
				//trace (className + ".onDragEnterHandler(...)");	
				if(e.clipboard.hasFormat(ClipboardFormats.FILE_LIST_FORMAT))
				{
					//Get the array of File objects
					var files:Array = e.clipboard.getData(ClipboardFormats.FILE_LIST_FORMAT) as Array;
					var file:File = File(files[0]);
					//Allow only one file as only supporting one in this application and soft check it is a swf file.
					if( files.length == 1 && file.url.substr(file.url.length-4).toLowerCase() == ".swf" )
					{
						//Triggers NativeDragEvent.NATIVE_DRAG_DROP event. 
						//This is when we see the drop icon.
						NativeDragManager.acceptDragDrop(this);
					}
				}
			}
			/**
			 * Handler for the NativeDragEvent.NATIVE_DRAG_DROP event.
			 * */
			private function onDragDropHandler(e:NativeDragEvent):void
			{
				//trace (className + ".onDragDropHandler(...)");	
				var files:Array = e.clipboard.getData(ClipboardFormats.FILE_LIST_FORMAT) as Array;
				var file:File = File(files[0]);
				//trace (className + ".onDragDropHandler(...) - file.url:" + file.url);
				url_ti.text = file.url;
				fileName = file.name;
				// Optional can initiate the load automatically.
				dispatcher(new LoadSwfUrlHeaderRequestEvent (LoadSwfUrlHeaderRequestEvent.LOAD, url_ti.text));
			}
		]]>
	</fx:Script>
	<fx:Declarations>
		<sf:Configure/>
		<mx:NumberFormatter id="numberFormatter" precision="0"  useThousandsSeparator="true" />
	</fx:Declarations>
	<!--- 
	Create NativeDrag target with BorderContainer
	--> 
	<s:BorderContainer 
				width="100%" height="100%"
					   borderAlpha="0"	
					   backgroundColor="0xffffff"
					   >
	<s:VGroup verticalAlign="top"  horizontalAlign="center" width="100%" height="100%"   
			   paddingTop="10"
			   >
		<s:HGroup >
			<s:Label  text="SWF URL:" paddingTop="6" />
			<s:VGroup >
				<s:HGroup  verticalAlign="middle">
				<s:TextInput id="url_ti" text="Enter swf http url or drag from desktop" width="294" />
				<s:Button  label="Load SWF" id="load_btn" click="load_btn_clickHandler(event)"/>	
				</s:HGroup>
				<!--- 
				Progress bar for slow loads such as the internet.
				--> 		
				<mx:ProgressBar id="bar"   labelPlacement="bottom" minimum="0" visible="true" maximum="100"
								color="0x323232" 
								label="Current Progress 0%" direction="right" 
								mode="manual" width="100%"/>
			</s:VGroup>
		</s:HGroup>
		<!--- 
		The result values
		--> 
		<mx:Form width="100%" height="100%"       >
			<mx:FormItem label="SWF File name: ">
				<s:Label color="0x000000" text="{fileName}"/>
			</mx:FormItem>
			<mx:FormItem label="Flash player version: ">
				<s:Label color="0x000000" text="{numberFormatter.format(model.swfHeader_vo.swfVersion)}"/>
			</mx:FormItem>
			<s:HGroup>
				<mx:FormItem label="Size uncompressed: ">
					<s:Label color="0x000000" text="{numberFormatter.format(model.swfHeader_vo.sizeUncompressed)}"/>
				</mx:FormItem>
				<mx:FormItem label="Compressed: ">
					<s:Label color="0x000000" text="{numberFormatter.format(model.swfHeader_vo.sizeCompressed)}"/>
				</mx:FormItem>			
			</s:HGroup>
			<mx:FormItem label="Frame rate: ">
				<s:Label color="0x000000" text="{numberFormatter.format(model.swfHeader_vo.frameRate)}"/>
			</mx:FormItem>
			<mx:FormItem label="Total frames: ">
				<s:Label color="0x000000" text="{numberFormatter.format(model.swfHeader_vo.totalFrames)}"/>
			</mx:FormItem>
			<mx:FormItem label="Width: ">
				<s:Label  color="0x000000" text="{numberFormatter.format(model.swfHeader_vo.width)}"/>
			</mx:FormItem>
			<mx:FormItem label="Height: ">
				<s:Label color="0x000000" text="{numberFormatter.format(model.swfHeader_vo.height)}"/>
			</mx:FormItem>
		</mx:Form>
	</s:VGroup>
	</s:BorderContainer>
</s:Group>

ApplicationCloseButton.as
Own version of the spark Button class.

package ui
{
	import spark.components.Button;
	/**
	 * Button to close the application
	 * */
	public class ApplicationCloseButton extends Button
	{
		public function ApplicationCloseButton()
		{
			super();
		}
	}
}

ApplicationCloseButtonSkin.mxml
Skin for the application close button class, ApplicationCloseButton. I had some trouble with the edges of the images causing a repeating state change on rollover. I solved this by making a transparent background on line 29 a few pixels larger than the bitmaps. The bitmaps then had their verticalCenter and horizontalCenter properities set to zero to keep them in the middle. See lines 34 and 36.

<?xml version="1.0" encoding="utf-8"?>
<!--- 
Skin for ui.ApplicationCloseButton
-->
<s:SparkSkin xmlns:fx="http://ns.adobe.com/mxml/2009"
			 xmlns:s="library://ns.adobe.com/flex/spark"
			 xmlns:fb="http://ns.adobe.com/flashbuilder/2009"
			 
			 alpha.disabled="0.5">
	
	<!-- host component -->
	<fx:Metadata>
		<![CDATA[
		/* @copy spark.skins.spark.ApplicationSkin#hostComponent 	*/
		[HostComponent("ui.ApplicationCloseButton")]
		]]>
	</fx:Metadata>
	
	<!-- states -->
	<s:states>
		<s:State name="up" />
		<s:State name="over" />
		<s:State name="down" />
		<s:State name="disabled" />
	</s:states>
	<!--- 
	Hit area.
	-->	
	<s:Rect  left="0" right="0" top="0" bottom="0"  width="34" height="34">
		<s:fill>
			<s:SolidColor color="0xffffff" alpha="0" />
		</s:fill>	
	</s:Rect>
	<s:BitmapImage  verticalCenter="0"  horizontalCenter="0" source="@Embed('../assets/red_glossy_close_up_button_published.png')" 
				   includeIn="up, disabled, down "/>
	<s:BitmapImage verticalCenter="0"  horizontalCenter="0"  source="@Embed('../assets/green_glossy_close_up_button_published.png')" 
				   includeIn="over"/>
	
</s:SparkSkin>

ApplicationModel.as
I could have merged the two models, but as a practice in Parsley frameworks, I create an ApplicationModel by default.

package models
{
	/**
	 * Application model.
	 * */
	public class ApplicationModel
	{

	}
}

[ad name=”Google Adsense”]
SWFHeaderModel.as
The model containing all the bound data. Flex binding is magnificent for creating model-view-controller solutions.

package models
{
	import vo.SWFHeader_vo;
	/**
	 * Model for the SWF header information.
	 * */
	public class SWFHeaderModel
	{
		/**
		 * Swf header field values.
		 * */
		[Bindable]
		public var swfHeader_vo:SWFHeader_vo;
		/**
		 * Nest the ApplicationModel.
		 * */		
		[Inject]
		[Bindable]
		public var applicationModel:ApplicationModel;
		/**
		 * Bytes loaded.
		 * */
		[Bindable]
		public var bytesLoaded:Number;
		/**
		 * Total bytes to load.
		 * */		
		[Bindable]
		public var bytesTotal:Number;
		/**
		 * Initialize model values.
		 * */		
		public function SWFHeaderModel()
		{
			swfHeader_vo = new SWFHeader_vo();
		}
	}
}

ApplicationController.as
I could have created a controller just for the SWF header if I wanted a separate model-view-controller for SWF header decoupled from the application.

package controllers
{
	import events.LoadSwfUrlHeaderRequestEvent;
	import events.SWFHeaderLoaderCompleteEvent;
	import events.SWFHeaderLoaderErrorEvent;
	import events.SWFHeaderLoaderProgressEvent;
	import flash.net.URLRequest;
	import models.SWFHeaderModel;
	import mx.controls.Alert;
	import swf.SWFHeaderLoader;
	import vo.SWFHeader_vo;
	/**
	 * Application controller
	 * */
	public class ApplicationController
	{
		/**
		 * Toggle to indicate if an Alert error is being shown. All loading errors are 
		 * routed to one method and so if there are more than one per SWFHeaderLoader.load
		 * request, this prevents multiple Alert popups.
		 * @see swfHeaderErroMessageHandler
		 * */
		private var showingSwfLoadError:Boolean = false;
		/**
		 * The SWFHeaderLoader
		 * */
		[Inject]
		public var swfHeaderLoader:SWFHeaderLoader;
		/**
		 * The model for MVC.
		 * */
		[Inject]
		[Bindable]
		public var model:SWFHeaderModel;
		/**
		 * Information Alert dialog title.
		 * */
		private var lang_info_alert_title:String = "Attention";
		/**
		 * Message when file name does not contain a .swf extension.
		 * */
		private var lang_tool_requires_swf:String = "Tool is designed for Flash Movie \".swf\" files";
		/**
		 * Message when swf file could not be loaded for any reason.
		 * */
		private var lang_unable_to_load_swf:String = "Sorry, unable to load the file.";
		/**
		 * Handler for LoadSwfUrlHeaderRequestEvent. Validate file extension. Load swf.
		 * */
		[MessageHandler]
		public function swfUrlLoadRequestMessageHandler( message : LoadSwfUrlHeaderRequestEvent ):void
		{
			model.swfHeader_vo = new SWFHeader_vo();
			// File url does not have a .swf at end.
			if ( message.swf_url.substr(message.swf_url.length-4).toLowerCase() != ".swf"  )
			{
				Alert.show(lang_tool_requires_swf ,lang_info_alert_title);
			}
			// File url has a .swf at end.
			else
			{
				showingSwfLoadError = false;
				swfHeaderLoader.load(message.swf_url);
			}
		}
		/**
		 * Handler for SWFHeaderLoaderProgressEvent.
		 * */
		[MessageHandler]
		public function swfHeaderProgressMessageHandler( message : SWFHeaderLoaderProgressEvent ):void
		{
			model.bytesTotal = message.bytesTotal;
			model.bytesLoaded = message.bytesLoaded;
		}
		/**
		 * Handler for SWFHeaderLoaderCompleteEvent.
		 * */
		[MessageHandler]
		public function swfHeaderLoadedMessageHandler( message : SWFHeaderLoaderCompleteEvent ):void
		{
			model.swfHeader_vo = message.swfHeader_vo;
		}
		/**
		 * Handler for SWFHeaderLoaderErrorEvent.
		 * */
		[MessageHandler]
		public function swfHeaderErroMessageHandler( message : SWFHeaderLoaderErrorEvent ):void
		{
			if (!showingSwfLoadError)
			{
				showingSwfLoadError = true;
				Alert.show(lang_unable_to_load_swf,lang_info_alert_title);
			}
		}		
	}
}

LoadSwfUrlHeaderRequestEvent.as
Event handler for requests to load a swf. Note how Parsley simplifies the event code.

package events
{
	import flash.events.Event;
	/**
	 * Request the loading of a swf file.
	 * */
	public class LoadSwfUrlHeaderRequestEvent extends Event
	{
		public static const LOAD:String = "event.load";
		public var swf_url:String;
		public function LoadSwfUrlHeaderRequestEvent(type:String, swf_url:String, bubbles:Boolean=false, cancelable:Boolean=false)
		{
			super(type, bubbles, cancelable);
			this.swf_url = swf_url;
		}
	}
}

SWFHeaderLoaderProgressEvent.as
For monitoring the loading particularly when over the internet.

package events
{
	import flash.events.Event;
	/**
	 * Progress of loading a swf file.
	 * */	
	public class SWFHeaderLoaderProgressEvent extends Event
	{
		public static const PROGRESS:String = "event_SWFHeaderLoaderEvent_progress";
		/**
		 * Bytes loaded.
		 * */	
		public var bytesLoaded:Number;
		/**
		 * Total bytes to load.
		 * */		
		public var bytesTotal:Number;
		public function SWFHeaderLoaderProgressEvent(type:String, bytesLoaded:Number, bytesTotal:Number, bubbles:Boolean=false, cancelable:Boolean=false)
		{
			super(type, bubbles, cancelable);
			this.bytesLoaded = bytesLoaded;
			this.bytesTotal = bytesTotal;
			
		}
	}
}

SWFHeaderLoaderCompleteEvent.as
When an SWF is completely loaded. You may want to redesign to stop once the header is loaded. However I had thought it might be nice to show the SWF at one point or proceed to extract other information.

package events
{
	import flash.events.Event;
	import vo.SWFHeader_vo;
	/**
	 * Completion of loading a swf file.
	 * */
	public class SWFHeaderLoaderCompleteEvent extends Event
	{
		public static const COMPLETE:String = "event_SWFHeaderLoaderEvent_complete";
		/**
		 * Swf file header data.
		 * */	
		public var swfHeader_vo:SWFHeader_vo;
		public function SWFHeaderLoaderCompleteEvent(type:String, swfHeader_vo:SWFHeader_vo, bubbles:Boolean=false, cancelable:Boolean=false)
		{
			super(type, bubbles, cancelable);
			this.swfHeader_vo = swfHeader_vo;
		}
	}
}

SWFHeaderLoaderErrorEvent.as
Errors in loading the Swf file.

package events
{
	import flash.events.Event;
	/**
	 * Errors from loading a swf file.
	 * */
	public class SWFHeaderLoaderErrorEvent extends Event
	{
		public static const SECURITY_ERROR:String = "event_SWFHeaderLoaderEvent_security";
		public static const HTTP_ERROR:String = "event_SWFHeaderLoaderEvent_HTTP";
		public static const IO_ERROR:String = "event_SWFHeaderLoaderEvent_IO";
		public function SWFHeaderLoaderErrorEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)
		{
			super(type, bubbles, cancelable);
		}
	}
}
Categories
Articles

Parsley MVC, RemoteObject, Zend AMF and MySQL Basic Flex Example

After conquering a basic Parsley Framework Flex example I wanted to create a minimal example of using the Flex Parsley framework, Zend AMF and MySQL. The Flex implementation goal is to mold a model view controller framework using Parsley and to communicate via Flex RemoteObject to Zend AMF. Eventually I applied what I learned to revamp DefineIt.com with Parsley and Zend AMF.

There are many approached to using Parsley for MVC. The Parsley documentation is very light on the subject. These also entail including using other frameworks like Caringorm. In this example I kept it simply by just using the Parsley messaging features and creating my own controller class.

I found other examples of Parsley that go further into its features. You might look at Christophe Coenraets’ example. This is referenced from the Parsley documentation . Christophe includes an update and add feature to the application. It also contains a dynamically loaded image class. It is a bit over featured and thus over involved for a pure beginner. It is void of substantive explanation leaving you to explore the Parsley documentation to try to understand why one approach was chosen over another in code. Worse code has no comments. This should be a minimum requirement for any referenced sample code. However there are some good Flex and Parsley techniques in there that can be helpful and one to bookmark for your learning process.

Learn More About Flex 4 From Adobe

Parsley 2.3 is used and the SWCs are included in the example Flex 4 project download I have here. I did appreciate these SWCs also being included in the downloads from the Parsley download page. Lets you get to learning faster.

You can build this with the free Flex SDK by using the code in the src folder and be sure to include a path to the Parsley and Spicelib library.

When you run this application, the trace log will show you the interactions between the model, controller, view and service.

Application Class – Parsley_Configuration.mxml
This is the main MXML file. Line 15 links the Parsley_Configuration.mxml file that configures Parsley framework.

Line 16 tells Parsley to include this mxml file and thus allows us to inject the ApplicationModel data.

I put all locale language into the model to simplify the example. In the effort the ApplicationModel class for application level data only contains the locale language title for the application you can see referenced in the Label on lines 26 and 39.

Line 40 contains the Panel to display the data grid.

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
			   xmlns:s="library://ns.adobe.com/flex/spark" 
			   xmlns:hellozend="com.alh.parsley.example.hellozend.*"
			   xmlns:parsley="http://www.spicefactory.org/parsley"
			   xmlns:mx="library://ns.adobe.com/flex/mx"
			   xmlns:sf="http://www.spicefactory.org/parsley"
			   minWidth="955" minHeight="600" 
			   xmlns:views="com.alh.parsley.example.hellozend.views.*"
			   >
	<fx:Declarations>
		<!-- 
		Parsley configuration MXML file
		-->
		<parsley:ContextBuilder config="Parsley_Configuration" />
		<sf:Configure/>
	</fx:Declarations>
	<fx:Script>
		<!&#91;CDATA&#91;
			import com.alh.parsley.example.hellozend.model.ApplicationModel;
			/**
			 * Parsley injects the ApplicationModel here.
			 * */
			&#91;Inject&#93;
			&#91;Bindable&#93;
			public var model:ApplicationModel;
			
		&#93;&#93;>
	</fx:Script>
	<!-- 
		UI
	-->
	<s:layout>
		<s:VerticalLayout gap="10" 
						  horizontalAlign="center"
						  paddingLeft="12" paddingRight="12" paddingBottom="12" paddingTop="12"
						  />
	</s:layout>
	<s:Label text="{model.lang_AppTitle}" fontSize="20"/>
	<views:AllMembersGrid/>
</s:Application>

Parsley Configuration – Parsley_Configuration.mxml
This is the file Parsley reads for configuration.

I included the RemoteObject definitions here as well. I am following Christophe Coenraets’ example on this. It makes sense to me to have this as part of the configuration file. Remember to change the YOUR_GATEWAY_URL on line 13 to reflect your link to the Zend AMF or other RemoteObject service.

You may note there is no services xml or configuration file. The channel information is in the MXML instead. You can learn more on this by reading
ZEND AMF (Action Message Format) Minimalist Example Using RemoteObject and MySQL

On lines 27 – 30 I have four Actionscript classes handling the model, view and controller. They are listed here so we can use the [Inject] metatag for Parsley.

<?xml version="1.0" encoding="utf-8"?>
<Objects 
	xmlns:fx="http://ns.adobe.com/mxml/2009"
	xmlns="http://www.spicefactory.org/parsley"
	xmlns:s="library://ns.adobe.com/flex/spark" 
	xmlns:hellozend="com.alh.parsley.example.hellozend.*" 
	xmlns:model="com.alh.parsley.example.hellozend.model.*" 
	xmlns:services="com.alh.parsley.example.hellozend.services.*" 
	xmlns:controller="com.alh.parsley.example.hellozend.controller.*">
	<fx:Declarations>
		<s:ChannelSet id = "zend_amf_channel_set">
			<s:channels>
				<s:AMFChannel uri="YOUR_GATEWAY_URL"/>
			</s:channels>
		</s:ChannelSet>
		<!-- MembershipService RemoteObject -->
		<s:RemoteObject	id="membershipService_ro" 
						destination="zend-amf" 
						source="MembershipService" 
						channelSet="{zend_amf_channel_set}"
						showBusyCursor="true" 
						>
		</s:RemoteObject>
		<!--
			Parsley defined objects slated for injection where &#91;Inject&#93; metatag appears.
		-->
		<model:ApplicationModel/>
		<model:MembershipModel/>
		<services:MembershipServiceRemote/>
		<controller:MembershipController/>
</fx:Declarations>
</Objects>

Application Model Class – ApplicationModel.as
This class defines application level data and we only have a language variable to simplify the example for localization.

package com.alh.parsley.example.hellozend.model
{
	/**
	 * Model for application level data
	 * */
	public class ApplicationModel
	{
		/**
		 * UI language in model to simplify example
		 * 
		 * Language for Application
		 * */
		[Bindable]
		public var lang_AppTitle:String = "Minimalist Parsley Zend AMF Example Using RemoteObject";;
	}
}

Membership Model Class – MembershipModel.as
This class defines membership data. Again you can see the variables to simplify the example for localization.

The key data here is the ArrayCollection on line 13 for the member data.

package com.alh.parsley.example.hellozend.model
{
	import mx.collections.ArrayCollection;
	/**
	 * Model for membership
	 * */
	public class MembershipModel
	{
		/**
		 * List of members;
		 * */
		[Bindable]
		public var members:ArrayCollection;
		/**
		 * UI language in model to simplify example
		 * 
		 * Language for AllMembersGrid view
		 * */
		[Bindable]
		public var lang_AllMembersGridTitle:String = "Membership Listing";
		[Bindable]
		public var lang_MemberKey:String = "Member Key";
		[Bindable]
		public var lang_FirstName:String = "First Name";
		[Bindable]
		public var lang_LastName:String = "Last Name";
		[Bindable]
		public var lang_EmailAddress:String = "Email Address";
		[Bindable]
		public var lang_GetMembers:String = "Get Members";
		[Bindable]
		public var lang_ClearMembers:String = "Clear";
	}
}

IMembershipService Class – IMembershipService.as
Interface to define methods for membership service classes. We only have one in the example: MembershipServiceRemote.

package com.alh.parsley.example.hellozend.services
{
	import mx.rpc.AsyncToken;
	/**
	 * Interface to define MembershipService classes
	 * */
	public interface IMembershipService
	{
		function getAllMembers():AsyncToken;
	}
}

[ad name=”Google Adsense”]
MembershipServiceRemote Class – MembershipServiceRemote.as
This class deals with the server service. Only the controller communicates with this class by calling the get getAllMembers() method.

Line 46 is the interface back to the MVC or to the application however you want to look at it. Here a Parsley message is created for the MembershipGetAllMembersEvent.GET_ALL_MEMBERS event that you will see the MembershipController class handles.

You could have the controller pass its own result and fault handlers to the getAllMembers() method and eliminate the MembershipGetAllMembersEvent.GET_ALL_MEMBERS message.

As a further simplification, the faults are all routed to the catchAllServiceErrorHandler(…) method where a view component is inserted breaking a clean MVC. You could create a message for the fault handler to dispatch and have the controller to handle this.

package com.alh.parsley.example.hellozend.services
{
	import com.alh.parsley.example.hellozend.events.MembershipGetAllMembersEvent;
	import com.alh.parsley.example.hellozend.services.IMembershipService;
	import mx.controls.Alert;
	import mx.rpc.AsyncResponder;
	import mx.rpc.AsyncToken;
	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;
	import mx.rpc.remoting.RemoteObject;
	/**
	 * IMembershipService class to handle RemoteObject data communications with server.
	 * */
	public class MembershipServiceRemote implements IMembershipService
	{
		/**
		 * Specify the remote service for Parsley to inject.
		 * */
		[Inject(id="membershipService_ro")]
		public var service:RemoteObject;		
		/**
		 * Parsley creates an event dispatcher function
		 * */
		[MessageDispatcher]
		public var dispatcher:Function;

		/**
		 * Get all members from RemoteObject service.
		 * */
		public function getAllMembers():AsyncToken
		{
			trace ("SERVICE: MembershipServiceRemote.getAllMembers()");
			var token:AsyncToken = service.getAllMembers();
			token.addResponder(
				new AsyncResponder(getAllMembersResultsHandler, catchAllServiceErrorHandler));
			return token;
		}
		/**
		 * ResultEvent handler for service.getAllMembers()
		 * */
		protected function getAllMembersResultsHandler(event:ResultEvent, result:Object):void
		{
			trace ("SERVICE: MembershipServiceRemote.getAllMembersResultsHandler()");
			//event.result == Array.
			var members:Array = event.result as Array;
			dispatcher( new MembershipGetAllMembersEvent(MembershipGetAllMembersEvent.GET_ALL_MEMBERS, members));
		}
		/**
		 * Default handler for MembershipServiceRemote calls
		 * */
		protected function catchAllServiceErrorHandler(e:FaultEvent):void
		{
			Alert.show(e.toString());
		}
	}
}

MembershipController Class – MembershipController.as
This controller ties in the MembershipModel class and the MembershipServiceRemote class on line 18 and 24 respectively.

The controller listens for the MembershipEvent events on line 29. The Parsley [MessageHandler] tag makes this happen. These events are dispatched in the view.

On line 46 the controller listens for the MembershipGetAllMembersEvent dispatched from the MembershipServiceRemote class.

package com.alh.parsley.example.hellozend.controller
{
	import com.alh.parsley.example.hellozend.events.MembershipEvent;
	import com.alh.parsley.example.hellozend.events.MembershipGetAllMembersEvent;
	import com.alh.parsley.example.hellozend.model.MembershipModel;
	import com.alh.parsley.example.hellozend.services.MembershipServiceRemote;
	import mx.collections.ArrayCollection;
	/**
	 * Controller for Membership model and views.
	 * */
	public class MembershipController
	{
		/**
		 * Parsley injects the MembershipModel here.
		 * */
		[Inject]
		[Bindable]
		public var model:MembershipModel;
		/**
		 * Parsley injects the MembershipService here.
		 * */
		[Inject]
		[Bindable]
		public var service:MembershipServiceRemote;
		/**
		 * Parsley identified handler for MembershipEvent
		 * */
		[MessageHandler]
		public function membershipEventHandler( message:MembershipEvent ):void
		{
			trace ("CONTROLLER: MembershipController.membershipEventHandler(...) - message.type:" + message.type);
			switch (message.type )
			{
				case MembershipEvent.GET_ALL_MEMBERS:
					service.getAllMembers();
					break
				case MembershipEvent.CLEAR_ALL_MEMBERS:
					model.members.removeAll();
					break
			}
		}
		/**
		 * Parsley identified handler for MembershipGetAllMembersEvent
		 * */
		[MessageHandler]
		public function membershipGetAllMembersEventHandler( message:MembershipGetAllMembersEvent ):void
		{
			trace ("CONTROLLER: MembershipController.membershipGetAllMembersEventHandler(...)");
			model.members = new ArrayCollection(message.members);
		}
	}
}

[ad name=”Google Adsense”]
Membership Grid View – AllMembersGrid.mxml
This is a Panel containing a DataGrid and Buttons for the user interaction.

Lines 27 and 28 sets up the class to dispatch Parsley messages. Line 35 dispatches the MembershipEvent.CLEAR_ALL_MEMBERS event and line 43 dispatches the MembershipEvent.GET_ALL_MEMBERS event. The MembershipController class handles these messages to update the model and communicate with the service as needed.

This view is tied to the MembershipModel class via Parsley on line 23. The DataGrid binds to the model members variable.

Other bindings for the language localizations appear as well.

<?xml version="1.0" encoding="utf-8"?>
<s:Panel xmlns:fx="http://ns.adobe.com/mxml/2009" 
		 xmlns:s="library://ns.adobe.com/flex/spark" 
		 xmlns:mx="library://ns.adobe.com/flex/mx"
		 xmlns:sf="http://www.spicefactory.org/parsley"
		 title = "{model.lang_AllMembersGridTitle}"
		>
	<fx:Declarations>
		<!-- 
		Parsely will manage this component.
		-->
		<sf:Configure/>
	</fx:Declarations>
	<fx:Script>
		<!&#91;CDATA&#91;
			import com.alh.parsley.example.hellozend.events.MembershipEvent;
			import com.alh.parsley.example.hellozend.model.MembershipModel;
			/**
			 * Parsley injects the MembershipModel here.
			 * */
			&#91;Inject&#93;
			&#91;Bindable&#93;
			public var model:MembershipModel;
			/**
			 * Parsley creates an event dispatcher function
			 * */
			&#91;MessageDispatcher&#93;
			public var dispatcher:Function;
			/**
			 * Dispatch MembershipEvent.CLEAR_ALL_MEMBERS event
			 * */
			protected function clearDataGrid():void
			{
				trace("VIEW: " + className + ".clearDataGrid()");
				dispatcher( new MembershipEvent( MembershipEvent.CLEAR_ALL_MEMBERS ));
			}
			/**
			 * Dispatch MembershipEvent.GET_ALL_MEMBERS event
			 * */
			protected function getAllMembers():void
			{
				trace("VIEW: " + className + ".getAllMembers()");
				dispatcher( new MembershipEvent( MembershipEvent.GET_ALL_MEMBERS ));
			}
		&#93;&#93;>
	</fx:Script>
	<s:layout>
		<s:VerticalLayout gap="10" 
						  paddingLeft="12" paddingRight="12" paddingBottom="12" paddingTop="12"
						  />
	</s:layout>
	<mx:DataGrid  id="member_dg"  height="100" dataProvider="{model.members}">
		<mx:columns>
			<mx:DataGridColumn headerText="{model.lang_MemberKey}" dataField="memberKey"/>
			<mx:DataGridColumn headerText="{model.lang_FirstName}" dataField="firstName"/>
			<mx:DataGridColumn headerText="{model.lang_LastName}" dataField="lastName"/>
			<mx:DataGridColumn headerText="{model.lang_EmailAddress}" dataField="emailAddress" width="200"/>
		</mx:columns>
	</mx:DataGrid>
	<s:HGroup horizontalAlign="center" width="100%">
		<s:Button label="{model.lang_GetMembers}" click="{getAllMembers();}"/>
		<s:Button label="{model.lang_ClearMembers}" click="{clearDataGrid();}"/>
	</s:HGroup>

</s:Panel>

MembershipEvent.as
This is a standard Event class to inform the MVC framework all membership data is required or needs to be cleared. These free the view from being coupled with the service or updating the model.

package com.alh.parsley.example.hellozend.events
{
	import flash.events.Event;
	/**
	 * Events related to membership.
	 * */
	public class MembershipEvent extends Event
	{
		/**
		 * Request to retrieve all membership data
		 * */
		public static const GET_ALL_MEMBERS:String = "com.alh.parsley.example.hellozend.events.MembershipEvent.getAllMembers";
		/**
		 * Request to clear all membership data
		 * */
		public static const CLEAR_ALL_MEMBERS:String = "com.alh.parsley.example.hellozend.events.MembershipEvent.clearAllMembers";

		public function MembershipEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)
		{
			super(type, bubbles, cancelable);
		}
	}
}

MembershipGetAllMembersEvent.as
This event message signals success retrieval of members data from the service.

package com.alh.parsley.example.hellozend.events
{
	import flash.events.Event;
	/**
	 * Event defining service receipt of membership data.
	 * */
	public class MembershipGetAllMembersEvent extends Event
	{
		public static const GET_ALL_MEMBERS:String = "com.alh.parsley.example.hellozend.events.MembershipGetAllMembersEvent.getAllMembers";
		/**
		 * Membership data
		 * */
		public var members:Array;
		
		public function MembershipGetAllMembersEvent(type:String, members:Array, bubbles:Boolean=false, cancelable:Boolean=false)
		{
			super(type, bubbles, cancelable);
			this.members = members;
		}
	}
}

Zend AMF Gateway PHP Script
This is the gateway program for the Zend Amf. This is the file that you reference on line 13 of the Parsley_Configuration.mxml file. In this example it was named index.php.

<?php
/**
*  Sample Zend AMF gateway
*  @return Endpoint &#91;Zend Amf Endpoint&#93;
* */

// Configurable values
// Debugging values
$debug = true;                             // Debugging status
if ($debug)
{
	// Report all errors, warnings, interoperability and compatibility
	error_reporting(E_ALL|E_STRICT);
	// Show errors with output
	ini_set("display_errors", "on");
}
else
{
	error_reporting(0);
	ini_set("display_errors", "off");
}
// Add the Zend AMF installation folder to the include path.
// In this example the frameworks folder is a sibling folder to
// this application folder. The frameworks folder contains the Zend
// folder that is extracted from http://framework.zend.com/download/amf
ini_set("include_path", ini_get("include_path") . PATH_SEPARATOR . "..\\frameworks" );

// Instantiate the Zend Amf server
require_once 'Zend/Amf/Server.php';
$server = new Zend_Amf_Server();

// Register your service classes
require_once 'MembershipService.php';
$server->setClass("MembershipService");

//Map ActionScript value objects to the PHP value objects.
$server->setClassMap("MemberData", "MemberData");

// Return the handle.
echo ($server->handle());

?>

[ad name=”Google Adsense”]
MembershipService Class
This is the service class that contains remote methods. Generally a the database and business logic is delegated to another API you write. In this case they are all together for simplicity.

<?php
/**
*	Service class exposing the methods to deal with membership.
*   This example includes business logic for simplicity of study.
*/
require_once 'MemberData.php';
class MembershipService
{
	public function MembershipService()
	{
		// Connect to MySql database.
		// Supply your own MySQL access values.
		// These are defaults when running on your own private computer.
		mysql_connect("localhost", "root", "");
		// Select the database.
		// Supply your own database name.
		mysql_select_db("test");
	}
	/**
	*	Get all members and all fields.
	*/
	public function getAllMembers()
	{
		// Array of MemberData objects.
		$members = array();
		// Selecting all fields and all records from table.
		// Supply your own table name and optionally your own SQL statement.
		$result = mysql_query("SELECT * FROM zend_amf_members");
		// Assuming mysql_query success. Slog through records.
		while ($row = mysql_fetch_assoc($result))
		{
			// Create a MemberData value object and populate.
			$member = new MemberData();
			$member->memberKey = $row["memberKey"];
			$member->firstName = $row["firstName"];
			$member->lastName = $row["lastName"];
			$member->emailAddress = $row["emailAddress"];
			array_push($members, $member);
		}
		// Return the members array to client.
		return $members;
	}
}
?>

MemberData Value Object Class for PHP
This is the value object on the server side to define the field names for a member object. This is mapped to the client side on line 37 of the Zend AMF gateway script.

<?php
/**
 * Value object defining the member data
 * */
class MemberData
{
  public $memberKey;	// uint
  public $firstName;	// String
  public $lastName;		// String
  public $emailAddress;	// String
}
?>

SQL To Create Testing Table
The PHP script uses zend_amf_members for the table and this is the SQL to create that table. In this example the database was called test.

SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";

--
-- Database: `test`
--

-- --------------------------------------------------------

--
-- Table structure for table `zend_amf_members`
--

CREATE TABLE IF NOT EXISTS `zend_amf_members` (
  `memberKey` int(10) unsigned NOT NULL auto_increment,
  `firstName` varchar(30) NOT NULL default '',
  `lastName` varchar(30) NOT NULL default '',
  `emailAddress` varchar(50) NOT NULL default '',
  PRIMARY KEY  (`memberKey`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='Zend Amf Examples' AUTO_INCREMENT=7 ;

--
-- Dumping data for table `zend_amf_members`
--

INSERT INTO `zend_amf_members` (`memberKey`, `firstName`, `lastName`, `emailAddress`) VALUES
(1, 'Donald', 'Duck', 'quacker@pond.com'),
(2, 'Daffy', 'Duck', 'daft_2x@farm.org'),
(3, 'Elmer', 'Fudd', 'elmer.fudd@hunters.net'),
(4, 'Bugs', 'Bunny', 'whats_up_doc@underground.org'),
(5, 'Yosemite', 'Sam', 'varmint_chaser@forest.com'),
(6, 'Wile', 'Coyote', 'ceo@acme.com');

Categories
Articles

Basic Parsley Framework Flex Example Explained

I decided to break down the Parsley framework for Flex and Flash projects. This is my own minimalist example. I am not going to extol virtues or evils with using Parsley and assume you just want to get a basic example that explains the pieces.

This does not tie into web server but I added a basic example that uses Zend AMF, PHP, MySQL, Flex Remote Objects.

Also if you want to look at a very basic Flash Actionscript example see Parsley Hello World For A Flash Builder Actionscript Project.

It was hard to find a good minimalist starter example that takes the time to explain the details. The Sitronnier (blogginglemon) examples had other technologies or unneeded features cluttering the code and was devoid of any substantive elaboration. The Sitronnier examples are linked out of the Parsley documentation and are worthwhile bookmarking to review once you get your feet wet.

The best I have found is Arturo Alvarado’s posts at Art’s Flex Notes. The 4 part series includes a comparative framework analysis and why he prefers Parsley. Art breaks his Parsley example down with one more level of granularity than I do here. I hope Art continues to add to his series as he communicates very clearly.

The “Hello World” getting started example in the Parsley documentation is also a great starting point except there is no explanation outside of comments to soothe your into it. But the comments are great and I added my own coding style to this example and simplified further along with a few changes to better illustrate some items.

My example focuses on the main structure and foundational features in Parsley. There is much more available in Parsley to learn and explore.

Learn More About Flex 4 From Adobe

Parsley 2.3 is used and the SWCs are included in the example Flex 4 project download I have here. I did appreciate these SWCs also being included in the downloads from the Parsley download page. Lets you get to learning faster.

You can build this with the free Flex SDK by using the code in the src folder and be sure to include a path to the Parsley and Spicelib library.

Application Class – ParsleyFramework_HelloWorld.mxml
This is the main MXML file. Line 12 links another MXML file, ParselyFramework_HelloWorld_Configuration, that configures Parsley framework. Other than that, this appears to be a basic Flex MXML file with two UI components.

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
			   xmlns:s="library://ns.adobe.com/flex/spark" 
			   xmlns:hw="com.alh.parsely.example.helloworld.*"
			   xmlns:parsley="http://www.spicefactory.org/parsley"
			   xmlns:mx="library://ns.adobe.com/flex/mx" 
			   minWidth="955" minHeight="600">
	<fx:Declarations>
		<!-- 
		Parsley configuration MXML file
		-->
		<parsley:ContextBuilder config="ParsleyFramework_HelloWorld_Configuration" />
	</fx:Declarations>
	<!-- 
		UI
	-->
	<s:VGroup width="100%" height="98%"  gap="10" horizontalAlign="center">
		<hw:InputPanel width="50%" height="100%" title="Input Panel"/>
		<hw:OutputPanel width="50%" height="100%"  title="Output Panel"/>
	</s:VGroup>

</s:Application>

Parsley Configuration – ParsleyFramework_HelloWorld_Configuration.mxml
This is a Parsley configuration file. Parsley reads this file.

There are three model files in this application. One, InputModel, is for the InputPanel, one, OutputModel, for the OutputPanel and a shared model, SharedModel.

This configuration file identifies these for the Parsley framework to use.

<?xml version="1.0" encoding="utf-8"?>
<Objects 
	xmlns:fx="http://ns.adobe.com/mxml/2009"
	xmlns="http://www.spicefactory.org/parsley"
	xmlns:hw="com.alh.parsley.example.helloworld.*">
	<fx:Declarations>
		<!--
			Parsley defined objects slated for injection where [Inject] metatag appears.
		-->
		<hw:SharedModel/>
		<hw:InputModel/>
		<hw:OutputModel/>
	</fx:Declarations>
</Objects>

[ad name=”Google Adsense”]
InputPanel.mxml
This is a UI component for our MVC framework. It lets a user enter text and press a button to submit. It validates that the entry contains input.

Line 13 registers this UI component with Parsley.

Lines 23-25 inject or we could say include the contents of InputModel, the data for a MVC framework.

InputPanel is self contained in its messaging but broadcasts a message via the InputModel model sendInputMessage (…) method on line 44.

Line 35 was my way of demonstrating the Parsley event to notify that a model has loaded properly. I do not see it necessary here other from a demonstration point of view. More about this when we review InputModel.

I decided to use the model to store the localization language rather than Flex resource manager for simplicity. I do not like localization language embedded in UI code so this is fine for demonstration and also small apps where manual language change has a low chance of being needed. You see the language references on lines 64 and 70.

Line 80 demonstrates the use of a nested model. The InputModel contains the SharedModel. More on that when we get to look at InputModel.

<?xml version="1.0" encoding="utf-8"?>
<s:Panel 
	xmlns:fx="http://ns.adobe.com/mxml/2009" 
	xmlns:s="library://ns.adobe.com/flex/spark"
	xmlns:sf="http://www.spicefactory.org/parsley"
	enabled="false"
	updateComplete="updateCompleteHandler(event)"
	>
	<fx:Declarations>
		<!-- 
		Parsley will manage this component.
		-->
		<sf:Configure/>
	</fx:Declarations>
	<fx:Script>
		<![CDATA[
			import mx.controls.Alert;
			import mx.events.FlexEvent;
			/** 
			 * Inject the model.
			 * The [Inject] metadata searches context for an object of matching type.
			 */
			[Inject]
			[Bindable]
			public var model:InputModel;
			/** 
			 * Handler from the UpdateComplete event
			 */
			protected function updateCompleteHandler(event:FlexEvent):void
			{
				// Demonstrates ways to test injection occured. See InputModel.
				// Model is injected: model != null
				// and
				// model init() was completed: model.initialized
				enabled =  model != null && model.initialized;
			}
			/** 
			 * Send a message to Parsley framework.
			 */
			private function sendMessage():void
			{
				if (input_ti.text.length > 0)
				{
					model.sendInputMessage(input_ti.text);
					input_ti.text = "";
				}
				else
				{
					Alert.show("Please enter a message.", "Attention");
				}
			}
		]]>
	</fx:Script>
	<!--
		UI
	-->
	<s:layout>
		<s:VerticalLayout gap="10" paddingLeft="12" paddingRight="12" paddingBottom="12" paddingTop="12"/>
	</s:layout>
	<s:HGroup verticalAlign="middle">
		<!--
			Reference to model
		-->
		<s:Label text="{model.lang_typeMessageHere + ' '}"/>
		<s:TextInput id="input_ti"/>
		<!--
			Reference to model
		-->
		<s:Button id ="send_button"
			label="{model.lang_send}" 
			click="sendMessage()"/>
	</s:HGroup>
	<!--
		Filler
	-->
	<s:Rect height="100%"/>
	<!--
		Use of a nested dependency. The sharedModel is a SharedModel object.
	-->
	<s:Label text="{model.sharedModel.lang_numberOfMessagesSent + ' ' +  model.sharedModel.numberOfMessagesSent }"/>
</s:Panel>

InputModel.mxml
This is data and messaging. By its name it says the model is more localized to the “input” UI component, but that is not a restriction.

Remember that the Parsley configuration mxml file, discussed above, registered this file.

Lines 20 and 21 show how to create a dispatcher method with Parsley. The dispatcher event is called on line 41.

Line 41 contains the Event, or in Parsley we say message, named InputMessage. We will look at this after we done with InputModel.

Line 39 shows the sendInputMessage(…) method used in InputPanel to broadcast the button click. This decouples the InputMessage event from the UI and places control with the model. Changing the event or adding additional logic or events leaves the UI unchanged. The UI is thus linked by the method sendInputMessage(…).

You may need to know if the model loaded properly. Parsley will call a method on its objects. You use the [Init] meta tag to identify the method and here it is shown on line 34. The init() method on line 35 sets the initialized variable in the model. The initialized variable then can be used by code dependent on the model to know it is loaded and firing. You see that back in the InputPanel code on line 34.

Lines 13 to 15 show how to inject another model. In this case the SharedModel file by its name implies having data shared by more than one area of the framework. Depending on your application structure you may find one model for all is sufficient or as demonstrated in this example having the model distributed for a modular approach.

package com.alh.parsley.example.helloworld
{
	public class InputModel
	{
		/**
		 * State of the init method executing or not. 
		 * */
		[Bindable]
		public var initialized:Boolean;
		/**
		 * Nest another model using injection. 
		 * */
		[Inject]
		[Bindable]
		public var sharedModel:SharedModel;
		/**
		 * Define a message in the model
		 * The [MessageDispatcher] metatag adds function to Parsley for sending (dispatch) messages.
		 */ 
		[MessageDispatcher]
		public var sendMessage:Function;
		/**
		 * Language
		 */ 
		[Bindable]
		public var lang_send:String = "Send";
		[Bindable]
		public var lang_typeMessageHere :String = "Type your message here:";
		
		/**
		 * The [Init] metadata tells Parsley to call the annotated method after 
		 * an instance of this object is created and configured. Used in InputPanel.
		 */ 
		[Init]
		public function init() : void 
		{
			initialized = true;
		}
		public function sendInputMessage(text:String) : void 
		{
			sendMessage(new InputMessage(InputMessage.SEND_MESSAGE,text));
		}
	}
}

[ad name=”Google Adsense”]
InputMessage.as
InputMessage is a custom Actionscript event class. Can be used in other frameworks or no framework. However the InputModel makes this a Parsley message.

package com.alh.parsley.example.helloworld
{
	import flash.events.Event;
	/** 
	 * Defines an input message for application.
	 */
	public class InputMessage extends Event
	{
		public static const SEND_MESSAGE:String = "com.alh.parsley.example.helloworld.send_message";
		/** 
		 * The message text
		 */
		public var messageText:String = "";
		/** 
		 * Constructor
		 */
		public function InputMessage(type:String, messageText:String, bubbles:Boolean = true, cancelable:Boolean = true)
		{
			super(type, bubbles, cancelable);
			this.messageText = messageText;
		}
	}
}

OutputPanel.mxml
Repeats many of the similarities of the InputPanel. There is no messaging included here. Basic binding to the Output model, that we will look at next, is set on lines 21 to 23.

Lines 32 and 40 have the bound values.

<?xml version="1.0" encoding="utf-8"?>
<s:Panel 
	xmlns:fx="http://ns.adobe.com/mxml/2009" 
	xmlns:s="library://ns.adobe.com/flex/spark"
	xmlns:sf="http://www.spicefactory.org/parsley"
	>
	<fx:Declarations>
		<!-- 
		Parsley will manage this component.
		-->
		<sf:Configure/>
	</fx:Declarations>
	<fx:Script>
		<![CDATA[
			/** 
			 * The [Inject] metadata tells Parsley to inject a dependency by 
			 * searching the context for an object of matching type.
			 */
			[Inject]
			[Bindable]
			public var model:OutputModel;
		]]>
	</fx:Script>
	<!--
		UI
	-->
	<s:layout>
		<s:VerticalLayout gap="10" paddingLeft="12" paddingRight="12" paddingBottom="12" paddingTop="12"/>
	</s:layout>
	<s:Label text="{model.lang_messagesTitle + ' '}"/>
	<!--
		Using model to bind values
	-->
	<s:TextArea width="100%" height="100%" text="{ model.messages }"/>
	<!--
		Use of a nested dependency. The sharedModel is a SharedModel object.
	-->
	<s:Label text="{model.sharedModel.lang_numberOfMessagesSent + ' ' +  model.sharedModel.numberOfMessagesSent }"/>
</s:Panel>

[ad name=”Google Adsense”]
OutputModel.as
This is another local model that by its name associates with InputPanel.

The meta tag [MessageHandler] registers the sendInputMessageHandler(…) method to receive InputMessage events. You recall the InputModel is dispatching InputMessage events.

The sendInputMessageHandler(…) basically updates the messages model value.

The practice of nesting the SharedModel is repeated on lines 13 -15.

package com.alh.parsley.example.helloworld
{
	public class OutputModel
	{
		/** 
		 * Hold the appended list of text messages sent.
		 */
		[Bindable]
		public var messages:String = "";
		/**
		 * Nest another model using injection. 
		 * */
		[Inject]
		[Bindable]
		public var sharedModel:SharedModel;
		/**
		 * Language
		 */ 
		[Bindable]
		public var lang_messagesTitle:String = "Messages Received:";
		/**
		 * Route InputMessage types to this method
		 * The [MessageHandler] metadata tells Parsley to make the routing happen.
		 */
		[MessageHandler]
		public function sendInputMessageHandler( message:InputMessage ) : void 
		{
			// Increment the message count.
			sharedModel.numberOfMessagesSent++;
			// Prepend a line number.
			message.messageText = sharedModel.numberOfMessagesSent + ". " + message.messageText;
			// Update model.
			messages += message.messageText + "\n";
		}
	}
}

SharedModel.as
Model data assumed by the file name shared throughout the application. You can see this above in InputModel and OutputModel that in turn allow InputPanel and OutputPanel to use this data.

package com.alh.parsley.example.helloworld
{
	public class SharedModel
	{
		[Bindable]
		public var numberOfMessagesSent:Number = 0;
		
		[Bindable]
		public var lang_numberOfMessagesSent :String = "Number of messages sent:";

	}
}