原文地址是: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里,代码看起来像这样:
<?xml version= "1.0" encoding= "utf-8" ?> <mx :HBox xmlns :mx= "http://www.adobe.com/2006/mxml" width= "100%" > <mx :Script > <! [CDATA [ import mx.collections.ArrayCollection; [Bindable ] public var dp : ArrayCollection = new ArrayCollection ( [ {label : 'Yes' }, {label : 'No' } ] ); ] ] > </mx :Script > <mx :Text id= "question" /> <mx :ComboBox id= "answer" dataProvider= "{dp}" /> </mx :HBox >
我们已经用别的MXML组件做了第一个ActionScript的应用。 ComboBox的dataProvider是脚本编写的, 它包含了两个对象,一个是,一个否。
不幸地是,这个组件依然缺少功能。 当用这个组件的时候,我们怎么指定问题文本呢? 您可以用“question.text”,但是如果我们能让他简单点儿会更好。 我们怎么知道被调查者选了什么答案呢? 我们也需要增加属性来描述那个值。
在ActionScript代码块添加这两个变量:
既然变量是公共的,人们就很容易用我们的组件获取这些变量。 我称之为变量属性,尽管我认为这并非正式名称。 接下来,您将会将变量绑定到这两个组件。 像这样修改MXML:
<mx :Text id= "question" text= "{questionText}" /> <mx :ComboBox id= "answer" dataProvider= "{dp}" change= "selectedAnswer = answer.selectedItem.label" />
数据绑定将问题文本绑定至文本显示。 ComboBox值改变时,您可以用change事件来更新已选择的答案。 实际上,这个组件都可以完成我们想要它实现的功能。 现在我们来验证一下。
为了验证这个组件,您得先创建一个简单的工程。 我决定建一个AIR的工程来验证,这样就不用费时做web server的设置了。 从代码观点而言,一个基于web的工程几乎没什么不同,只是将WindowedApplication转换为 Application。 以下就是我主要的应用程序文件:
<?xml version= "1.0" encoding= "utf-8" ?> <mx :WindowedApplication xmlns :mx= "http://www.adobe.com/2006/mxml" layout= "absolute" xmlns :MXMLToAS3= "com.flextras.InsideRIA.MXMLToAS3.*" > <mx :VBox > <MXMLToAS3 :YesNoQuestionV1 id= "q1" questionText= "Do you want to take a Survey?" /> <mx :Text text= "{q1.selectedAnswer}" /> </mx :VBox > </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。
首先,创建组件变量:
protected var question : Text; protected var answer : ComboBox;
因为这些是受保护的变量,意味着扩展该组件的任意组件都能访问这些变量,但是如果新组件未扩展该文件,就不能访问了。 这就是 createChildren()方法:
override protected function createChildren ( ) : void { super.createChildren ( ); this.question = new Text ( ); this.question. text = questionText; this. addChild ( this.question ); this.answer = new ComboBox ( ); this.answer.dataProvider = this.dp; this. addChild ( this.answer ); this.answer. addEventListener (ListEvent. CHANGE, onChange ); }
createChildren)_是在UIComponent类中初始化定义的。 所有的Flex用UI组件都扩展了UIComponent,我们的也不例外,虽然我们要靠近底端一些。 为了在YesNoQuestion组件中实现这个方法,我们重写了signature方法,并调用了super()方法。 在重写方法时,调用super方法非常重要。 您永远不会知道哪些代码会在更高层次执行。 该组件创建了问题文本实例和答案ComboBox实例。 它设置了默认值并将其加入容器。
在MXML版本中,您用了change事件来保持已选择答案的同步。而在ActionScript中,您会用相同的方法,只不过需要通过 addEventListener()来设置事件监听器。它指定您要监听的事件类型及事件发布时要运行的函数,例如本例中的change事件。当我在事件 类中会提到事件常量而不用事件名“change”。当然,两者都可以,只不过说常量会更灵活一些。如果事件名称变化了,我们也不用改代码。
以下就是监听函数:
protected function onChange (e :ListEvent ) : void { this.selectedAnswer = this.answer.selectedItem.label; }
这个监听函数接收事件参数, 这个方法中的单行代码与之前的MXML版本使用的顺序代码是一样的。
版本 3: commitProperties()
如果当createChildren() 被调用时,questionText 仍然是空字符串,那会怎样呢? 那么问题就会什么都不显示。 在目前的代码中,questionText的属性变化时并不会更新问题。 这里有一个解决方案。 Flex框架提供了 commitProperties() 方法在组件的所有属性都设置好以后来运行代码。 只要值一变化,我们就使用这个方法来设置更新问题文本。
首先,将commitProperties()方法加入代码。 我们可以将这行复制粘贴到body中以设置问题文本。
override protected function commitProperties ( ) : void { super.commitProperties ( ); this.question. text = questionText; }
当然,这个方法也会调用它的super方法,与我们在createChildren()方法中做的类似。 Flex组件生命周期给我们提供了一个名叫 invalidateProperties()的失效方法。 在组件中,我们可以随时调用这个方法,那么在下一个render事件中,组件将强制 commitProperties() 触发。 这就是 createChildren() 方法和commitProperties()方法的一个不同之处. createChildren() 仅运行一次; 而commitProperties() 在初始化安装期间运行一次,之后有需要的时候还可以再次调用。
为了触发 commitProperties() 失效,我们将用get/set 属性来取代questionText 的变量属性。 Flash Builder 4包含一些代码来做这个,其中的代码类似以下代码:
[Bindable ] return this._questionText; } this._questionText = value; }
commitProperties() 方法在应用程序运行过程中运行,比我们改变questionText更为常见。 为了让commitProperties()知道它究竟需要做什么,我们需要加一个属性变化标识, 像这样:
set方法用来将标识设置为true,并调用invalidateProperties()方法:
this._questionText = value; this.questionTextChanged = true; this.invalidateProperties ( ) }
还需要重新访问commitProperties() 方法来检查标识:
override protected function commitProperties ( ) : void { super.commitProperties ( ); if ( this.questionTextChanged == true ) { this.question. text = questionText; this.questionTextChanged = false; } }
通过建立使用变量属性的selectedAnswer 方法,组件用户可以随意改变,这也会导致未意料的效果。 我们可以用get方法来取代这个属性。 省去set方法的话,只能从外部读取值。 这是更新过的set方法:
[Bindable (event= 'selectedAnswerChanged' ) ] return this._selectedAnswer; }
注意,我改变了Bindable元数据标签。 我增加了一个事件而没有使用它的默认状态。 当set方法存在时,Flex框架知道如何绑定属性,但是若没有set方法,则会引发警告。 这个方案用来指定绑定事件,当属性改变时,您可以自己发布。 我们为属性变化新增了一个方法:
this._selectedAnswer = value; }
属性是在onChange事件处理器中设置的。 我们必须加以更改,这样它才能取到set方法而不是直接取变量属性。
protected function onChange (e :ListEvent ) : void { setSelectedAnswer ( this.answer.selectedItem.label ); }
我想指出的是,并没有相关代码能阻碍您用不同的访问控制语句来获取get和set方法。 但是,ASDoc工具对此会有点儿问题,这就是我为什么刚才删除了set 和属性名之间的空格。 如果您不用ASDoc的话,就放心创建公共的getter和受保护的setter吧。
为了验证questionTx的设置,我们对主要的应用程序文件做些调整。 在q1上增加一个 TextInput 和一个 button 来修改questionText:
<mx :TextInput id= "questionText" /> <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中是对我们隐藏的。
接下来我们将类导入:
import mx.containers.HBox; import mx.controls.ComboBox; import mx.controls.Text; import mx.collections.ArrayCollection; 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这两个属性来实现。
下面是这个方法:
override protected function measure ( ) : void { super.measure ( ); this.measuredHeight = question.measuredHeight + answer.measuredHeight; this.measuredWidth = question.measuredWidth + answer.measuredWidth; }
这个方法重写了父方法,并调用了该方法的super版本。 然后它通过对每个子方法的 measuredHeight和measuredWdth 求和算出了measuredHeight。 在大部分情况下,这个计算方法只是循环了子组件,计算出了我们到此为止的近似值。
Measure()方法可以任意设置measuredMinWidth和measuredMinHeight属性。这两个属性指定了组件最小能缩到 多小程度。我并没有在这人指定值,但是经常会默认他们为100,仅仅是为了他们有个值。我已经碰到过不指定最小值的组件在使用百分比高度时的老问题。
最后一个方法是实现 updateDisplayList()方法, updateDisplayList()方法主要用来定位子组件并设置其大小。 然而您也可以用它来完成其他显示项,例如设置样式或者用图形API画图。以下就是我们的 updateDisplayList()方法:
this.question.setActualSize ( this.question.getExplicitOrMeasuredWidth ( ), this.question.getExplicitOrMeasuredHeight ( ) ); this.question.move ( 0, 0 ); this.answer.setActualSize ( this.answer.getExplicitOrMeasuredWidth ( ), this.answer.getExplicitOrMeasuredHeight ( ) ); this.answer.move ( this.question. width, 0 ); }
updateDisplayList()方法包含在组件的unscaledWidth和the unscaledHeight这两个自变量中。实质上,这些值是您想用来设置组件的大小。为了定位组件,使用了move方法。第一个question放在 左上角,x坐标和y坐标均为0.answer组件挨着第一个放置,x坐标值等于question实例的宽度,y坐标值仍然为0. setActualSize() 方法用来设置组件的大小。在这个例子中,我们用了两个方法:getExplicitoOrMeasuredWidth() 和getExplicitOrMeasuredHeight()方法。既然我们从来没有设置明确的高度或者宽度,那么就将组件设置为计算出的高度和宽 度。
最后的代码
在您开始建立组件的时候,不妨略加思考。 为了重用以及在多种场合以不同方式使用这些组件的话,您想要优化他们么? 或者这些只是你想要为您的应用程序创建的一次性组件。 即使您是用ActionScript搭建的所有一切,它也不值得你花费额外的时间。 但是,尽管用了MXML组件,您仍然需要利用ActionScript技术和Flex组件生命周期方法来创建健壮的组件。
为了保持前后一致,最终代码就会附在本文末尾 :
package com.flextras.InsideRIA.MXMLToAS3 { import mx.containers.HBox; import mx.controls.ComboBox; import mx.controls.Text; import mx.collections.ArrayCollection; import mx.events.ListEvent; import mx.core.UIComponent; public class YesNoQuestionV5 extends UIComponent { public function YesNoQuestionV5 ( ) { super ( ); } [Bindable ] public var dp : ArrayCollection = new ArrayCollection ( [ {label : 'Yes' }, {label : 'No' } ] ); [Bindable ] return this._questionText; } this._questionText = value; this.questionTextChanged = true; this.invalidateProperties ( ) } [Bindable (event= 'selectedAnswerChanged' ) ] return this._selectedAnswer; } this._selectedAnswer = value; } protected var question : Text; protected var answer : ComboBox; override protected function commitProperties ( ) : void { super.commitProperties ( ); if ( this.questionTextChanged == true ) { this.question. text = questionText; this.questionTextChanged = false; } } override protected function createChildren ( ) : void { super.createChildren ( ); this.question = new Text ( ); this. addChild ( this.question ); this.answer = new ComboBox ( ); this.answer.dataProvider = this.dp; this. addChild ( this.answer ); this.answer. addEventListener (ListEvent. CHANGE, onChange ); } override protected function measure ( ) : void { super.measure ( ); this.measuredHeight = this.question.measuredHeight + this.answer.measuredHeight; this.measuredWidth = this.question.measuredWidth + this.answer.measuredWidth; } this.question.setActualSize ( this.question.getExplicitOrMeasuredWidth ( ), this.question.getExplicitOrMeasuredHeight ( ) ); this.question.move ( 0, 0 ); this.answer.setActualSize ( this.answer.getExplicitOrMeasuredWidth ( ), this.answer.getExplicitOrMeasuredHeight ( ) ); this.answer.move ( this.question. width, 0 ); } protected function onChange (e :ListEvent ) : void { setSelectedAnswer ( this.answer.selectedItem.label ); } } }