Pages

Sunday, August 10, 2008

MVC in Flex

In Flex you can put all your code in one mxml. Off course this works perfectly but is not easy to maintain and all the components are tidely connected. The Model View Controller design pattern can help you to split up the flex application so we have loosely coupled and cleaner components. These components can be easily reused without breaking up the whole application. With MVC design pattern helps you to make a big flex application without having a code nightmare.
Here is an example of a simple mxml application where the label get it's data directly from the list.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:List id="list" >
<mx:dataProvider>
<mx:Array>
<mx:Object label="one"/>
<mx:Object label="two"/>
</mx:Array>
</mx:dataProvider>
</mx:List>
<mx:Label text="{list.selectedItem.label}"/>
</mx:Application>

Let's change this Flex application in an Model View Controller application. I know this is an overkill for this example but in the large application you will benefit from this approach. Create an new component List1 in the new folder view.

<?xml version="1.0" encoding="utf-8"?>
<mx:List xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:dataProvider>
<mx:Array>
<mx:Object label="one"/>
<mx:Object label="two"/>
</mx:Array>
</mx:dataProvider>
</mx:List>

Do the same for the Label and name this component Label1. Store Label1 in the view folder.

<?xml version="1.0" encoding="utf-8"?>
<mx:Label xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:text>empty</mx:text>
</mx:Label>

We can use these components after adding a namespace to the application mxml.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:view="view.*">
<view:List1/>
<view:Label1/>
</mx:Application>

This time we have the same application but there is no interaction between the components. We can change this by fire an event when the list is changed. This event can be captured by the controller which stores the new list value in the model. Events make these components loosely coupled. The label component can retrieve the new list value from the model or actionscriptcan monitor the list value of the model so when it is changed, it can update a local variable.
Create a new event and fire this event when the list component is changed.
Let's create a new actionsript class ChangeEventClass which extends from Event. Store this class in the event folder. We want to pass the new value of the list to the other classes. So we need to build a custom event.

package event
{
import flash.events.Event;
public class ChangeEventClass extends Event {

public static const name:String = "changeEvName";
public var newValue:String;
public function ChangeEventClass( prop:String
, type:String
, bubbles:Boolean=false
, cancelable:Boolean=false){
newValue = prop;
super(type, bubbles, cancelable);
}
}
}

Now we can change the list component where we use the change property to fire the new event. It is very important to set the bubbles parameter to true else the Controller class can not capture the event. Default only the parent component can listen for this event.

<?xml version="1.0" encoding="utf-8"?>
<mx:List xmlns:mx="http://www.adobe.com/2006/mxml" change="changeList(event);">
<mx:Script>
<![CDATA[
import event.ChangeEventClass;

private function changeList(event:Event):void {
var eventObj:ChangeEventClass =
new ChangeEventClass( selectedItem.label
, ChangeEventClass.name
, true);
dispatchEvent(eventObj);
}
]]>
</mx:Script>

<mx:dataProvider>
<mx:Array>
<mx:Object label="one"/>
<mx:Object label="two"/>
</mx:Array>
</mx:dataProvider>
</mx:List>

We want to capture this event and store the value in the model. Let's create the model first. The model has to be a singleton class so an other component can retrieve this value. Create a new actionscript class Global and put this class in the model folder. The listValue has to be bindable else we can not retrieve this value from the label component.

package model
{
public class Global
{
public function Global(){
if( instance != null ){
throw( new Error( "there can be only one instance of global" ) );
}
}

private static var instance:Global;

[Bindable]
public var listValue:String;

public static function getInstance():Global{
if( instance == null ){
instance = new Global();
}
return instance;
}
}
}

The next step is to create a Controller class and put this class in the controller folder. In this class we will capture the list change event and store the new value in the model variable.

package controller
{
import event.ChangeEventClass;
import flash.events.Event;
import model.Global;
import mx.core.UIComponent;
import mx.events.FlexEvent;

public class Controller extends UIComponent{
public function Controller() {
addEventListener( FlexEvent.CREATION_COMPLETE, setupEventListeners );
}
private function setupEventListeners( event:Event ):void {
systemManager.addEventListener(ChangeEventClass.name,changeEvNameHandler);
}
private function changeEvNameHandler(listEvent:ChangeEventClass):void {
Global.getInstance().listValue = listEvent.newValue;
}
}
}

The application mxml will now look like this. We can import the Controller class to this mxml.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:view="view.*"
xmlns:controller="controller.*">
<controller:Controller/>
<view:List1/>
<view:Label1/>
</mx:Application>

We only have to change the label component so its retrieve its text value from the model class.

<?xml version="1.0" encoding="utf-8"?>
<mx:Label xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
<![CDATA[
import model.Global;
]]>
</mx:Script>

<mx:text>{Global.getInstance().listValue}</mx:text>
</mx:Label>

Or we can listen when the listValue changes in the model class.

<?xml version="1.0" encoding="utf-8"?>
<mx:Label xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
<![CDATA[
import model.Global;
import mx.events.PropertyChangeEvent;
import mx.binding.utils.ChangeWatcher;

private var stateWatcher:ChangeWatcher = ChangeWatcher.watch( Global.getInstance()
, "listValue"
, handle_stateChange );
private function handle_stateChange( event:PropertyChangeEvent ):void {
text = Global.getInstance().listValue;
}
]]>
</mx:Script>
</mx:Label>

Now we have a MVC flex application.

3 comments:

  1. I am attempting to have the controller dispatch the event. Could you tell me why it is not working? Thanks.

    Alteration to List1:
    change="myController.changeList(selectedItem.label, event)"


    Addition to Controller.as:

    public function changeList(label:String, event:Event):void
    {
    var eventObj:ChangeEventClass = new ChangeEventClass( label
    , ChangeEventClass.name
    , true);
    dispatchEvent(eventObj);
    }

    ReplyDelete
  2. I think that you maybe have the bubbles parameter of the event not on true.
    or you have a problem by dispatching the event . I know an event from a component to controler works fine the other way around does not work.

    Please try my the sample application of Cairngorm MVC framework blog entry. Then you can use CairngormEventDispatcher.getInstance().dispatchEvent(new ChangeEvent( label , ChangeEventClass.name ))This will work.

    ReplyDelete
  3. This is a really great article. Great way to think about FLEX architecture. Thank you!

    ReplyDelete