将您的Flex组件从MXML迁移至ActionScript 3

来源:互联网 发布:php读取本地图片 编辑:程序博客网 时间:2024/05/15 00:55

原文地址是:http://insideria.com/2010/05/moving-your-flex-components-fr.html

我经常听说,比较酷的开发者都用 ActionScript写他们的Flex组件而不用MXML。 我不太认同这种观点。 对布局而言,MXML是强大的。 对于构建简单的组件来说也是强大的。 声明语法让许多开发工作更容易些,例如设置样式和添加事件监听。 但是,如果您仔细研读Flex框架源代码或者商业级别的组件,如我在Flextras构建的那些,您会注意到他们并没有使用MXML。 用ActionScript(AS)就完全可以构建。 那是为什么呢? 

ActionScript能让你粒度级地控制你的代码(非常灵活)。 MXML是ActionScript语言的另外一种表述形式。 Flex框架分析MXML并将其转换成ActionScript。 这意味着您写的代码并不是可以立即运行的代码。 若将编码看做是做饭的话,使用MXML就如同从商店里购买现成的蛋糕材料, 您只需要加点水就可以烤蛋糕了。 而ActionScript类似于先从面粉开始,并仔细挑选好其他调料才能烤蛋糕。 它费时较长,需要考虑的东西也多一些,但往往比较值得。 

这篇文章会向您展示怎样将您的组件开发从MXML迁移到ActionScript。 顺便我们会接触到Flex组件生命周期的各个方面。 一般来说,当您在受约束的环境下建自己的应用程序时,用MXML会更有意义;而且确实也不错。 但是,懂得怎样用ActionScript从头创建,会使您对Flex的工作机制有更深入的了解。 

今天的应用程序

今天,我希望您假设,您的老板让您做一个问卷调查程序。 就如同大部分调查一样,这个程序包括一大堆的问题,需要一些收集答案的方式。 大部分问题都可以用是或否来回答。 在正常情况下,您会用单选按钮来获取是或否的回答。 

不幸的是,您需要跟我试着想想您的老板有一点儿变态。 他讨厌单选按钮。 您永远也不会知道为什么,但还必须这么做。 他坚持让您用下拉框来代替单选按钮。 好吧! 我们可以做到。 

既然知道这个调查程序与很多“是”或“否”的回答相关联,您决定据此做个组件。 

YesNoQuestion组件版本1

那让我们来投入创建组件的第一个版本。 您可以用Text组件来显示是或否,用ComboBox来做选择。 然后把这些全放进HBox里,代码看起来像这样:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" width="100%">
  3. <mx:Script>
  4. <![CDATA[
  5. import mx.collections.ArrayCollection;
  6. [Bindable]
  7. public var dp : ArrayCollection = new ArrayCollection([
  8. {label:'Yes'},
  9. {label:'No'}
  10. ]);
  11. ]]>
  12. </mx:Script>
  13. <mx:Text id="question" />
  14.  
  15. <mx:ComboBox id="answer" dataProvider="{dp}" />
  16.  
  17. </mx:HBox>

我们已经用别的MXML组件做了第一个ActionScript的应用。 ComboBox的dataProvider是脚本编写的, 它包含了两个对象,一个是,一个否。 

不幸地是,这个组件依然缺少功能。 当用这个组件的时候,我们怎么指定问题文本呢? 您可以用“question.text”,但是如果我们能让他简单点儿会更好。 我们怎么知道被调查者选了什么答案呢? 我们也需要增加属性来描述那个值。 

在ActionScript代码块添加这两个变量:

  1. [Bindable]
  2. public var questionText : String;
  3.  
  4. [Bindable]
  5. public var selectedAnswer : String;

既然变量是公共的,人们就很容易用我们的组件获取这些变量。 我称之为变量属性,尽管我认为这并非正式名称。 接下来,您将会将变量绑定到这两个组件。 像这样修改MXML:

  1. <mx:Text id="question" text="{questionText}" />
  2.  
  3. <mx:ComboBox id="answer" dataProvider="{dp}" change="selectedAnswer = answer.selectedItem.label" />

数据绑定将问题文本绑定至文本显示。 ComboBox值改变时,您可以用change事件来更新已选择的答案。 实际上,这个组件都可以完成我们想要它实现的功能。 现在我们来验证一下。 

为了验证这个组件,您得先创建一个简单的工程。 我决定建一个AIR的工程来验证,这样就不用费时做web server的设置了。 从代码观点而言,一个基于web的工程几乎没什么不同,只是将WindowedApplication转换为 Application。 以下就是我主要的应用程序文件: 

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:MXMLToAS3="com.flextras.InsideRIA.MXMLToAS3.*">
  3.  
  4. <mx:VBox>
  5. <MXMLToAS3:YesNoQuestionV1 id="q1" questionText="Do you want to take a Survey?" />
  6. <mx:Text text="{q1.selectedAnswer}" />
  7. </mx:VBox>
  8.  
  9. </mx:WindowedApplication>

这段代码导入了包含这个组件的包。 它创建了该组件的一个实例q1,并将问题文本声明为string类型。 这段代码包含了一个附件的文本组件实例,该实例绑到了q1的选项属性。 当我们改变问题的答案时,将会看到选项属性也随之改变。

版本2:隐藏实现

将您的代码从组件中分离出来的一个关键原因就是为了隐藏实现。若隐藏实现的话,您可以只更改实现而不用改API,而使用组件的所有代码都应该没有问 题。当前的组件并没有隐藏实现细节,就如您看到的这样:问题和答案字段都是暴露在外的。通过MXML创建的话,他们都被当做公共属性。如果有人改变 ComboBox的数据源( dataProvider)的话,会出现什么情况呢?您要尽量预防此类干预。 

为达到这个目的,我们打算将我们的Text以及YesNoQuestion这两个MXML组件转换为受保护的ActionScript变量,并充分利用Flex框架组件生命周期 createChildren()来创建组件并将它们加到stage中。 

在组件初始化安装的时候会调用createChildren() 方法,目的是为了创建子组件并将其加入到父容器。当处理ActionScript States时,我经常在createChildren()时初始化所有AddChild或RemoveChild state元素。我在Flextras Calendar组件实现天、周和年视图时就是这样做的。 

为了保持selectedAnswer变量与当前事件同步,我们要响应change事件并设置值。 新做法将是一样的,但是我们会在ActionScript安装事件监听器和事件处理器,而不使用内置的MXML。 

首先,创建组件变量: 

  1. protected var question : Text;
  2. protected var answer : ComboBox;

因为这些是受保护的变量,意味着扩展该组件的任意组件都能访问这些变量,但是如果新组件未扩展该文件,就不能访问了。 这就是 createChildren()方法: 

  1. override protected function createChildren():void{ super.createChildren();
  2.  
  3. this.question = new Text();
  4. this.question.text = questionText;
  5. this.addChild(this.question);
  6.  
  7. this.answer = new ComboBox();
  8. this.answer.dataProvider = this.dp;
  9. this.addChild(this.answer);
  10. this.answer.addEventListener(ListEvent.CHANGE, onChange);
  11. }

createChildren)_是在UIComponent类中初始化定义的。 所有的Flex用UI组件都扩展了UIComponent,我们的也不例外,虽然我们要靠近底端一些。 为了在YesNoQuestion组件中实现这个方法,我们重写了signature方法,并调用了super()方法。 在重写方法时,调用super方法非常重要。 您永远不会知道哪些代码会在更高层次执行。 该组件创建了问题文本实例和答案ComboBox实例。 它设置了默认值并将其加入容器。 

在MXML版本中,您用了change事件来保持已选择答案的同步。而在ActionScript中,您会用相同的方法,只不过需要通过 addEventListener()来设置事件监听器。它指定您要监听的事件类型及事件发布时要运行的函数,例如本例中的change事件。当我在事件 类中会提到事件常量而不用事件名“change”。当然,两者都可以,只不过说常量会更灵活一些。如果事件名称变化了,我们也不用改代码。 

以下就是监听函数: 

  1. protected function onChange(e:ListEvent):void{
  2. this.selectedAnswer = this.answer.selectedItem.label;
  3. }

这个监听函数接收事件参数, 这个方法中的单行代码与之前的MXML版本使用的顺序代码是一样的。 

版本 3: commitProperties()

如果当createChildren() 被调用时,questionText 仍然是空字符串,那会怎样呢? 那么问题就会什么都不显示。 在目前的代码中,questionText的属性变化时并不会更新问题。 这里有一个解决方案。 Flex框架提供了 commitProperties() 方法在组件的所有属性都设置好以后来运行代码。 只要值一变化,我们就使用这个方法来设置更新问题文本。 

首先,将commitProperties()方法加入代码。 我们可以将这行复制粘贴到body中以设置问题文本。 

  1. override protected function commitProperties():void{
  2. super.commitProperties();
  3. this.question.text = questionText;
  4. }

当然,这个方法也会调用它的super方法,与我们在createChildren()方法中做的类似。 Flex组件生命周期给我们提供了一个名叫 invalidateProperties()的失效方法。 在组件中,我们可以随时调用这个方法,那么在下一个render事件中,组件将强制 commitProperties() 触发。 这就是 createChildren() 方法和commitProperties()方法的一个不同之处. createChildren() 仅运行一次; 而commitProperties() 在初始化安装期间运行一次,之后有需要的时候还可以再次调用。 

为了触发 commitProperties() 失效,我们将用get/set 属性来取代questionText 的变量属性。 Flash Builder 4包含一些代码来做这个,其中的代码类似以下代码: 

  1. private var _questionText : String;
  2. [Bindable]
  3. public function get questionText(): String{
  4. return this._questionText;
  5. }
  6. public function set questionText(value:String):void{
  7. this._questionText = value;
  8. }

commitProperties() 方法在应用程序运行过程中运行,比我们改变questionText更为常见。 为了让commitProperties()知道它究竟需要做什么,我们需要加一个属性变化标识, 像这样: 

  1. private var questionTextChanged : Boolean = false;

set方法用来将标识设置为true,并调用invalidateProperties()方法: 

  1. public function set questionText(value:String):void{
  2. this._questionText = value;
  3. this.questionTextChanged = true;
  4. this.invalidateProperties()
  5. }

还需要重新访问commitProperties() 方法来检查标识: 

  1. override protected function commitProperties():void{
  2. super.commitProperties();
  3. if(this.questionTextChanged == true){
  4. this.question.text = questionText;
  5. this.questionTextChanged = false;
  6. }
  7. }

通过建立使用变量属性的selectedAnswer 方法,组件用户可以随意改变,这也会导致未意料的效果。 我们可以用get方法来取代这个属性。 省去set方法的话,只能从外部读取值。 这是更新过的set方法:

  1. private var _selectedAnswer : String
  2. [Bindable(event='selectedAnswerChanged')]
  3. public function get selectedAnswer (): String{
  4. return this._selectedAnswer;
  5. }

注意,我改变了Bindable元数据标签。 我增加了一个事件而没有使用它的默认状态。 当set方法存在时,Flex框架知道如何绑定属性,但是若没有set方法,则会引发警告。 这个方案用来指定绑定事件,当属性改变时,您可以自己发布。 我们为属性变化新增了一个方法:

  1. protected function setSelectedAnswer(value:String):void{
  2. this._selectedAnswer = value;
  3. this.dispatchEvent(new Event('selectedAnswerChanged'));
  4. }

属性是在onChange事件处理器中设置的。 我们必须加以更改,这样它才能取到set方法而不是直接取变量属性。

  1. protected function onChange(e:ListEvent):void{
  2. setSelectedAnswer(this.answer.selectedItem.label);
  3. }

我想指出的是,并没有相关代码能阻碍您用不同的访问控制语句来获取get和set方法。 但是,ASDoc工具对此会有点儿问题,这就是我为什么刚才删除了set 和属性名之间的空格。 如果您不用ASDoc的话,就放心创建公共的getter和受保护的setter吧。 

为了验证questionTx的设置,我们对主要的应用程序文件做些调整。 在q1上增加一个 TextInput 和一个 button 来修改questionText: 

  1. <mx:TextInput id="questionText" />
  2. <mx:Button click="q1.questionText = questionText.text" />

运行测试代码,您会发现我们改变问题文本一点儿问题都没有了;只读的selectedAnswer属性仍然会随着问题文本改变主要应用程序的文本组件。 一切都很好。 

版本 4: 迈向完全的 ActionScript

如果您看一下您的组件代码,您会发现大部分已经是ActionScript了。 把MXML组件转化为ActionScript组件并不是一个大的跳跃。 组件从包定义开始:

package com.flextras.InsideRIA.MXMLToAS3

包定义在组件所在的文件结构中。 因此, 在YesNoQuestionV4这个应用程序中。文件存放于com目录下的Flextras 下InsideRIA目录下的MXMLToAS3。 com目录存放于主要源码根目录下。 这部分在MXML中是对我们隐藏的。 

接下来我们将类导入: 

  1. import mx.containers.HBox;
  2. import mx.controls.ComboBox;
  3. import mx.controls.Text;
  4. import mx.collections.ArrayCollection;
  5. import mx.events.ListEvent;

这些与之前的MXML版本的唯一不同在于我们导入了Hbox,我们的组件并不以此为基础。 这些也同样导入到MXML组件的脚本标签中。 在 ActionScript 版本中,不导入到脚本标签,事实上ActionScript中根本就没有脚本标签。 

接下来就是类定义: 

public class YesNoQuestionV4 extends HBox

类可以与属性和方法用相同的访问控制语句。 在MXML中开发时不能指定访问控制语句 下面是类构造函数:

public function YesNoQuestionV4(){super();}

在这个例子,构造函数除了调用父类的构造函数外什么都不做。 但是,我会经常用它定义默认样式或者操作组件状态的安装。 您通常对creationComplete 事件写的任何代码十有八九都属于构造函数,然而MXML组件不支持这些构造函数。 

接下来是我们之前MXML版本中代码段中的所有ActionScript代码。 在这儿我就不再复制了。 最后,方括号从类开始,到包定义结束

虽然现在您的组件已经100%都是ActionScript了,我们的主程序并不需要改变。 真正实现了隐藏的话,使用您的组件的应用程序或者其他组件不关心它是ActionScript还是MXML,还是两者混合实现的。

版本5: 扩展UI组件

在之前的版本中,我们扩展了HBox类。 这让我们做起来更加容易,因为我们能使用HBox的继承能力来定位布局问题文本以及ComboBox组件。 然而,一些类中的布局算法对于您的需求来说可能过于复杂。 有些时候一些简单的就能提供比较好的性能。 我们将在YesNoQuestion组件的最后一个版本中,扩展UI组件。 

第一行用来修改类定义。 之前它扩展了HBox,现在它扩展UI组件,如下: 

public class YesNoQuestionV5 extends UIComponent

这是唯一需要更改的一行代码,但是我们需要做些补充。 有两个Flex生命周期方法我们还未曾使用,那就是measure() 方法和 updateDisplayList()方法 将这两个方法实现了就可以完成我们的组件开发了。 

measure() 方法用来是为您的组件设置合理的高度和宽度,从而不会产生滚动条。 组件的父类最终为它的形状负责,因此measure() 方法其实只是设置时的参考建议,通过 measuredHeight和 measuredWidth这两个属性来实现。 

下面是这个方法: 

  1. override protected function measure():void{
  2. super.measure();
  3. this.measuredHeight = question.measuredHeight + answer.measuredHeight;
  4. this.measuredWidth = question.measuredWidth + answer.measuredWidth;
  5. }

这个方法重写了父方法,并调用了该方法的super版本。 然后它通过对每个子方法的 measuredHeight和measuredWdth 求和算出了measuredHeight。 在大部分情况下,这个计算方法只是循环了子组件,计算出了我们到此为止的近似值。 

Measure()方法可以任意设置measuredMinWidth和measuredMinHeight属性。这两个属性指定了组件最小能缩到 多小程度。我并没有在这人指定值,但是经常会默认他们为100,仅仅是为了他们有个值。我已经碰到过不指定最小值的组件在使用百分比高度时的老问题。 

最后一个方法是实现 updateDisplayList()方法, updateDisplayList()方法主要用来定位子组件并设置其大小。 然而您也可以用它来完成其他显示项,例如设置样式或者用图形API画图。以下就是我们的 updateDisplayList()方法:

  1. override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void{
  2. this.question.setActualSize(
  3. this.question.getExplicitOrMeasuredWidth(),
  4. this.question.getExplicitOrMeasuredHeight());
  5.  
  6. this.question.move(0,0);
  7.  
  8. this.answer.setActualSize(
  9. this.answer.getExplicitOrMeasuredWidth(),
  10. this.answer.getExplicitOrMeasuredHeight());
  11.  
  12. this.answer.move(this.question.width, 0);
  13.  
  14. }

updateDisplayList()方法包含在组件的unscaledWidth和the unscaledHeight这两个自变量中。实质上,这些值是您想用来设置组件的大小。为了定位组件,使用了move方法。第一个question放在 左上角,x坐标和y坐标均为0.answer组件挨着第一个放置,x坐标值等于question实例的宽度,y坐标值仍然为0. setActualSize() 方法用来设置组件的大小。在这个例子中,我们用了两个方法:getExplicitoOrMeasuredWidth() 和getExplicitOrMeasuredHeight()方法。既然我们从来没有设置明确的高度或者宽度,那么就将组件设置为计算出的高度和宽 度。 

最后的代码

在您开始建立组件的时候,不妨略加思考。 为了重用以及在多种场合以不同方式使用这些组件的话,您想要优化他们么? 或者这些只是你想要为您的应用程序创建的一次性组件。 即使您是用ActionScript搭建的所有一切,它也不值得你花费额外的时间。 但是,尽管用了MXML组件,您仍然需要利用ActionScript技术和Flex组件生命周期方法来创建健壮的组件。 

为了保持前后一致,最终代码就会附在本文末尾 :

  1. package com.flextras.InsideRIA.MXMLToAS3
  2. {
  3. import mx.containers.HBox;
  4. import mx.controls.ComboBox;
  5. import mx.controls.Text;
  6. import mx.collections.ArrayCollection;
  7. import mx.events.ListEvent;
  8. import mx.core.UIComponent;
  9.  
  10. public class YesNoQuestionV5 extends UIComponent
  11. {
  12. public function YesNoQuestionV5()
  13. {
  14. super();
  15. }
  16.  
  17. [Bindable]
  18. public var dp : ArrayCollection = new ArrayCollection([
  19. {label:'Yes'},
  20. {label:'No'}
  21. ]);
  22.  
  23. private var _questionText : String;
  24. private var questionTextChanged : Boolean = false;
  25. [Bindable]
  26. public function get questionText(): String{
  27. return this._questionText;
  28. }
  29. public function set questionText(value:String):void{
  30. this._questionText = value;
  31. this.questionTextChanged = true;
  32. this.invalidateProperties()
  33. }
  34.  
  35. private var _selectedAnswer : String
  36. [Bindable(event='selectedAnswerChanged')]
  37. public function get selectedAnswer (): String{
  38. return this._selectedAnswer;
  39. }
  40.  
  41. protected function setSelectedAnswer(value:String):void{
  42. this._selectedAnswer = value;
  43. this.dispatchEvent(new Event('selectedAnswerChanged'));
  44. }
  45.  
  46. protected var question : Text;
  47. protected var answer : ComboBox;
  48.  
  49. override protected function commitProperties():void{
  50. super.commitProperties();
  51. if(this.questionTextChanged == true){
  52. this.question.text = questionText;
  53. this.questionTextChanged = false;
  54. }
  55. }
  56.  
  57. override protected function createChildren():void{
  58. super.createChildren();
  59.  
  60. this.question = new Text();
  61. this.addChild(this.question);
  62.  
  63. this.answer = new ComboBox();
  64. this.answer.dataProvider = this.dp;
  65. this.addChild(this.answer);
  66. this.answer.addEventListener(ListEvent.CHANGE, onChange);
  67. }
  68.  
  69. override protected function measure():void{
  70. super.measure();
  71.  
  72. this.measuredHeight = this.question.measuredHeight + this.answer.measuredHeight;
  73. this.measuredWidth = this.question.measuredWidth + this.answer.measuredWidth;
  74. }
  75.  
  76. override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void{
  77. this.question.setActualSize( this.question.getExplicitOrMeasuredWidth(), this.question.getExplicitOrMeasuredHeight());
  78. this.question.move(0,0);
  79. this.answer.setActualSize(this.answer.getExplicitOrMeasuredWidth(), this.answer.getExplicitOrMeasuredHeight());
  80. this.answer.move(this.question.width, 0);
  81.  
  82. }
  83.  
  84.  
  85. protected function onChange(e:ListEvent):void{
  86. setSelectedAnswer(this.answer.selectedItem.label);
  87. }
  88.  
  89.  
  90. }
  91. }