Refactoring a Flex project with Mate, ZendAMF and MVC
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, ...) :
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:
/** * Value object that map a 'users' db tupple to the AS User class * @package Services * @subpackage ValueObject */ class User { /** * @var string */ public $u_id=""; /** * @var string */ public $u_email=""; /** * @var string */ public $u_password=""; /** * @var string Enum, 'super_admin','admin','customer' */ public $u_type=""; /** * @var string */ public $id_campaign=""; /** * @var string ZendAMF type to pass to flash */ public $_explicitType = 'User'; /** * ZendAMF internal function used in class mapping */ public function getASClassName() { return 'User'; } //here buisness method to ease type casting between flash and php }
Flash side, you must use the Bindable metatag, with RemoteClass:
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:
public function RootModel(){ } eventDispatcher.addEventListener(type, listener, useCapture, priority, useWeakReference); } eventDispatcher.removeEventListener(type, listener, useCapture); } return eventDispatcher.dispatchEvent(event); } return eventDispatcher.hasEventListener(type); } return eventDispatcher.willTrigger(type); } }
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:
<EventHandlers type="{ConfigLoadedEvent.CONFIG_LOADED}" debug="true"> <RemoteObjectInvoker destination="zend" source="ConfigLoader" method="getCampaignData" arguments="{[event.configId]}" debug="true"> <resultHandlers> <MethodInvoker generator="{BDDModelManager}" method="setCampaign" arguments="{resultObject}" /> <MethodInvoker generator="{TabScreensaver}" method="init" /> <MethodInvoker generator="{TabAnimation}" method="initEditor" /> </resultHandlers> </RemoteObjectInvoker> </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:
<EventMap xmlns:mx="http://www.adobe.com/2006/mxml" xmlns="http://mate.asfusion.com/"> <Debugger level="{Debugger.ALL}" /> <EventHandlers type="{FlexEvent.PREINITIALIZE}" debug="true"> <ObjectBuilder generator="{ FileConfigHelper }" /> <ObjectBuilder generator="{ FileConfig }" constructorArguments="{[ scope.dispatcher, lastReturn]}" /> <ObjectBuilder generator="{ HelpManager }" constructorArguments="{ scope.dispatcher }" /> <MethodInvoker generator="{ HelpManager }" method="loadContent" /> ... </EventHandlers> ...
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:
<Injectors target="{MenuHandler}" debug="true"> <PropertyInjector targetKey="model" source="{FileConfig}" /> <PropertyInjector targetKey="bddmodel" source="{BDDModelManager}" /> </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:
<EventHandlers type="{UiReadyEvent.UI_READY}" debug="true"> <ObjectBuilder generator="{ TabDotation }" /> <!-- Call a static method --> <InlineInvoker method="{Configurator.setDotationTab}" arguments="{ lastReturn }" /> </EventHandlers> <EventHandlers type="{CommandEvent.CREATE_EMPTY_CAMPAIGN}" debug="true"> <!-- Keep a reference of the event Mate variable, to use within the inner tag. Data is another Mate special variable --> <DataCopier source="event" sourceKey="data" destination="data" destinationKey="campaign" /> <RemoteObjectInvoker destination="zend" source="ConfigWriter" method="writeEmptyCampaign" arguments="{[data.campaign.name,data.campaign.path]}" debug="true"> <resultHandlers> ... </resultHandlers> </RemoteObjectInvoker> </EventHandlers> <EventHandlers type="{TabEvent.ITEM_CLICK}" debug="true"> <!-- Stop the event flow if stopEventIfTabIndexisNotTabNetwork function, defined in the event map, return true --> <StopHandlers stopFunction="_stopEventIfTabIndexisNotTabNetwork" /> <RemoteObjectInvoker destination="zend" source="ScanTerminal" method="result" debug="true"> ... </RemoteObjectInvoker> </EventHandlers> ...
I created a CommandEvent, that will trigger an action in the EventMap:
{ ... //many more actions // "Un-typed" data, but castable with "as" { //We need the event to bubble, so that Mate can catch it. We make it cancelable to. super(type, true, true); this.data=data; } }
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!