Repeater で DataGrid もどき

実は、つい最近まで Repeater コンポーネントの存在を知らなかった taiga です。こんにちは。

Repeater コンポーネントの存在を知ってドキュメントを読んだとき、サンプルコードには、<mx:Script> タグを使用していたり、MXML 内で波括弧を使用して AS を埋め込んでいたりと、あまり模範にしたくない断罪サンプルしか掲載されてなかったので、自分好みのサンプルを作成しました。カスタムコンポーネントを Repeater で繰り返し処理させるサンプルです。

RepeaterSample (画面)
http://labs.taiga.jp/flex2/RepeaterSample/

RepeaterSample (ソース)
http://labs.taiga.jp/flex2/RepeaterSample/srcview/

ソースの説明をする前に、Repeater コンポーネントを知らない人向けに簡単に説明しておきます。
Repeater コンポーネントは、実行時に動的 or 静的な配列に基いて UI コンポーネントを繰り返し処理するコンポーネントです。Repeater.dataProvider = hogeArray 的な使い方ができるので、 ComboBox や DataGrid を使ったことのある人であれば、容易に理解できると思います。他にも、for 文で new して addChild() して…といった AS コーディングの手間を省くことができるというメリットもあります。

ソースの説明に話を移しますが、Repeater コンポーネントを持つ親コンポーネント(RepeaterSample.mxml, ApplicationBase.as)では、特別な処理を行っていません。 今回注目したいのは、繰り返し表示されるカスタム子コンポーネントクラス (SubRowBase.as) の中身です。

public function SubRowBase()
{
    addEventListener(FlexEvent.INITIALIZE, initializeHandler, false, 0, true);
}
private function initializeHandler(event:FlexEvent):void
{
    var lColor:uint = (0 == repeaterIndex % 2) ? 0xffffff : 0xeeeeee;
    mComponent = event.currentTarget as SubRow;
    with (mComponent) {
        compCheckBox.addEventListener(Event.CHANGE, changeHandler, false, 0, true);
        compCheckBox.selected = repeater.currentItem.check;
        comp_0_Label.text     = repeater.currentItem.labelFirst;
        comp_1_Label.text     = repeater.currentItem.labelSecond;
        setStyle('backgroundColor', lColor);
        setStyle('backgroundAlpha', 0.5);
    }
}

Repeater コンポーネントにネストされた子コンポーネントは、親コンポーネントから渡された配列の値を、自分自身の repeater.currentItem プロパティを使用して参照することができます。( repeater プロパティは、UIComponent クラスから派生したすべてのコンポーネントに実装されているプロパティです)

このとき注意しなければならないのは、FlexEvent.CREATION_COMPLETE イベント発行のタイミングで repeater プロパティを参照すると、null 参照になり例外エラーが発生するということです。実際に FlexEvent.INITIALIZE イベントを addEventListener している理由は、この例外エラーを回避するためなのです。

次に、子コンポーネント内のデータの変更方法について、CheckBox の Event.CHANGE イベントリスナー関数を例に説明します。

private function changeHandler(event:Event):void
{
    var lRowIndex:int     = repeaterIndex;
    var lSelected:Boolean = event.currentTarget.selected;
    repeater.dataProvider[lRowIndex].check = lSelected;
}

repeaterIndex プロパティは、親コンポーネントの Repeater.dataProvider プロパティ内にある、子コンポーネントのインデックスを取得することができます。( repeaterIndex プロパティも、repeater プロパティと同じく、UIComponent クラスから派生したすべてのコンポーネントに実装されているプロパティです。余談ですが、サンプル画面のストライプの表現でも、repeaterIndex プロパティを使用して、インデックスが偶数か奇数かを判別して背景色を切り替える処理を行っています。)

そして、repeater.dataProvider プロパティを使用することによって、親コンポーネントの Repeater.dataProvider プロパティを参照できるので、子コンポーネントのデータが更新されたときには、repeater.dataProvider[repeaterIndex] に対してデータの変更(上書き)処理を行えばよいわけです。

以上のポイントさえ抑えておけば、Repeater コンポーネントを使用したときでも、MXML コーディングはシンプルかつ最小限に留め、画面のコントロールはすべて AS クラスにまとめる…という、ステキアーキテクチャが割と簡単にできあがるハズです。

DateField 小ネタ

時分秒 (hh:mm:ss) の値を含んだ Date オブジェクトを selectedDate プロパティにセットすると、アイコンをクリックしたとき表示されるカレンダーに、セットした日にち (DD) がフォーカスされない…という不具合が発生します。

この現象、Flex 3 では解消されていますが、時間情報を含んだ日付情報を扱うような Flex 2 アプリを作るときには、注意が必要です。(余談ですが、fla ファイル (AS 2.0) で扱える DateField コンポーネントでは、上記現象は発生しません)

以下検証コード

<?xml version="1.0" encoding="utf-8"?>
<!-- DateFieldShotTest -->
<mx:Application
    xmlns:mx         = "http://www.adobe.com/2006/mxml"
    layout           = "vertical"
    creationComplete = "init();">

    <mx:Script>
        <![CDATA[
            private function dateOutput():void
            {
                traceText.text = String(comp_0_DF.selectedDate)+'\n'+
                                 String(comp_1_DF.selectedDate)+'\n'+
                                 String(comp_2_DF.selectedDate)+'\n'+
                                 String(comp_3_DF.selectedDate)+'\n'+
                                 String(comp_4_DF.selectedDate);
            }
            private function init():void
            {
                traceText.text = '';
                comp_0_DF.selectedDate = new Date();
                comp_1_DF.selectedDate = new Date(2007, 0, 23);
                comp_2_DF.selectedDate = new Date(2007, 0, 23, 0, 0, 0);
                comp_3_DF.selectedDate = new Date(2007, 0, 23, null, null, null);
                comp_4_DF.selectedDate = new Date(2007, 0, 23, 12, 34, 56);
            }
        ]]>
    </mx:Script>

    <mx:Style>
        Label
        {
            font-weight: bold;
        }
    </mx:Style>

    <mx:VBox width="400" textAlign="left">

        <mx:HBox>
            <mx:DateField id="comp_0_DF" />
            <mx:Label text="new Date();" />
        </mx:HBox>

        <mx:HBox>
            <mx:DateField id="comp_1_DF" />
            <mx:Label text="new Date(2007, 0, 23);" />
        </mx:HBox>

        <mx:HBox>
            <mx:DateField id="comp_2_DF" />
            <mx:Label text="new Date(2007, 0, 23, 0, 0, 0);" />
        </mx:HBox>

        <mx:HBox>
            <mx:DateField id="comp_3_DF" />
            <mx:Label text="new Date(2007, 0, 23, null, null, null);" />
        </mx:HBox>

        <mx:HBox>
            <mx:DateField id="comp_4_DF" />
            <mx:Label text="new Date(2007, 0, 23, 12, 34, 56);" color="#CC0000" />
        </mx:HBox>

    </mx:VBox>

    <mx:TextArea  id="traceText" width="400" height="100" />

    <mx:HBox>
        <mx:Button label="trace" click="{dateOutput()}" />
        <mx:Button label="reset" click="{init()}" />
    </mx:HBox>

</mx:Application>

どう書く? itemRenderer (ComboBox)

前回のエントリーに引き続き、itemRenderer について書きます。

サンプルは以下の通り。

ItemRendererComboBoxSample (画面)
http://labs.taiga.jp/flex2/ItemRendererComboBoxSample/

ItemRendererComboBoxSample (ソース)
http://labs.taiga.jp/flex2/ItemRendererComboBoxSample/srcview/

まず、ItemRendererComboBoxSample.mxml の <mx:DataGridColumn> タグに着目します。

<!-- カスタム ComboBox -->
<mx:DataGridColumn
    headerText       = "colmn01"
    dataField        = "combo"
    itemRenderer     = "lib.SubComboBox"
    rendererIsEditor = "true"
    editorDataField  = "selectedIndex"
    editable         = "true"
/>

itemRenderer プロパティ値に、カスタム ComboBox クラスを定義しています。
rendererIsEditor プロパティと editorDataField プロパティを設定している箇所は、前回のカスタム CheckBox と同じです。
そして今回、editorDataField プロパティには、ComboBox のプロパティ名である slectedIndex を定義しています。

次に、SubComboBox.as に着目します。

package lib
{
    import flash.events.Event;
    import mx.collections.ArrayCollection;
    import mx.controls.ComboBox;
    import mx.controls.dataGridClasses.DataGridListData;
    /**
     * SubComboBox
     * itemRenderer 用 ComboBox サブクラス
     */
    public class SubComboBox extends ComboBox
    {
        /**
         * mValue
         * @private
         * クラス内部で保持する selectedIndex 値
         */
        private var mValue:uint;
        /**
         * SubComboBox
         * コンストラクタ
         */
        public function SubComboBox()
        {
            super();
            width        = 100;
            dataProvider = [ {label:'Label_A', data:10},
                             {label:'Label_B', data:20},
                             {label:'Label_C', data:30} ];
            addEventListener(Event.CHANGE, changeHandler, false, 0, true);
        }
        /**
         * tmpValue
         * @private
         * selectedIndex 値(内部保持用)
         */
        private function get tmpValue():uint
        {
              return mValue;
        }
        /**
         * @private
         */
        private function set tmpValue(aValue:uint):void
        {
            mValue = aValue;
            invalidateProperties();
        }
        /**
         * data
         * @param aValue itemRenderer の値
         * @private
         * プロパティ ComboBox の selectedIndex 値
         */
        override public function set data(aObject:Object):void
        {
            super.data = aObject;
            if (null != dataProvider)
            {
                selectedIndex = getLabelIndex( data[DataGridListData(listData).dataField] );
            }
        }
        /**
         * commitProperties
         * @private
         * Binding しているプロパティの更新
         */
        override protected function commitProperties():void
        {
            this.selectedIndex = getLabelIndex( data[DataGridListData(listData).dataField] );
            super.commitProperties();
        }
        /**
         * getLabelIndex
         * @param aValue dataProvider の値
         * @return selectedIndex 値
         * @private
         * 引数と ComboBox 内のデータを評価して、マッチしたら該当インデックスを返却
         */
        private function getLabelIndex(aValue:int):uint
        {
            var lArrayCollection:ArrayCollection = dataProvider as ArrayCollection;
            var lLength:Number = lArrayCollection.length;
            for (var i:uint = 0; i < lLength; i++)
            {
                if (lArrayCollection.getItemAt(i).data == aValue)
                {
                    return i;
                }
            }
            return 0;
        }
        /**
         * changeHandler
         * @param event Event オブジェクト
         * @private
         * ComboBox の change イベント
         */
        private function changeHandler(event:Event):void
        {
            var lArrayCollection:ArrayCollection = dataProvider as ArrayCollection;
            if (listData)
            {
                tmpValue = lArrayCollection.getItemAt(this.selectedIndex).data;
                data[DataGridListData(listData).dataField] = tmpValue;
            }
        }
    }
}

コンストラクタで ComboBox の label 値と data 値を定義します。

DataGrid をスクロールさせると ComboBox.commitProperties() メソッドが実行されます。
このとき ComboBox の label 値を変更していると初期表示状態に戻ってしまうので、この現象を回避するために ComboBox.commitProperties() メソッドをオーバーライドし、changeHandler() メソッドで保持しておいた data 値をセットさせる処理を追記しています。

DataGrid.dataProvider プロパティにセットする ComboBox の値ですが、ComboBox の data 値のみ使用します。( label は、ただのお飾りということです)
なぜかというと、本来 ComboBox.selectedIndex プロパティには、label 値と data 値を内包したオブジェクト(例: {label:”hoge”, data:”fuga”} )を代入しないと正しく動作しないのですが、DataGrid.dataProvider プロパティに例のようなオブジェクトをセットしてしまうと、DataGird のヘッダをクリックしてソート処理を行ったとき、DataGird がソート対象を判別できず、例外エラーが発生してしまうためです。

そして、正しく動作しない selectedIndex プロパティを正しく動作させるために、getLabelIndex() というメソッドを作成し、ComboBox の data 値と DataGrid.dataProvider プロパティにセットされた値を、総当たりでマッチングさせます。(賢い方法ではありませんが)

どう書く? itemRenderer (CheckBox)

今日も、みんなが大嫌いな(?) DataGird について書きます。

ソースの可読性や管理のしやすさを考慮した個人的な「宗教」に基づき、見た目に直接関与しない MXML タグは、可能な限り AS クラスに記述したいと思っています。
DataGrid のヘルプや Tips などで見かける <mx:itemRenderer> タグも、私の「断罪対象」にカテゴライズされるので、サンプルを用意しました。

ItemRendererCheckBoxSample (画面)
http://labs.taiga.jp/flex2/ItemRendererCheckBoxSample/

ItemRendererCheckBoxSample (ソース)
http://labs.taiga.jp/flex2/ItemRendererCheckBoxSample/srcview/

まず、ItemRendererCheckBoxSample.mxml の <mx:DataGridColumn> タグに着目します。

<!-- カスタム CheckBox -->
<mx:DataGridColumn
    headerText       = "colmn01"
    dataField        = "checkBoxValue"
    itemRenderer     = "lib.SubCheckBox"
    rendererIsEditor = "true"
    editorDataField  = "selected"
    editable         = "true"
/>

<!-- 標準の CheckBox -->
<mx:DataGridColumn
    headerText       = "colmn02"
    dataField        = "hoge"
    itemRenderer     = "mx.controls.CheckBox"
    rendererIsEditor = "true"
    editorDataField  = "selected"
    editable         = "true"
/>

itemRenderer プロパティ値に、カスタム CheckBox クラスを定義しています。(ついでに 2 カラム目には、標準の CheckBox を定義しました)
他には、rendererIsEditor プロパティと、editorDataField プロパティを設定していますが、どちらも CheckBox の編集に関わる大切なプロパティです。
rendererIsEditor プロパティ値を true と定義することによって、アイテムレンダラーがアイテムエディタであることを示します。( itemEditor プロパティを使用するより良いみたいです)
editorDataField プロパティには、アイテムエディタのプロパティ名が設定できるので、今回は CheckBox のプロパティ名である slected を定義しています。

その他 itemRenderer 周りの詳しい解説は、@IT に掲載されているクラスメソッド社の成瀬氏の記事に詳しく書かれているので、興味のある方は下記リンク先を見てください。


参考: 現場で使えるFlex実践テクニック(4)
Page1
Page2
Page3

次に、SubCheckBox.as に着目します。

/**
 * commitProperties
 * @private
 * Binding しているプロパティの更新
 */
override protected function commitProperties():void
{
    selected = mValue;
    super.commitProperties();
}
/**
 * updateDisplayList
 * @param aUnscaledWidth 親コンテナにより決定されるコンポーネントの幅
 * @param aUnscaledHeight 親コンテナにより決定されるコンポーネントの高さ
 * @private
 * CheckBox の再描画と再配置
 */
override protected function updateDisplayList(aUnscaledWidth:Number, aUnscaledHeight:Number):void
{
    super.updateDisplayList(aUnscaledWidth, aUnscaledHeight);
    var lObject:IFlexDisplayObject = mx_internal::currentIcon;
    if(lObject)
    {
        var lIconWidth :uint = lObject.width;
        var lIndent    :uint = (width - lIconWidth) / 2;
        lObject.x = lIndent;
    }
}

itemRenderer のコーディングお作法という趣旨から若干脱線しますが、commitProperties() メソッドと updateDisplayList() メソッドをオーバーライドして、dataProvider の内容を更新させたり再描画させたりしています。
この処理を記述しないと、2 カラム目に表示させている標準 CheckBox のような状態になってしまいます。

commitProperties() メソッドと updateDisplayList() メソッドについての詳しい解説は、仲間内で神ブログと呼ばれている前回よりは成長したブログさんの記事に詳しく書かれているので、興味のある方は下記リンク先を見てください。


参考:前回よりは成長したブログ
[Flex]asでカスタムコンポーネント(3)

DataGrid 小ネタ

先日、仕事でプロジェクタを使って、作成した Flex アプリを映していて気付いた現象です。
画面の色数を 16bit Color に変更すると、若干ですが DataGrid の 1 列目以外のストライプ背景色が薄く変色してしまいます。
列を入れ替えても、同じ状態を維持します。

もしかしたら、スタイルの alternatingItemColors を定義したら直るかと思ったのですが、試した結果、無駄な努力に終わりました。(根本的な解決にはなりませんが、濃い色を定義すると、誤魔化せる場合があるかもしれません←ダメです)

スクリーンショットを撮ったので、比較してみてください。

DataGrid 比較画像 (32bit Color)
32bit Color

DataGrid 比較画像 (16bit Color)
16bit Color

なお、この現象は、Flex3 でも再現します。
ソースを解読していないので原因はわかりませんが、もしご存知の方いらっしゃいましたらツッコミいただけると幸いです。

余談ですが、お堅い基幹業務系 Web アプリ案件の場合、「表示環境:色 16bit Color 」 と要件を提示されることが未だにあるとかないとか。

MXML Coding Best Practice 概要(候補 2 )

下記コードは、先日のエントリーで掲載したコードとほぼ同じ内容ですが、主な相違点は IMXMLObject を使用していることです。

Application タグ内に、IMXMLObject インターフェイスを実装した AS クラスをタグ化して配置することにより、Application タグのインスタンスと、同インスタンスがタグ化した AS クラスを参照するための識別子が、AS クラスの initialized() メソッドに送出されます。

initialized() メソッドは、MXML アプリケーションから明示的に呼び出さずとも、MXML タグに指定されたすべてのコンポーネントプロパティが初期化された後、コンストラクタと同じように呼び出されます。(仮にコンストラクタを定義しても、ビルドエラーは発生しません)

MXML Application
<?xml version="1.0" encoding="utf-8"?>
<mx:Application
    xmlns:base = "lib.*"
    xmlns:mx   = "http://www.adobe.com/2006/mxml">
    <base:ApplicationBase id="hogeName" />
    <mx:Button id="hogeButton" label="test" />
</mx:Application>
lib/ApplicationBase.as package lib
{
    import flash.events.MouseEvent;
    import mx.core.IMXMLObject;
    import mx.events.FlexEvent;
    /**
     * MXML Application コントロールクラス
     */
    public class ApplicationBase implements IMXMLObject
    {
        /**
         * MXML Application インスタンス変数
         */
        private var mApplication:MXML Application Name;
        /**
         * initialized
         * @param aDocment このクラスを作成した MXML オブジェクト
         * @param aId aDocment がこのクラスを参照するための識別子
         * 実装オブジェクトが作成され、MXML タグに指定されたすべてのコンポーネントプロパティが初期化された後に呼び出されます。
         */
        public function initialized(aDocment:Object, aId:String):void
        {
            mApplication = aDocment as MXML Application Name;
            mApplication.addEventListener(FlexEvent.CREATION_COMPLETE, creationCompleteHandler, false, 0, true);
        }
        /**
         * clickHandler
         * @param event MouseEvent オブジェクト
         * テスト出力
         */
        private function clickHandler(event:MouseEvent):void
        {
            trace("hoge");
        }
        /**
         * creationCompleteHandler
         * @param event FlexEvent オブジェクト
         * 初期化
         */
        private function creationCompleteHandler(event:FlexEvent):void
        {
            mApplication.hogeButton.addEventListener(MouseEvent.CLICK, clickHandler, false, 0, true);
        }
    }
}

参考:IMXMLObject の実装について — Flex 2

MXML Coding Best Practice 概要(候補 1 )

ここでいう「概要」とは、MXML アプリケーションと ActionScript のアーキテクチャだと解釈していただければと思います。( MXML コンポーネントについては、別途エントリーする予定です)

下記のコードは、Adobe MAX Japan 2007 にて、Adobe の Ted Patrick 氏が紹介していた Best Practice で、AS クラスを MXML アプリケーションのルートタグにするという手段。
個人的には、この手法が一番好きです。

MXML Application
<?xml version="1.0" encoding="utf-8"?>
<base:ApplicationBase
    xmlns:base = "lib.*"
    xmlns:mx   = "http://www.adobe.com/2006/mxml">
    <mx:Button id="hogeButton" label="test" />
</base:ApplicationBase>

lib/ApplicationBase.as
package lib
{
    import flash.events.MouseEvent;
    import mx.core.Application;
    import mx.events.FlexEvent;
    /**
     * MXML Application コントロールクラス
     */
    public class ApplicationBase extends Application
    {
        /**
         * MXML Application インスタンス変数
         */
        private var mApplication:MXML Application Name;
        /**
         * ApplicationBase
         * コンストラクタ
         */
        public function ApplicationBase()
        {
            super();
            this.addEventListener(FlexEvent.CREATION_COMPLETE, creationCompleteHandler, false, 0, true);
        }
        /**
         * clickHandler
         * @param event MouseEvent オブジェクト
         * テスト出力
         */
        private function clickHandler(event:MouseEvent):void
        {
            trace("hoge");
        }
        /**
         * creationCompleteHandler
         * @param event FlexEvent オブジェクト
         * 初期化
         */
        private function creationCompleteHandler(event:FlexEvent):void
        {
            mApplication = event.currentTarget as MXML Application Name;
            mApplication.hogeButton.addEventListener(MouseEvent.CLICK, clickHandler, false, 0, true);
        }
    }
}

あと宗教っぽくてアレですが、

  • MXML タグ内でイベント属性は定義しない
  • MXML タグ内でスタイル属性は定義しない
  • <mx:Script> タグは一切使用しない

などといったルールを設けておくと、クラスベースなコーディングができて、ドキュメントやソース管理が簡潔になり、スッキリするかもしれません。