目前手上的 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
你會發現在 itemClick 後使用了 dispatchEvent ,而不直接讓 ApplicationMediator 來監聽 tb,當 Mediator 不了解 viewComponent 的實作細節,這樣的寫法對之後更改 Application view 配置較不會互相影響。
ApplicationFacade.as 幾乎跟官方長得一模一樣...=P
這邊修改了 startup function 為了統一使用 ApplicationFacade.as 就不綁各自的 Application.mxml 實體 (ex. pureMVCAndStateMachine),所以將傳入改為 UIComponent 類別 ( 這樣每開一個專案就是直接 copy 改 package 就可以使用了...=) )
StartupCommand.as
State Machine 的 inject command
使用 State Machine 的時候, Erin 都習慣寫一張 const 表來用
GlobalStates.as
InjectFSCommand.as 寫法很統一~~
使用 State Machine 有個好處就是 state tag 內,如果沒有將要串接的 State 用 transition tag 宣告出來 ,就不可能會跳到那個 State...
將 DIPLAY and EXIT 註冊到各自的 view Command
裝好 State Machine 後,來寫 ViewPrepCommand.as
ShopCommand.as and Shop1Command.as 的寫法其實很一致,就是將 Shop and Shop1 的相關的 views、mediators、proxies and commands 啟用與移除都在自己的 command 運作
ShopCommand.as ( Shop1Command.as 就差 Class 名稱改一下 )
Command 都準備好囉~~最後將 ApplicationMediator.as 寫上~~
StateMachine 的用法:
你一定會問,為什麼不直接將 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
在 Application 的 MXML 中使用 ViewStack 來控制場景變換,是很直覺的作法。但如果你專案還有擴充的空間,這樣做場景到最後你一定後悔!因為可能不是每個 View 都會被新增到 同一個地方。以下是一個簡單的範例講解 pureMVC + StateMachine 的應用。
範例的 Classes tree

開始前請先下載以下的 library:為了方便開發,統一使用 multicore 版本
PureMVC MultiCore for AS3
Utility - AS3 StateMachine
範例場景說明:
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
Hello Erin:
ReplyDelete我是pureMVC的新手,拜讀你的文章後,學到蠻多的。
有一個問題想請教你,目前我想做一個像一般Windows AP的介面,有menu,點了menu item後,會開啟MDI Form,每個MDI Form(i功能)都是獨立的,所以我選擇了multicore的版本,但我確定menu的eventhandel是否要納入Mediator的管轄呢?~~是否可以給個建議,或是有什麼文章可以參考。
謝謝
ilin
如果是我大概會處理成 menu + MenuMeditaor(只用接收 menu click 事件後 sendNotification),再由 command 處理呼叫 對應 form ui 的開啟跟關閉
ReplyDelete每個 MDI form - FormMediator 都自己配一組,這樣要更換 Form 會比較容易...