Skip to main content

[Flex] pureMVC and Utility - StateMachine

目前手上的 Flex 專案幾乎都是使用 pureMVC 的架構來開發,用到現在還是沒有發現什麼大的缺點,嚴格說起來還挺好用的。組合好心人分享的 pureMVC 工具包也相當好玩。除了自己寫的工具外,目前最愛用的便是 Neil Manuell (project owner) 所分享的 Utility - AS3 StateMachine,小型專案只需要配合這個就非常完美了~~

在 Application 的 MXML 中使用 ViewStack 來控制場景變換,是很直覺的作法。但如果你專案還有擴充的空間,這樣做場景到最後你一定後悔!因為可能不是每個 View 都會被新增到 同一個地方。以下是一個簡單的範例講解 pureMVC + StateMachine 的應用。

範例的 Classes tree


開始前請先下載以下的 library:為了方便開發,統一使用 multicore 版本
PureMVC MultiCore for AS3
Utility - AS3 StateMachine
使用 swc 的話請直接將 .swc 檔放置到 libs/ 內,如果是下載 class 的話,請放置到 src/ 中

範例場景說明:
Application MXML 中有個 tab bar 點選後會換場景 Shop <-> Shop1 ( 當然場景可以無限新增,這只是個範例...)

pureMVCAndStateMachine.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
applicationComplete="init()" layout="vertical">
<mx:Script>
<![CDATA[
import mx.events.ItemClickEvent;
import com.mvc.ApplicationFacade;
private function init():void{
ApplicationFacade.getInstance( ApplicationFacade.NAME ).startup( this );
}
]]>
</mx:Script>
<mx:TabBar dataProvider="['Shop','Shop1']" id="tb"
itemClick="dispatchEvent( new ItemClickEvent('MenuClick', false, false, null, tb.selectedIndex ))" />
</mx:Application>


你會發現在 itemClick 後使用了 dispatchEvent ,而不直接讓 ApplicationMediator 來監聽 tb,當 Mediator 不了解 viewComponent 的實作細節,這樣的寫法對之後更改 Application view 配置較不會互相影響。

ApplicationFacade.as 幾乎跟官方長得一模一樣...=P
package com.mvc
{

import com.mvc.controls.StartupCommand;
import mx.core.UIComponent;

import org.puremvc.as3.multicore.interfaces.IFacade;
import org.puremvc.as3.multicore.patterns.facade.Facade;

public class ApplicationFacade extends Facade implements IFacade
{
public static var NAME:String = "SystemFacade";
// Notification constants
public static const STARTUP:String = 'startup';

public function ApplicationFacade( key:String = null )
{
super( NAME );
}

/**
* Singleton ApplicationFacade Factory Method
*/
public static function getInstance( key:String = null ) : ApplicationFacade
{
if ( instanceMap[ key ] == null ) instanceMap[ key ] = new ApplicationFacade( key );
return instanceMap[ key ] as ApplicationFacade;
}

/**
* Register Commands with the Controller
*/
override protected function initializeController() : void
{
super.initializeController();
registerCommand( STARTUP, StartupCommand );
}

/**
* Application startup
*
* @param app a reference to the application component
*/
public function startup( app:UIComponent ):void
{
sendNotification( STARTUP, app );
}
}
}


public function startup( app:UIComponent):void
{
sendNotification( STARTUP, app );
}


這邊修改了 startup function 為了統一使用 ApplicationFacade.as 就不綁各自的 Application.mxml 實體 (ex. pureMVCAndStateMachine),所以將傳入改為 UIComponent 類別 ( 這樣每開一個專案就是直接 copy 改 package 就可以使用了...=) )

StartupCommand.as
package com.mvc.controls
{
import org.puremvc.as3.multicore.patterns.command.MacroCommand;

public class StartupCommand extends MacroCommand
{
public function StartupCommand()
{
super();
}

override protected function initializeMacroCommand() :void
{
//addSubCommand( ModelPrepCommand ); //請自己補
addSubCommand( ViewPrepCommand );
addSubCommand( InjectFSMCommand );
}
}
}


State Machine 的 inject command
addSubCommand( InjectFSMCommand );

使用 State Machine 的時候, Erin 都習慣寫一張 const 表來用

GlobalStates.as

package com.mvc.utils
{
public class GlobalStates
{
private static const NAME:String = "GlobalStates";
public static const SHOP1:String = "/shop1";
public static const SHOP:String = "/shop";

private static const ACTION:String = "/action";
public static const ACTION_SHOP1:String = NAME +ACTION + SHOP1;
public static const ACTION_SHOP:String = NAME +ACTION + SHOP;

private static const DISPLAY:String = "/display";
public static const DISPLAY_SHOP1:String = NAME +DISPLAY + SHOP1;
public static const DISPLAY_SHOP:String = NAME +DISPLAY + SHOP;

private static const EXIT:String = "/exit";
public static const EXIT_SHOP1:String = NAME +EXIT + SHOP1;
public static const EXIT_SHOP:String = NAME +EXIT + SHOP;

public function GlobalStates(){}
}
}


InjectFSCommand.as 寫法很統一~~


package com.mvc.controls
{
import org.puremvc.as3.multicore.interfaces.INotification;
import org.puremvc.as3.multicore.patterns.command.SimpleCommand;
import org.puremvc.as3.multicore.utilities.statemachine.FSMInjector;

import com.mvc.utils.GlobalStates;
import com.mvc.controls.Shop1Command;
import com.mvc.controls.ShopCommand;

/**
* Create and inject the StateMachine.
*/
public class InjectFSMCommand extends SimpleCommand
{
override public function execute ( note:INotification ) : void
{
//將所有相關的 Command 直接在這邊 register
facade.registerCommand( GlobalStates.DISPLAY_SHOP1, Shop1Command );
facade.registerCommand( GlobalStates.EXIT_SHOP1, Shop1Command );
facade.registerCommand( GlobalStates.DISPLAY_SHOP, ShopCommand );
facade.registerCommand( GlobalStates.EXIT_SHOP, ShopCommand );
// Create the FSM definition
var fsm:XML =
<fsm initial={GlobalStates.SHOP}>
<state name={GlobalStates.SHOP} exiting={GlobalStates.EXIT_SHOP} changed={GlobalStates.DISPLAY_SHOP}>
<transition action={GlobalStates.ACTION_SHOP1} target={GlobalStates.SHOP1}/>
</state>
<state name={GlobalStates.SHOP1} exiting={GlobalStates.EXIT_SHOP1} changed={GlobalStates.DISPLAY_SHOP1}>
<transition action={GlobalStates.ACTION_SHOP} target={GlobalStates.SHOP}/>
</state>
</fsm>;
// Create and inject the StateMachine
var injector:FSMInjector = new FSMInjector( fsm );
injector.initializeNotifier(this.multitonKey);
injector.inject();
}
}
}


使用 State Machine 有個好處就是 state tag 內,如果沒有將要串接的 State 用 transition tag 宣告出來 ,就不可能會跳到那個 State...
<state name={GlobalStates.SHOP} exiting={GlobalStates.EXIT_SHOP} changed={GlobalStates.DISPLAY_SHOP}>
<transition action={GlobalStates.ACTION_SHOP1} target={GlobalStates.SHOP1}/>
</state>


將 DIPLAY and EXIT 註冊到各自的 view Command
facade.registerCommand( GlobalStates.DISPLAY_SHOP1, Shop1Command );
facade.registerCommand( GlobalStates.EXIT_SHOP1, Shop1Command );
facade.registerCommand( GlobalStates.DISPLAY_SHOP, ShopCommand );
facade.registerCommand( GlobalStates.EXIT_SHOP, ShopCommand );


裝好 State Machine 後,來寫 ViewPrepCommand.as
package com.mvc.controls
{
import com.mvc.views.ApplicationMediator;

import mx.core.UIComponent;

import org.puremvc.as3.multicore.interfaces.INotification;
import org.puremvc.as3.multicore.patterns.command.SimpleCommand;


public class ViewPrepCommand extends SimpleCommand
{
override public function execute( note:INotification ) :void
{
var app:UIComponent = note.getBody() as UIComponent ;
facade.registerMediator( new ApplicationMediator (ApplicationMediator.NAME , app) );
}
}
}


ShopCommand.as and Shop1Command.as 的寫法其實很一致,就是將 Shop and Shop1 的相關的 views、mediators、proxies and commands 啟用與移除都在自己的 command 運作

ShopCommand.as ( Shop1Command.as 就差 Class 名稱改一下 )
package com.mvc.controls
{
import org.puremvc.as3.multicore.interfaces.ICommand;
import org.puremvc.as3.multicore.patterns.command.SimpleCommand;
import org.puremvc.as3.multicore.interfaces.INotification;
import com.mvc.utils.GlobalStates;

public class ShopCommand extends SimpleCommand implements ICommand
{
public function ShopCommand()
{
super();
}

override public function execute(notification:INotification):void
{
switch( notification.getName() ){
case GlobalStates.DISPLAY_SHOP:
// init shop view...請自己補~~
break;
case GlobalStates.EXIT_SHOP:
// remove shop view 請自己補~~
break;
}
}

}
}


Command 都準備好囉~~最後將 ApplicationMediator.as 寫上~~
package com.mvc.views
{
import flash.display.DisplayObject;

import mx.core.UIComponent;
import mx.events.ItemClickEvent;

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

import org.puremvc.as3.multicore.utilities.statemachine.StateMachine;

import com.mvc.utils.GlobalStates;

public class ApplicationMediator extends Mediator implements IMediator
{
public static const NAME:String = "ApplicationMediator";

public function ApplicationMediator(mediatorName:String=null, viewComponent:Object=null)
{
super(mediatorName, viewComponent);
}
override public function onRegister():void
{
//這樣 Mediator 就不需要認識 viewComponent 內的 Children
app.addEventListener( 'MenuClick' , onClick );
}
// 直接發 StateMachine.ACTION notification...
private function onClick(event:ItemClickEvent):void{
switch( event.index ){
case 0:
this.sendNotification( StateMachine.ACTION , null, GlobalStates.ACTION_SHOP );
break;
case 1:
this.sendNotification( StateMachine.ACTION , null, GlobalStates.ACTION_SHOP1 );
break;
}
}
private function get app():UIComponent{
return this.viewComponent as UIComponent;
}
}
}


StateMachine 的用法:
sendNotification( StateMachine.ACTION , null, "ACTION_STATE" );

你一定會問,為什麼不直接將 view 生成移除的語法寫在 ApplicationMediator.as 內?
如果將處理 view 註冊跟移除收集在 Mediator 內,等到 view 需要被新增到不同的 UI containder 內的時候,你就會發現處理起來會非常痛苦,有時候客戶一時心血來潮告知你說能不能將 A view 放到左邊又或者 b view 被改到別的 view component 內的時候,這時候也只需要修改 command 即可!

其他 pureMVC 相關文章:
[Flex] pureMVC Standard 練習筆記
[Flex] pureMVC 練習筆記啪兔
[Flex] Cairngorm v.s. pureMVC
[Flex] pureMVC MultiCore with Modules

Comments

  1. Hello Erin:
    我是pureMVC的新手,拜讀你的文章後,學到蠻多的。
    有一個問題想請教你,目前我想做一個像一般Windows AP的介面,有menu,點了menu item後,會開啟MDI Form,每個MDI Form(i功能)都是獨立的,所以我選擇了multicore的版本,但我確定menu的eventhandel是否要納入Mediator的管轄呢?~~是否可以給個建議,或是有什麼文章可以參考。
    謝謝

    ilin

    ReplyDelete
  2. 如果是我大概會處理成 menu + MenuMeditaor(只用接收 menu click 事件後 sendNotification),再由 command 處理呼叫 對應 form ui 的開啟跟關閉
    每個 MDI form - FormMediator 都自己配一組,這樣要更換 Form 會比較容易...

    ReplyDelete

Post a Comment

Popular posts from this blog

[Flex] PureMVC standard with Spring extensions

由於上次稍微玩了一下 Robotlegs 依賴注入(DI) 主導的 MVC 框架,而著名也使用依賴注入的 Java / Java EE 的 Spring framework 出了 for ActionScript 的版本,剛好在最近 Spring ActionScript 1.0 正式 release 了(想了解 Spring 是啥咪東東的話請自行找 google 大神),這個版本除了基本框架外,也包含了 Cairngorm 與 PureMVC 的外掛...想當然耳,就拿來測試一下用在 PureMVC 內的感覺囉!! 參考了 官方範例 中 PureMVC 唯二的範例原始檔,以下使用的是「設定檔依賴注入 facade 透過 addConfigSource() 的方式來 init 」:(其實除了 embed 外,都是外部載入) Online Demo with source code 工作環境:FlashBuilder, Flex SDK4 請下載 PureMVC Standard 版本 再下載 Spring ActionScript 最新版本後,除了 spring-actionscript-cairngorm 不需要外,都放到 /src 下(記得只需要 org 開始...),也別忘了lib 內的 swc 檔 copy 到 /libs 下 Spring 的 injection 並不像 Robotlegs 直接來個 [Inject] metadata 的自動化那樣方便,但是其冷血度(檔案的鬆偶程度)更勝後者!如果你要使用設定檔(applicationContext.xml) 來做注入的話,準備工作就挺多的...XD 依照 applicationContext.xml 內設定的方式分別寫入 constructor 或者是 setter 依賴注入(本範例統一使用 setter injection) 為了跟大家都沒關係所以都使用 interface 來處理,所以你會在範例中發現大家都有介面...(並沒有真的研究過 Spring,也許還有其他作法) 準備 compiler 時候要用的 classe。由於在 setter, getter 的寫法上都使用 interface,所以真正用到的 class 需要預先在輸出階段就打包到程式內。 基本上 PureMVC 類 class...

[543] 最近的 Erin 在做什麼?

最近 Erin 噗浪玩很大,都忘了要寫 blog... 自從四月初開始認真當 SOHO 後,每天都過著"醉生夢死"的生活...?? 目前的行程: 五六月 - 滿檔中... 七月( 每週一三晚間 ) - 授課 飛肯 ActionScript 3.0 & XML 資料庫整合應用班 (會準備什麼隱藏課程?你來了就知道...XD ) 七八九月 - 短期約聘面談中 十月 - 等你聯絡囉~~^^ 近期如果你有"不急的" Flex / AIR / Flash / Flash lite 相關的案子需要外發都可以直接與我聯絡!也接受短期約聘喔! 1 ) 如何聯絡 Erin? 寄 e-mail 是最快的喔!不然可以透過 blog 上的 Plugoo 視窗與我聯絡,再來就是噗浪...XD E-mail: erinylin [at] gmail [dot] com Plurk: http://www.plurk.com/erinlin 2 ) 為什麼不公開留 MSN...?? 嗯...因為 Erin 記憶不好,外加 MSN 一堆幾百年沒有聯絡的朋友名單...所以還是先透過 e-mail 聯絡一下再加入比較記得... 3 ) Erin 到底會什麼? 啊...這個問題就有點難回答了,Erin 古早以前是個美術設計師,也當過電腦兼職講師,後來轉職當互動工程師,現在主要是做 AS1/AS2/AS3 "都可以"的前端互動開發(程式為主)。凡舉 Flex / AIR / Flash / Flash lite 相關的案子都做過,整合過很多專案,動畫也製作過(人物設定+動畫製作),在手機產業界接觸了 UI 設計與 UE 研究,最近經手的案子有國外活動網站(AS3+pv3D)中文化,公司進銷存系統網路化(Flex)...所以 Erin 到底能做什麼?嗯...就等你來研究囉~~ 4 ) 為什麼沒有作品集可以看? 這個就要問問為什麼一堆公司都要你簽 NDA (保密協定) ,搞的 Erin 也懶得公開經手過的作品,所以想要看到 Erin 做過什麼,就請記得找 Erin 面談時要註明『請帶電腦』這四個字 =) 裏 1 ) 目前 Erin 缺什麼? 嗯...缺男人(咦?這才是這篇的重點嗎?)