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

[Swift3] weak 與 unowned 關鍵字

雖然在 Swift 中看起來"很像"是不需要煩惱內存管理的問題,不過實際上它還是遵循著自動引用計數 (ARC) 的規則,當一個物件沒有被其他對象引用時會自動被銷毀,如果三魂七魄沒有完全回位的話,就會有個靈體留在現世的空間裡,最經典的範例如下: 閉包(Closure)引用 classClassA { typealias Complete = ()->() var name : String var onComplete : Complete? init(_ name: String){ self.name = name print("Hello I am \(self.name)") onComplete = { print("\(self.name): onComplete!") // --> 閉包引用 self, 計數 + 1 } } deinit { print("deinit: \(self.name)") } } var a : ClassA? = ClassA("A") // --> 引用計數 + 1 a = nil // 2-1 = 1 還剩下 1 所以沒辦法銷毀 ---output------- Hello I am A 由於這邊的 onComplete 宣告為 Optional, 正確的做法要連同 onComplete 一起刪除才可以被回收,若不是 Optional 則會進入無法回收狀態: var b : ClassA? = ClassA("B") b?.onComplete = nil // --> 還好是 Optional 可以設成 nil 計數 - 1 b = nil // 計數 = 0 所以被回收 ---output------- Hello I am B deinit: B 但是做人不需要煩惱太多,這時候就出動 unowned 關鍵字讓物件可以順利被回收: onComplete = { [unowned self] in print...

[作品]Flex 3.4 + PureMVC 的企業級 RIA 應用

認識 Erin 的人都知道, Erin 最懶得寫作品集相關的文章,但是這個案子比較有趣,所以特別提出來聊一下囉! 廚櫃業某公司內部軟體,目前系統尚在 Beta 階段,所以下面圖片皆有模糊化處理。 [可擴充功能面板首頁,後台皆包含在管理系統內(有 iPhone 的 fu)] 這個案子是幫一家傳統公司做的專用軟體線上化,想要開發企業級 RIA 想當然爾就是使用 Flex 來處理囉! 整個案子前端是以 Flex SDK 3.4 + Flex Builder3, PureMVC MultiCore for AS3 , Utility - AS3 StateMachine 以及先前 Erin 所發表的 WidgetsConsole 來處理整個系統的轉換。 整個系統包含前後台共七大系統,分別是:估價,客戶,店面管理,貨品上架,會員管理,分店系統以及權限系統。超級管理員可以利用權限系統分別開出不同職級的權限,然後使用者透過同一入口,依照權限區分,畫面會顯示不同的功能。整個系統最複雜的部份當然是估價系統,由於櫥櫃的建立是透過選擇資料即時演算出來,更何況櫥櫃類的 BOM 表階層是怪物般的可怕...所以在 DB 的建置與前端資料整合的部分著實費了我們一番苦心。 [櫥櫃 BOM 表編輯視窗及櫥櫃 BOM 單物件材質指定] 此外,在 UI 設計上也特別花了一些心思。本案子 Target User 是管理者、店長與公司旗下的業務群,此公司成員年齡約 30-60 歲,皆為一般電腦使用者,所以 UI 設計及操作 Flow 必須比一般線上 RIA 更加友善,才能幫助縮減公司的人事教育花費。如放大整個系統的標準字 size;新增資料時不能碰的快速鍵會鎖定;產品類縮圖雙點擊即可觀看放大圖;操作 flow 皆以左上至右下結束以及利用畫面引導使用者操作都是一些設計上的巧思。 [新增估價單] [簡單易用的業務員用客戶系統] 此外這個專案在實作 PureMVC 過程當然是相當的快樂,因為它的彈性與權職切分實在太令人滿意了!不管是擴充性,Rich Data 管理又或者除錯都非常的容易。當然本人早就已經是 PureMVC 的忠實粉絲了...=P 快來加入 Flex + PureMVC 的行列吧~~ 結果這篇到最後變成 PureMVC 的勸進文...XD