×

Stay in touch

×

Refactoring a Flex project with Mate, ZendAMF and MVC

Refactoring a Flex project with Mate, ZendAMF and MVC

  • Jan 27, 2012

Sometimes, software development projects do not go as we want...

Here, maybe frightened by technical issues, we first set out to solve the technical requirements, while setting aside the design phase.

It was to trigger a flash game via a push button that writes to the serial port.

But the flash player does not have access to low-level devices, so we built a bridge between the os and Flex, with a small Java program that relays messages from the serial port to a TCP socket.

The problem was that once these uncertainties removed, we directly begun development, without a true model of the application.

Big mistake!

If one day you are offered such a deal, get out!

For me I stayed, and now 2 years have gone...

Of course, we recently had to deliver the product to the customer, and thus we were compelled to refactor the code to produce something that is up to the Tanukis web-studio quality requirement...

This is not the game code, but the Air application that configures it. Obviously, being a commercial project, I could not deliver the sources.

Here we go, 10 days Tanukis refactoring crash-course!

After applying a few changes, here's how I have structured the project. I tried to separate each layer of the application (view, model, ...) :

Eclipse project screen capture

Better isn't it ?

The backend, Zend AMF

The first thing I did is to code a model, some value object that open the door to class mapping between Flash => PHP => MySQL.

Here is a typical PHP class:

  1. /**
  2.  * Value object that map a 'users' db tupple to the AS User class
  3.  * @package Services
  4.  * @subpackage ValueObject
  5.  */
  6. class User
  7. {
  8. /**
  9. * @var string
  10. */
  11. public $u_id="";
  12.  
  13. /**
  14. * @var string
  15. */
  16. public $u_email="";
  17.  
  18. /**
  19. * @var string
  20. */
  21. public $u_password="";
  22.  
  23. /**
  24. * @var string Enum, 'super_admin','admin','customer'
  25. */
  26. public $u_type="";
  27.  
  28. /**
  29. * @var string
  30. */
  31. public $id_campaign="";
  32.  
  33. /**
  34. * @var string ZendAMF type to pass to flash
  35. */
  36. public $_explicitType = 'User';
  37.  
  38. /**
  39. * ZendAMF internal function used in class mapping
  40. */
  41. public function getASClassName()
  42. {
  43. return 'User';
  44. }
  45.  
  46. //here buisness method to ease type casting between flash and php
  47. }

Flash side, you must use the Bindable metatag, with RemoteClass:

  1. //User.as
  2.  
  3. [Bindable]
  4. [RemoteClass(alias="User")]
  5. public class User extends RootModel{
  6.  
  7. public var u_id:String;
  8. public var u_email:String;
  9. public var u_password:String;
  10. public var u_type:String;
  11. public var id_campaign:String;
  12.  
  13. public function User(){
  14. super();
  15. }
  16. }

That's all, Zend do all the job behind the ground.

You just have to init class mapping with this kind of code in your AMF endpoint (I wrote a small how-to setup ZendAMF some times ago)

$oServer = new Zend_Amf_Server();
...
$oServer->setClassMap('User', 'User');

If you need to debug your service, try PHPSimpleTest, to have an easy to setup first php exec env, or ZamfBrowser, an air app that allows you to test your AMF web-services.

During my search on the model definition, I tried this very interesting solution that uses a dynamic class (you can dynamically add properties) and extends the proxy class, which allows data-binding with these dynamic fields.

But as my model is fixed, persisted in base, why not benefit from the Flex intellisense, and define precisely these fields ?

As is, an object does not dispatch an event if a property change, you must implement IEventDispatcher to be able to bind data together. So, here is my RootModel class:

  1. public class RootModel implements IEventDispatcher {
  2.  
  3. protected var eventDispatcher:EventDispatcher;
  4.  
  5. public function RootModel(){
  6. eventDispatcher = new EventDispatcher(this);
  7. }
  8.  
  9. public function addEventListener(type:String, listener:Function, useCapture:Boolean=false, priority:int=0, useWeakReference:Boolean=false):void{
  10. eventDispatcher.addEventListener(type, listener, useCapture, priority, useWeakReference);
  11. }
  12.  
  13. public function removeEventListener(type:String, listener:Function, useCapture:Boolean=false):void{
  14. eventDispatcher.removeEventListener(type, listener, useCapture);
  15. }
  16.  
  17. public function dispatchEvent(event:Event):Boolean{
  18. return eventDispatcher.dispatchEvent(event);
  19. }
  20.  
  21. public function hasEventListener(type:String):Boolean{
  22. return eventDispatcher.hasEventListener(type);
  23. }
  24.  
  25. public function willTrigger(type:String):Boolean{
  26. return eventDispatcher.willTrigger(type);
  27. }
  28. }

Mate : easy and powerful

Mate is a Flex framework, which I find easy to learn, and that immediately gives a structure to your project.

I chose to implement MVC, but Mate may well adapt to other software architectures, such as the very popular Model-View-ViewModel (MVVM).

Mate works very well with Flex 4. I wondered if I should upgrade the Flex version, but I did not dare. Maybe I should have ...

Which makes Mate so easy to use is that everything is written in mxml, making the developer particularly productive.

To invoke a web service for example, just write that kind of tags:

  1. <EventHandlers type="{ConfigLoadedEvent.CONFIG_LOADED}" debug="true">
  2.  
  3. <RemoteObjectInvoker destination="zend" source="ConfigLoader" method="getCampaignData" arguments="{[event.configId]}" debug="true">
  4. <resultHandlers>
  5. <MethodInvoker generator="{BDDModelManager}" method="setCampaign" arguments="{resultObject}" />
  6. <MethodInvoker generator="{TabScreensaver}" method="init" />
  7. <MethodInvoker generator="{TabAnimation}" method="initEditor" />
  8. </resultHandlers>
  9. </RemoteObjectInvoker>
  10.  
  11. </EventHandlers>

Mate is based on the EventMap class, which is instantiate with a tag in the WindowedApplication of your project:

 <mx:WindowedApplication
    xmlns:mx="http://www.adobe.com/2006/mxml"
    xmlns:maps="com.tanukis.leth.configurator.events.map.*">
   
    <maps:MainEventMap />

You have to create an mxml component, like MainEventMap.mxml:

  1. <EventMap xmlns:mx="http://www.adobe.com/2006/mxml" xmlns="http://mate.asfusion.com/">
  2.  
  3. <Debugger level="{Debugger.ALL}" />
  4.  
  5. <EventHandlers type="{FlexEvent.PREINITIALIZE}" debug="true">
  6. <ObjectBuilder generator="{ FileConfigHelper }" />
  7. <ObjectBuilder generator="{ FileConfig }" constructorArguments="{[ scope.dispatcher, lastReturn]}" />
  8. <ObjectBuilder generator="{ HelpManager }" constructorArguments="{ scope.dispatcher }" />
  9. <MethodInvoker generator="{ HelpManager }" method="loadContent" />
  10. ...
  11. </EventHandlers>
  12. ...

This class will be the backbone of your project.

It will catch all the important events, and respond with methods call. It allows to decouple components, to separate responsibilities, and thus to establish MVC.

One of the difficulties is to access a class or an instance reference in the EventMap.

That's why we create here a listener on the pre-initializes event. We create all of our controllers, and so, will have references to them to invoke when we need to accomplish a task (persist data, display a popup , ...).

Once Mate has created an object, it keeps a cached instance, creating a registry, in which we can call an instance when we need it.

For each tag, Mate creates the variable "lastReturn", which is used here to pass a FileConfigHelper reference to the constructor of  FileConfig.

The variable "scope.dispatcher" is the source of the event, here the WindowedApplication. We will then use this reference to emit events that can be caught in the EventMap.

In order to decouple the components, we use dependency injection with the "Injectors" tag:

  1. <Injectors target="{MenuHandler}" debug="true">
  2. <PropertyInjector targetKey="model" source="{FileConfig}" />
  3. <PropertyInjector targetKey="bddmodel" source="{BDDModelManager}" />
  4. </Injectors>

When an instance of MenuHandler will be created, Mate will inject a reference to FileConfig and BDDModelManager. Easy and Powerfull...

Here are some bulk tag that were useful to me:

  1. <EventHandlers type="{UiReadyEvent.UI_READY}" debug="true">
  2. <ObjectBuilder generator="{ TabDotation }" />
  3. <!-- Call a static method -->
  4. <InlineInvoker method="{Configurator.setDotationTab}" arguments="{ lastReturn }" />
  5. </EventHandlers>
  6.  
  7. <EventHandlers type="{CommandEvent.CREATE_EMPTY_CAMPAIGN}" debug="true">
  8. <!-- Keep a reference of the event Mate variable, to use within the inner tag. Data is another Mate special variable -->
  9. <DataCopier source="event" sourceKey="data" destination="data" destinationKey="campaign" />
  10. <RemoteObjectInvoker destination="zend" source="ConfigWriter" method="writeEmptyCampaign" arguments="{[data.campaign.name,data.campaign.path]}" debug="true">
  11. <resultHandlers>
  12. ...
  13. </resultHandlers>
  14. </RemoteObjectInvoker>
  15. </EventHandlers>
  16.  
  17. <EventHandlers type="{TabEvent.ITEM_CLICK}" debug="true">
  18. <!-- Stop the event flow if stopEventIfTabIndexisNotTabNetwork function, defined in the event map, return true -->
  19. <StopHandlers stopFunction="_stopEventIfTabIndexisNotTabNetwork" />
  20. <RemoteObjectInvoker destination="zend" source="ScanTerminal" method="result" debug="true">
  21. ...
  22. </RemoteObjectInvoker>
  23. </EventHandlers>
  24.  
  25. ...

I created a CommandEvent, that will trigger an action in the EventMap:

  1. public class CommandEvent extends Event
  2. {
  3. public static const SAVE:String="com.tanukis.event.command.save";
  4. public static const HELP_NEXT_STEP:String = "com.tanukis.event.help-next-step";
  5. public static const CREATE_EMPTY_CAMPAIGN:String = "com.tanukis.event.create-empty-campaign";
  6. ... //many more actions
  7.  
  8. // "Un-typed" data, but castable with "as"
  9. public var data:Object;
  10.  
  11. public function CommandEvent(type:String, data:Object=null)
  12. {
  13. //We need the event to bubble, so that Mate can catch it. We make it cancelable to.
  14. super(type, true, true);
  15. this.data=data;
  16. }
  17.  
  18. }

Data-binding

Like all my views and controllers share a common reference to the model, I do not need to loop, move some variable from one part of the application to another (plumbing code ...). Whatever end-user action, the data is read from and written to a single model. It avoids a lot of bugs, since the view always reflect ongoing state of the model...

Flex 4 introduces the birectionnal data-binding , with the "@" sign.

With Flex 3 we can simulate a two-way databinding, at the cost of a slight loss in readability, using the change event:

<mx:TextInput text="{bddModel.campaign.name}" change="{bddModel.campaign.name=(event.currentTarget as TextInput).text }" />

Most of my views are connected to the model in this way, using "event.currentTarget as ..." to write within in.

Here it is, completed this short introduction to the Mate Flex framework.

We must always ask ourselves, how many days to debug plumbing code ?

What will be the final cost of a ill-conceived project ? Would it not be better to go back on solid ground, satisfy the customer, and opening the door to evolution ? How to update a poorly designed project ? ... Happy refactoring!