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

PureMVC 我也會 [0]

最近感覺 PureMVC 又熱了起來,也剛好好久沒有更新文章了, 就順便將去年底做的企業內訓 PureMVC 課程部分整理寫出來, 要講 PureMVC 當然要先從啥是 MVC 講起: Model-View-Control 出處: 維基百科 MVC ,大概節錄一段: (控制器Controller)- 負責轉發請求,對請求進行處理。 (檢視View) - 介面設計人員進行圖形介面設計。 (模型Model) - 程式設計師編寫程式應有的功能(實作算法等等)、數據庫專家進行資料管理和數據庫設計(可以實作具體的功能)。 其實到 Flash 的世界來講,Model and Control 都是由 .as 處理,而 View 便是 .fla+.as ,為了要鬆綁之間的關係,Event 機制就相當重要。其實每個人對 MVC 的最佳解釋都不同,真的要多練習才會有所領悟。 簡單來說: Model = 餐廳廚房 data: 西餐類 action:依照點菜單做餐點 action: 做完餐點就是將餐點放在出菜口按下通知鈴等服務生來 Control = 服務生 action: 聽到大門歡迎鈴就要說「歡迎光臨」 action: 看到客人揮揮手要去收點菜單 action: 聽到廚房通知鈴看是哪桌的餐點去送菜 View = 餐廳外場 view: 田園式的西餐廳裝潢 action: 客人進門會有歡迎鈴 action: 客人揮揮手叫服務生過來服務,是哪個服務生都無所謂,重點只要會收點菜就行了。 action: 客人收到餐點準備開動 當餐廳要改成外炒店,這時候只需要將大廚換成會中餐廚師,其出的菜就是中式快炒。 當餐廳外場由田園式外觀重新裝潢成華麗感夜店風,其進門的客層也會有所不同。 重點就是當你換掉一個地方時,對其它的部份不會造成太大的影響或者根本無所謂,這就是 MVC 所講求的境界... 一般來說,小專案有沒有必要使用 MVC 就是由各位自己判斷了,當你習慣將程式切分開來,發現 debug 不是一件痛苦的事情時,這時候有沒有強制使用 MVC 倒不是重點,因為你已經養成良好的撰寫習慣。但是開始接觸大型專案配合 team work 時,在沒有一個共用的核心框架前提下,這個專案開發到最後一定會是一個多手多腳的怪物,共用核心框架的價值就在這邊展現,這

[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