MXML からスクリプトを分離する方法まとめ(4種類)

ここ数日 Flex をいじっていて、 MXML からスクリプトを分離する方法を調べてたらいろいろ出てきたのでまとめ*1。使用環境は FlashDevelop 4.0.1 RTM + Flex SDK 4.6.0 + Windows XP Pro 32bit 。

以下で紹介するのは以下の4種類。結論を先に書くと mx.core.IMXMLObject インターフェースを実装する方法がおすすめ。

  1. <fx:Script> の source 属性を使う or include する方法
  2. mx.core.IMXMLObject インターフェース実装を使う方法
  3. mx.core.FlexGlobals.topLevelApplication を参照する方法
  4. mx.core.Application の継承クラスを使う方法

分離する前の状態

まず <fx:Script><![CDATA[ ... ]]></fx:Script> 内に処理を書いている状態を想定。ここからロジックを分離していく。

プロパティをビューにバインドしてコントロールにイベントをアタッチできれば大概のことはできるだろう、ということで処理的なものはバインドとイベントアタッチのみ。

HelloWorld/
├── bin/
├── lib/
└── src/
    └── HelloWorld/
        └── Main.mxml
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
               xmlns:s="library://ns.adobe.com/flex/spark" 
               creationComplete="onCreationComplete()">

    <fx:Script>
        <![CDATA[

        [Bindable] private var statusMsg:String = "initialized.";

        private function onCreationComplete():void
        {
            statusMsg = "Creation complete.";
        }

        private function onButtonClick():void
        {
            statusMsg = "Clicked!";
        }

        ]]>
    </fx:Script>

    <s:HGroup>
        <s:Label text="Status: {statusMsg}"/>
        <s:Button label="Click me" click="onButtonClick()"/>
    </s:HGroup>

</s:Application>

<fx:Script> の source 属性を使う or include する方法

一応ファイルだけは「分離」できるけど動作が変わるわけではない。

プロジェクトフォルダの構成
HelloWorld/
├── bin/
├── lib/
└── src/
    └── HelloWorld/
        ├── Main.mxml
        └── script.as
    [Bindable] private var statusMsg:String = "initialized.";

    private function onCreationComplete():void
    {
        statusMsg = "Creation complete.";
    }

    private function onButtonClick():void
    {
        statusMsg = "Clicked!";
    }
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
               xmlns:s="library://ns.adobe.com/flex/spark" 
               creationComplete="onCreationComplete()">

    <fx:Script source="script.as" />

    <s:HGroup>
        <s:Label text="Status: {statusMsg}"/>
        <s:Button label="Click me" click="onButtonClick()"/>
    </s:HGroup>

</s:Application>

これは以下のように include するのと実質変わらない。

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
               xmlns:s="library://ns.adobe.com/flex/spark" 
               creationComplete="onCreationComplete()">

    <fx:Script>
        <![CDATA[
        include "script.as";
        ]]>
    </fx:Script>

    <s:HGroup>
        <s:Label text="Status: {statusMsg}"/>
        <s:Button label="Click me" click="onButtonClick()"/>
    </s:HGroup>

</s:Application>

mx.core.IMXMLObject インターフェース実装を使う方法

本命。 mx.core.IMXMLObject を実装すると初期化時に initialized メソッドが呼び出されるので CREATION_COMPLETE ハンドラを仕込み、そのハンドラ内で各コンポーネントにイベントを仕込む。これで MXML 内でイベントを設定する必要はなくなる。

この場合でも MXML 内でイベント時に実行するメソッドを指定することは可能だが、ロジックをできるだけ分離する方針にもとづき、あくまで AS ファイル内で addEventListener する。

プロジェクトフォルダの構成
ViewHelper/
├── bin/
├── lib/
└── src/
    └── ViewHelper/
        ├── View.mxml
        └── ViewHelper.as
ViewHelper.as
package ViewHelper
{
    import mx.core.IMXMLObject;
    import mx.events.FlexEvent;
    import flash.events.MouseEvent;

    public class ViewHelper implements IMXMLObject 
    {
        // MXML 内から呼ぶ場合は internal 以上に
        [Bindable] internal var statusMsg:String;

        private var view:View;

        // IMXMLObject が要求するメソッドは initialized のみ
        public function initialized(document:Object, id:String):void 
        {
            statusMsg = "initialized.";
            view = document as View;

            view.addEventListener(FlexEvent.CREATION_COMPLETE, onCreationComplete);

            // ボタンなどにイベントリスナーをアタッチしようとしてもまだ null
        }

        private function onCreationComplete(event:FlexEvent):void
        {
            statusMsg = "Creation Complete.";

            // ボタンへのアタッチは CREATION_COMPLETE のタイミングで
            view.btn.addEventListener(MouseEvent.CLICK, onButtonClick);
        }

        private function onButtonClick(event:MouseEvent):void
        {
            statusMsg = "Clicked via View.";
        }
    }
}
View.as
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:helper="ViewHelper.*">

    <fx:Declarations>
        <helper:ViewHelper id="vh"/>
    </fx:Declarations>

    <s:HGroup>
        <s:Label text="Status: {vh.statusMsg}"/>
        <s:Button id="btn" label="View" />
    </s:HGroup>

</s:Application>

mx.core.FlexGlobals.topLevelApplication を参照する方法

mx.core.FlexGlobals.topLevelApplication を参照してアクセスする。

プロジェクトフォルダの構成
MyClass/
├── bin/
├── lib/
└── src/
    └── MyClass/
        ├── Main.mxml
        └── MyClass.as
MyClass.as

イベントハンドラしか持たないようなクラスを作成。

package MyClass
{
    import mx.core.FlexGlobals;
    import mx.events.FlexEvent;
    import flash.events.MouseEvent;
    
    public class MyClass
    {
        [Bindable] internal var statusMsg:String;

        private var app:Main;

        public function MyClass()
        {
            statusMsg = "initialized.";
        }

        // メソッドも MXML 内から呼ぶなら internal 以上に
        internal function onCreationComplete():void
        {
            statusMsg = "Creation Complete.";

            app = FlexGlobals.topLevelApplication as View;
            app.btn.addEventListener(MouseEvent.CLICK, onButtonClick);
        }

        private function onButtonClick(event:MouseEvent):void
        {
            statusMsg = "Clicked via App.";
        }
    }
}
Main.mxml

MXML 側に何か仕込まないとスクリプト側に処理が回らないので creationComplete イベント時の実行メソッドを MXML 内で指定。

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mc="MyClass.*"
               creationComplete="mc.onCreationComplete()">

    <fx:Declarations>
        <mc:MyClass id="mc"/>
    </fx:Declarations>

    <s:HGroup>
        <s:Label text="Status: {mc.statusMsg}"/>
        <s:Button id="btn" label="App" />
    </s:HGroup>

</s:Application>

IMXMLObject 実装の中で FlexGlobals.topLevelApplication 参照を同時に使ってみたり(あまり意味はない)。

package ViewHelper 
{
    import mx.core.IMXMLObject;
    import mx.core.FlexGlobals;
    import mx.events.FlexEvent;
    import flash.events.MouseEvent;
    
    public class ViewHelper implements IMXMLObject 
    {
        [Bindable] internal var statusMsg:String;

        private var view:View;
        private var app:View;

        public function initialized(document:Object, id:String):void 
        {
            statusMsg = "initialized.";

            view = document as View;
            app = FlexGlobals.topLevelApplication as View;

            view.addEventListener(FlexEvent.CREATION_COMPLETE, onCreationComplete);
        }

        private function onCreationComplete(event:FlexEvent):void
        {
            statusMsg = "Creation Complete.";
            view.btn1.addEventListener(MouseEvent.CLICK, handleClickViaView);
            app.btn2.addEventListener(MouseEvent.CLICK, handleClickViaApp);
        }

        private function handleClickViaView(event:MouseEvent):void
        {
            statusMsg = "Clicked via View.";
        }

        private function handleClickViaApp(event:MouseEvent):void
        {
            statusMsg = "Clicked via App.";
        }
    }
}
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:helper="ViewHelper.*">

    <fx:Declarations>
        <helper:ViewHelper id="vh"/>
    </fx:Declarations>

    <s:HGroup>
        <s:Label text="Status: {vh.statusMsg}"/>
        <s:Button id="btn1" label="View" />
        <s:Button id="btn2" label="App" />
    </s:HGroup>

</s:Application>

mx.core.Application の継承クラスを使う方法

MXML で作られるオブジェクトは mx.core.Application インスタンスなので、これを継承すれば自前のクラスを MXML で使える。ただしそのままでは *.as 側でコントロールにイベントをアタッチすることができないなどいくらか制限がある。

プロジェクトフォルダの構成
ExtendsApp/
├── bin/
├── lib/
└── src/
    └── ExtendsApp/
        ├── ExtendsApp.as
        └── Main.mxml
ExtendsApp.as
package ExtendsApp 
{
    import flash.events.MouseEvent;
    import mx.core.Application;

    public class ExtendsApp extends Application
    {
        [Bindable] protected var statusMsg:String;

        public function ExtendsApp() 
        {
            statusMsg = "Hello world!!";
        }

        protected function onCreationComplete():void
        {
            statusMsg = "Creation complete.";

            // このパターンを使う場合、このイベントアタッチはできない
            //btn2.addEventListener(MouseEvent.CLICK, onButtonClick);
        }

        protected function onButtonClick():void
        {
            statusMsg = "Clicked!";
        }
    }
}
Main.mxml
<?xml version="1.0" encoding="utf-8"?>
<component:ExtendsApp xmlns:component="ExtendsApp.*"
                      xmlns:fx="http://ns.adobe.com/mxml/2009"
                      xmlns:s="library://ns.adobe.com/flex/spark"
                      creationComplete="onCreationComplete()">

    <s:HGroup>
        <s:Label text="Status: {statusMsg}"/>
        <s:Button id="btn1" label="btn1" click="onButtonClick()"/>
        <s:Button id="btn2" label="btn2" />
    </s:HGroup>

</component:ExtendsApp>

*1:こういうのは Flash Builder 使うとバシバシ自動生成してくれるのかもしれないけど使ったことないのでわかりません><