2010年5月11日火曜日

Flex のコンポーネント遅延ロードとデータバインディングとモデリングに関する考察

Flexではコンポーネントの遅延ローディングがデフォルトで行われるようになっています。そのため、mxml ファイル内で定義されたコンポーネントに対して同一の mxml ファイル内の ActionScript からアクセスしても、null が返る可能性があります。

そのような場合、内部コンポーネントを定義するXML要素の属性として、ActionScript 内の変数をバインディングするというワークアラウンドで乗り切ることができます。しかし、データバインディングをする場合、モデルが既にあったとしても、バインディング用の変数をわざわざ用意する必要があります。モデルの中で配列で100要素バインディングするとしたら、100種類の変数を用意する必要があります。

これも、ArrayCollection 型を使用することで解決できますが、既に Vector でデータを持っているモデルがあるとしたら、毎回移し変えなければなりません。

このように、ワークアラウンドで乗り切ることもできますが、あまり建設的ではないように思えます。まだまだ落とし穴があるように思えますし、、、。

ということで、今後の作業効率や成果物の品質にも大きく影響すると考えられるので、この機会に確認しておこうと思います。


例1:ある state で生成された情報を、これから初めて遷移される state に表示する場合。

ソース
思惑
  • State 1 が初期状態。
  • State 1 上の入力フォームから結果表示用ラベルに A と B の値を入力し、add ボタンを押下する。
  • State 2 に遷移し、A + B の結果が表示される。
結果
  • State 2 で表示される結果表示用ラベルの値が null となり、エラーが発生。
考察
  • creationPolicy を all にすることにより遅延ロードを抑制できないか?
  • ドキュメントによると、単一ビューコンテナでは、auto でも all でも同じ動作となり、『デフォルトでは、アプリケーションが最初に起動されたときに単一ビューコンテナのすべての子が生成されます。』となるらしい。生成されてるのに null なのはなぜ? (´・ω・`)
  • State が絡む場合はダメらしい。結局、State の遷移を先に行うしかないらしい。
解決法
  • ある State に含まれるコンポーネントへのアクセスは、その State に遷移してから行う。
解決後のソース

例2:複雑なモデル内部のデータを参照する場合。

バインディング版
  • ソース:Ex2.mxml
  • 特に問題なく動作する。


例3:配列へバインディングしようとして失敗する版
  • ソース:Ex2_1.mxml
  • 最初の1回目はうまく行くが、2回目以降も最初の結果が出続ける。

例4:ArrayCollection を用いて対応する版
  • ソース:Ex2_2.mxml
  • 問題なく動作するが、バインディングのために設計を崩すことになった。バインディングするよりも、特定のタイミングでデータをリロードしてくれるような設計の方がありがたいのだが。例えば状態遷移のタイミングとか。
例5:ボタンクリックのタイミングでデータを格納する版
  • ソース:Ex2_3.mxml
  • サブコンポーネントに対する text の指定を、親コンポーネント側で決定することが自然な設計であれば、無理にバインディングする必要はない。そのため、クリックのハンドラでラベルの表示を行っている。
例6:Stateに入るタイミングでデータを格納する版
  • ソース:Ex2_4.mxml
  • サブコンポーネントに対する text の指定を、親コンポーネント側で決定することが自然な設計であれば、無理にバインディングする必要はない。状態遷移をする箇所が一箇所であればその場所にラベルにデータを書き込む処理を埋め込めば良いのだが、ソースの様々な箇所から状態遷移が発生する場合には同様の処理を複数回記述することになる。その際、状態遷移をトリガーにした方がシンプルになる場合は、State に入るイベントを用いた方がよい。
付録:ソース


Ex1.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:mx="library://ns.adobe.com/flex/mx" width="100%" height="100%">
 <s:layout>
  <s:VerticalLayout horizontalAlign="center" verticalAlign="middle"/>
 </s:layout>

 <fx:Script>
  <![CDATA[
   import mx.controls.Alert;
   <!--- A + B の計算をして resultLabel に値を設定し、State2 に遷移するメソッド -->
   private function addButton_clickHandler(event:MouseEvent):void {
    var a:String = aNs.value.toString();
    var b:String = bNs.value.toString();
    var result:String = (aNs.value + bNs.value).toString();
    Alert.show("resultLabel = " + resultLabel); // ここで、resultLabel が null になる。
    resultLabel.text = a + " + " + b + " = " + result;
    currentState = "State2";
   }
  ]]>
 </fx:Script>

 <!-- state を用いて画面遷移を模倣 -->
 <s:states>
  <!-- A と B の値を入力する画面 -->
  <s:State name="State1"/>
  <!-- A + B の結果を表示する画面 -->
  <s:State name="State2"/>
 </s:states>
 
 <!-- State1用コンポーネント群 -->
 <mx:Form includeIn="State1">
  <mx:FormItem label="A">
   <!--- A の値入力用 NumericStepper -->
   <s:NumericStepper id="aNs"/>
  </mx:FormItem>
  <mx:FormItem label="B">
   <!--- B の値入力用 NumericStepper -->
   <s:NumericStepper id="bNs"/>
  </mx:FormItem>
 </mx:Form>
 <s:Button includeIn="State1" label="add" id="addButton" click="addButton_clickHandler(event)"/>
 
 <!-- State2用コンポーネント群 -->
 <!--- A + B = (A+Bの結果) という文字列を表示するためのラベル -->
 <s:Label id="resultLabel" includeIn="State2"/>
 
</s:Application>


Ex1_1.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:mx="library://ns.adobe.com/flex/mx" width="100%" height="100%">
 <s:layout>
  <s:VerticalLayout horizontalAlign="center" verticalAlign="middle"/>
 </s:layout>

 <fx:Script>
  <![CDATA[
   import mx.controls.Alert;
   <!--- A + B の計算をして resultLabel に値を設定し、State2 に遷移するメソッド -->
   private function addButton_clickHandler(event:MouseEvent):void {
    var a:String = aNs.value.toString();
    var b:String = bNs.value.toString();
    var result:String = (aNs.value + bNs.value).toString();
    currentState = "State2";
    Alert.show("resultLabel = " + resultLabel); // ここで、resultLabel が null ではなく、きちんと表示される。
    resultLabel.text = a + " + " + b + " = " + result;
   }
  ]]>
 </fx:Script>

 <!-- state を用いて画面遷移を模倣 -->
 <s:states>
  <!-- A と B の値を入力する画面 -->
  <s:State name="State1"/>
  <!-- A + B の結果を表示する画面 -->
  <s:State name="State2"/>
 </s:states>
 
 <!-- State1用コンポーネント群 -->
 <mx:Form includeIn="State1">
  <mx:FormItem label="A">
   <!--- A の値入力用 NumericStepper -->
   <s:NumericStepper id="aNs"/>
  </mx:FormItem>
  <mx:FormItem label="B">
   <!--- B の値入力用 NumericStepper -->
   <s:NumericStepper id="bNs"/>
  </mx:FormItem>
 </mx:Form>
 <s:Button includeIn="State1" label="add" id="addButton" click="addButton_clickHandler(event)"/>
 
 <!-- State2用コンポーネント群 -->
 <!--- A + B = (A+Bの結果) という文字列を表示するためのラベル -->
 <s:Label id="resultLabel" includeIn="State2"/>
 
</s:Application>


Ex2.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:mx="library://ns.adobe.com/flex/mx" width="100%" height="100%">
 <s:layout>
  <s:VerticalLayout horizontalAlign="center" verticalAlign="middle"/>
 </s:layout>

 <fx:Script>
  <![CDATA[
   import mx.controls.Alert;
   <!--- A + B の計算をして resultLabel に値を設定し、State2 に遷移するメソッド -->
   private function addButton_clickHandler(event:MouseEvent):void {
    currentState = "State2";
   }

   <!--- State2 から State1 に戻るボタン -->
   private function backButton_clickHandler(event:MouseEvent):void {
    currentState = "State1";
   }

  ]]>
 </fx:Script>

 <!-- state を用いて画面遷移を模倣 -->
 <s:states>
  <!-- A と B の値を入力する画面 -->
  <s:State name="State1"/>
  <!-- A + B の結果を表示する画面 -->
  <s:State name="State2"/>
 </s:states>
 
 <!-- State1用コンポーネント群 -->
 <mx:Form includeIn="State1">
  <mx:FormItem label="Value 1">
   <!--- 値1 の値入力用 NumericStepper -->
   <s:NumericStepper id="v1Ns"/>
  </mx:FormItem>
  <mx:FormItem label="Value 2">
   <!--- 値2 の値入力用 NumericStepper -->
   <s:NumericStepper id="v2Ns"/>
  </mx:FormItem>
  <mx:FormItem label="Value 3">
   <!--- 値3 の値入力用 NumericStepper -->
   <s:NumericStepper id="v3Ns"/>
  </mx:FormItem>
 </mx:Form>
 <s:Button includeIn="State1" label="add" id="addButton" click="addButton_clickHandler(event)"/>
 
 <!-- State2用コンポーネント群 -->
 <!--- v1 + v2 + v3 = (v1+v2+v3の結果) という文字列を表示するためのラベル -->
 <s:Label id="resultLabel" includeIn="State2" text="{v1Ns.value} + {v2Ns.value} + {v3Ns.value} = {v1Ns.value + v2Ns.value + v3Ns.value}"/>
 <s:Button includeIn="State2" label="back" id="backButton" click="backButton_clickHandler(event)"/>
 
</s:Application>


Ex2_1.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:mx="library://ns.adobe.com/flex/mx" width="100%" height="100%">
 <s:layout>
  <s:VerticalLayout horizontalAlign="center" verticalAlign="middle"/>
 </s:layout>

 <fx:Script>
  <![CDATA[
   import mx.controls.Alert;
   
   <!--- 値を保持する Vector -->
   private var values:Vector.<Number> = new Vector.<Number>();
   
   <!--- A + B の計算をして resultLabel に値を設定し、State2 に遷移するメソッド -->
   private function addButton_clickHandler(event:MouseEvent):void {
    // コンポーネントの値を values に格納。
    values.push(v1Ns.value);
    values.push(v2Ns.value);
    values.push(v3Ns.value);
    
    currentState = "State2";
   }

   <!--- State2 から State1 に戻るボタン -->
   private function backButton_clickHandler(event:MouseEvent):void {
    currentState = "State1";
   }

  ]]>
 </fx:Script>

 <!-- state を用いて画面遷移を模倣 -->
 <s:states>
  <!-- A と B の値を入力する画面 -->
  <s:State name="State1"/>
  <!-- A + B の結果を表示する画面 -->
  <s:State name="State2"/>
 </s:states>
 
 <!-- State1用コンポーネント群 -->
 <mx:Form includeIn="State1">
  <mx:FormItem label="Value 1">
   <!--- 値1 の値入力用 NumericStepper -->
   <s:NumericStepper id="v1Ns"/>
  </mx:FormItem>
  <mx:FormItem label="Value 2">
   <!--- 値2 の値入力用 NumericStepper -->
   <s:NumericStepper id="v2Ns"/>
  </mx:FormItem>
  <mx:FormItem label="Value 3">
   <!--- 値3 の値入力用 NumericStepper -->
   <s:NumericStepper id="v3Ns"/>
  </mx:FormItem>
 </mx:Form>
 <s:Button includeIn="State1" label="add" id="addButton" click="addButton_clickHandler(event)"/>
 
 <!-- State2用コンポーネント群 -->
 <!--- v1 + v2 + v3 = (v1+v2+v3の結果) という文字列を表示するためのラベル -->
 <s:Label id="resultLabel" includeIn="State2" text="{values[0]} + {values[1]} + {values[2]} = {values[0] + values[1] + values[2]}"/>
 <s:Button includeIn="State2" label="back" id="backButton" click="backButton_clickHandler(event)"/>
 
</s:Application>


Ex2_2.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:mx="library://ns.adobe.com/flex/mx" width="100%" height="100%">
 <s:layout>
  <s:VerticalLayout horizontalAlign="center" verticalAlign="middle"/>
 </s:layout>

 <fx:Script>
  <![CDATA[
   import mx.collections.ArrayCollection;
   import mx.controls.Alert;
   
   [Bindable]
   /**
    * 値を保持する ArrayCollection
    */
   private var values:ArrayCollection = new ArrayCollection(new Array(3));
   
   /**
    * A + B の計算をして resultLabel に値を設定し、State2 に遷移するメソッド
    */
   private function addButton_clickHandler(event:MouseEvent):void {
    // コンポーネントの値を values に格納。
    values.setItemAt(v1Ns.value, 0);
    values.setItemAt(v2Ns.value, 1);
    values.setItemAt(v3Ns.value, 2);
    
    currentState = "State2";
   }

   <!--- State2 から State1 に戻るボタン -->
   private function backButton_clickHandler(event:MouseEvent):void {
    currentState = "State1";
   }

  ]]>
 </fx:Script>

 <!-- state を用いて画面遷移を模倣 -->
 <s:states>
  <!-- A と B の値を入力する画面 -->
  <s:State name="State1"/>
  <!-- A + B の結果を表示する画面 -->
  <s:State name="State2"/>
 </s:states>
 
 <!-- State1用コンポーネント群 -->
 <mx:Form includeIn="State1">
  <mx:FormItem label="Value 1">
   <!--- 値1 の値入力用 NumericStepper -->
   <s:NumericStepper id="v1Ns"/>
  </mx:FormItem>
  <mx:FormItem label="Value 2">
   <!--- 値2 の値入力用 NumericStepper -->
   <s:NumericStepper id="v2Ns"/>
  </mx:FormItem>
  <mx:FormItem label="Value 3">
   <!--- 値3 の値入力用 NumericStepper -->
   <s:NumericStepper id="v3Ns"/>
  </mx:FormItem>
 </mx:Form>
 <s:Button includeIn="State1" label="add" id="addButton" click="addButton_clickHandler(event)"/>
 
 <!-- State2用コンポーネント群 -->
 <!--- v1 + v2 + v3 = (v1+v2+v3の結果) という文字列を表示するためのラベル -->
 <s:Label id="resultLabel" includeIn="State2" text="{values.getItemAt(0)} + {values.getItemAt(1)} + {values.getItemAt(2)} = {values.getItemAt(0) + values.getItemAt(1) + values.getItemAt(2)}"/>
 <s:Button includeIn="State2" label="back" id="backButton" click="backButton_clickHandler(event)"/>
 
</s:Application>


Ex2_3.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:mx="library://ns.adobe.com/flex/mx" width="100%" height="100%">
 <s:layout>
  <s:VerticalLayout horizontalAlign="center" verticalAlign="middle"/>
 </s:layout>

 <fx:Script>
  <![CDATA[
   import mx.controls.Alert;
   
   <!--- 値を保持する Vector -->
   private var values:Vector.<Number> = new Vector.<Number>();
   
   <!--- A + B の計算をして resultLabel に値を設定し、State2 に遷移するメソッド -->
   private function addButton_clickHandler(event:MouseEvent):void {
    // コンポーネントの値を values に格納。
    if (values.length == 0) {
     values.push(v1Ns.value);
     values.push(v2Ns.value);
     values.push(v3Ns.value);
    } else {
     values[0] = v1Ns.value;
     values[1] = v2Ns.value;
     values[2] = v3Ns.value;
    }
        
    currentState = "State2";
    
    // ラベルの値を直接指定する。
    resultLabel.text = values[0].toString() + ' + ' + values[1].toString() + ' + ' + values[2].toString() + ' = ' + (values[0] + values[1] + values[2]);

   }

   <!--- State2 から State1 に戻るボタン -->
   private function backButton_clickHandler(event:MouseEvent):void {
    currentState = "State1";
   }
  ]]>
 </fx:Script>

 <!-- state を用いて画面遷移を模倣 -->
 <s:states>
  <!-- A と B の値を入力する画面 -->
  <s:State name="State1"/>
  <!-- A + B の結果を表示する画面 -->
  <s:State name="State2"/>
 </s:states>
 
 <!-- State1用コンポーネント群 -->
 <mx:Form includeIn="State1">
  <mx:FormItem label="Value 1">
   <!--- 値1 の値入力用 NumericStepper -->
   <s:NumericStepper id="v1Ns"/>
  </mx:FormItem>
  <mx:FormItem label="Value 2">
   <!--- 値2 の値入力用 NumericStepper -->
   <s:NumericStepper id="v2Ns"/>
  </mx:FormItem>
  <mx:FormItem label="Value 3">
   <!--- 値3 の値入力用 NumericStepper -->
   <s:NumericStepper id="v3Ns"/>
  </mx:FormItem>
 </mx:Form>
 <s:Button includeIn="State1" label="add" id="addButton" click="addButton_clickHandler(event)"/>
 
 <!-- State2用コンポーネント群 -->
 <!--- v1 + v2 + v3 = (v1+v2+v3の結果) という文字列を表示するためのラベル -->
 <s:Label id="resultLabel" includeIn="State2"/>
 <s:Button includeIn="State2" label="back" id="backButton" click="backButton_clickHandler(event)"/>
 
</s:Application>


Ex2_4.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:mx="library://ns.adobe.com/flex/mx" width="100%" height="100%">
 <s:layout>
  <s:VerticalLayout horizontalAlign="center" verticalAlign="middle"/>
 </s:layout>

 <fx:Script>
  <![CDATA[
   import mx.controls.Alert;
   
   <!--- 値を保持する Vector -->
   private var values:Vector.<Number> = new Vector.<Number>();
   
   <!--- A + B の計算をして resultLabel に値を設定し、State2 に遷移するメソッド -->
   private function addButton_clickHandler(event:MouseEvent):void {
    // コンポーネントの値を values に格納。
    if (values.length == 0) {
     values.push(v1Ns.value);
     values.push(v2Ns.value);
     values.push(v3Ns.value);
    } else {
     values[0] = v1Ns.value;
     values[1] = v2Ns.value;
     values[2] = v3Ns.value;
    }
        
    currentState = "State2";
   }

   <!--- State2 から State1 に戻るボタン -->
   private function backButton_clickHandler(event:MouseEvent):void {
    currentState = "State1";
   }
  ]]>
 </fx:Script>

 <!-- state を用いて画面遷移を模倣 -->
 <s:states>
  <!-- A と B の値を入力する画面 -->
  <s:State name="State1"/>
  <!-- A + B の結果を表示する画面 -->
  <s:State name="State2" enterState="resultLabel.text = values[0].toString() + ' + ' + values[1].toString() + ' + ' + values[2].toString() + ' = ' + (values[0] + values[1] + values[2])"/>
 </s:states>
 
 <!-- State1用コンポーネント群 -->
 <mx:Form includeIn="State1">
  <mx:FormItem label="Value 1">
   <!--- 値1 の値入力用 NumericStepper -->
   <s:NumericStepper id="v1Ns"/>
  </mx:FormItem>
  <mx:FormItem label="Value 2">
   <!--- 値2 の値入力用 NumericStepper -->
   <s:NumericStepper id="v2Ns"/>
  </mx:FormItem>
  <mx:FormItem label="Value 3">
   <!--- 値3 の値入力用 NumericStepper -->
   <s:NumericStepper id="v3Ns"/>
  </mx:FormItem>
 </mx:Form>
 <s:Button includeIn="State1" label="add" id="addButton" click="addButton_clickHandler(event)"/>
 
 <!-- State2用コンポーネント群 -->
 <!--- v1 + v2 + v3 = (v1+v2+v3の結果) という文字列を表示するためのラベル -->
 <s:Label id="resultLabel" includeIn="State2"/>
 <s:Button includeIn="State2" label="back" id="backButton" click="backButton_clickHandler(event)"/>
 
</s:Application>

0 件のコメント:

コメントを投稿