Flex 数据绑定易犯的错误:普遍的误用和错误
数据绑定(data binding)- 在创建Flex或Aodbe AIR应用程序中,最常用而且很有用的是将数据从一个对象传递给另外一个对象。于此同时,若开发者不完全理解它的机制的话,可能会给程序造成初始化缓慢或失败的问题。在必须使用的时候,正确地使用数据绑定无疑是好的办法。在这篇文章中,我列出了10个最易犯的错误,以便开发者在创建应用中合理地使用数据绑定。
错过无声的错误
有一种情况,就是你使用了数据绑定,但最终的结果不是你所期待的,没有看到绑定的效果;而且也没有错误提示。
绑定表达式或绑定的函数里抛出的异常和错误,被绑定框架所捕获,这种叫无声捕获。最终导致的结果就是,你通过Flash Player的debug版本去调试程序时,不能在运行时看到异常信息。不仅绑定功能没有工作,连错误都没显示出来。
为什么会有这种无声捕获呢?
在绑定情景出现前,代码需要满足绑定机制所必须的条件。绑定机制可以吞咽任何错误以阻止在运行时抛出异常信息。这样可以避免出现哪些不必要的错误提示信息。
观察并考虑下面这个简单的绑定例子:
- <?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" minWidth="955" minHeight="600"
- preinitialize="preinitializeHandler(event)"
- >
- <fx:Script>
- <!--[CDATA[
- import mx.events.FlexEvent;
- [Bidable]
- private var xml:XML =
- <users>
- <user>
- <name>EladElrom</name>
- <address>1 Wall Street</address>
- </user>
- </users>;
- protected function preinitializeHandler(event:FlexEvent):void
- {
- xml = null;
- }
- ]]-->
- </fx:Script>
- <s:Label id="label" text="{xml.user.name}" />
- </s:Application>
我已把xml变量绑定到一个Label控件上了。代码看上去没有任何错误,但是,我在预初始化程序时把变量xml的值赋为了null。当初始化这个Laberl 控件时,没有任何设置。因为变量xml为空,所以xml对象的name属性也为空。当你运行程序时,你会看到界面没有任何显示,也没有异常错误,这时的错误信息被悄悄地捕获了。
Debugging bindings
尽管这些错误信息被悄悄地捕获了,但还是可以查出它们在哪里被捕获了。在BindingManager.as和Binding.as代码中,我们可以跟踪到它,但首先你必须安装了完整的Flex SDK.(见图1)
相反,你可以设置断点和钻取绑定对象相关的属性值,查明是什么错误。在这种情况下,你会发现XML 对象的值被设置为null,导致了绑定失败。
另外一个解决办法更直观,就是运用BindingManger 类里的debugBinding方法。设置你所要看到的控件和属性,然后你能看到错误信息如何被解雇和捕获的。
在上面的例子代码中,我注释了BindingManager.debugBinding("label.text");这句代码。
放开注释,重新调试运行;可以在控制台栏里查看错误信息。(见图2)
看一看 Binding.as和BindingManager.as类,会发现代码里有好多if 和 try... catch 语句,这些语句确保是否是有效的绑定。下面给出在使用绑定时的错误信息会被悄悄地捕获:
Error #1006: Call attempted on an object that is not a function.
Error #1009: null has no properties.
Error #1010: undefined has no properties.
Error #1055: - has no properties.
Error #1069: Property - not found on - and there is no default value
如果会出现上面任何一个错误,绑定管理者会悄悄地捕获,绑定就起不了作用。
运行时错误在大多数场景下会正常地出现。比如,当含有绑定表达式的Label控件初始化时,目标对象的属性没有没赋值或表达式不正确,导致发生了1009的错误信息。同样地,一些绑定表达式只有在应用程序发生动作时才起作用。比如你将数据绑定到一个List 控件的 selectedItem 属性上,只有当你选择item时才起到绑定的作用。
试图绑定到一个没有实现IPropertyChangeNotifier接口的类上
实现了IPropertyChangeNotifier接口的类,当类的属性发生改变时,都会派发事件。举个例子,你可以查看UIComponent 类的代码。实际上UIComponent 类实现了,当属性一旦改变都会派发dispatchPropertyCangeEvent事件。
现在来查看下面这个用户信息的类:
- package vo
- {
- public final class UserInfo
- {
- public var userName:String;
- public var password:String;
- public function UserInfo()
- {
- }
- }
- }
当你试图将Label控件的text属性绑定到某个UserIfo类的属性上时,正如下面的代码一样,绑定不起作用。
- <?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"
- minWidth="1024" minHeight="768"
- creationComplete="creationCompleteHandler()">
- <fx:Script>
- <!--[CDATA[
- import vo.UserInfo;
- [Bindable]
- private var userInfo:UserInfo = new UserInfo();
- protected function creationCompleteHandler():void
- {
- userInfo.userName = "EladElrom";
- }
- ]]-->
- </fx:Script>
- <s:Label id="lbl" text="{userInfo.userName}" />
- </s:Application>
因为上面的代码试图绑定到一个没有实现IPropertyChangeNotifier接口的类上,绑定机制不能工作。
在这种情况下,在IDE的Problems视图窗口中会看到如下信息:
data binding will not be able to detect assignments to "userName".
在上面这个例子中,你可以通过给UserIfno类添加一个[Bindable]标签来解决绑定不工作的问题。这样就可以确
保这个类的公有属性都可以被绑定。Flex编译器会生成相应的一对公有的getter和setter构造器,来确保它们满足
绑定机制的要求。或者,若不希望所有属性可被绑定,可以将[Bindable]标签添加到类的指定属性上。
- package vo
- {
- [Bindable]
- public final class UserInfo
- {
- public var userName:String;
- public var password:String;
- public function UserInfo()
- {
- }
- }
- }
ObjectProxy 类
如果一个类要想使用数据绑定的功能,必须得实现IPropertyChangeNotifier接口;否则,这个对象不能够绑定的。
但是,有一些类的属性或变量,比如简单的变量,它们不能使用[Bindable]标签,也不能实现必要的接口。也就是说,
这个类属于你创建的,你可以添加[Bindable]来轻松实现绑定的功能;但若这个类不属于你,而你又想实现绑定的功能,
这时你就可以考虑使用ObjectProxy类。ObjectProxy 类包装一个非绑定类和一个属性改变就会派发的PropertyChangeEvent事件。
下面是ObjectProxy的应用例子。创建一个ObjectProxy类对象,并用非绑定类来实例化ObjectProxy,在这里我们用UserInfo这个类。
接着给实例化对象添加一个PropertyChangeEvent事件的侦听函数,并跟踪UserInfo某个条目的改变。
- 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"
- minWidth="1024" minHeight="768"
- creationComplete="creationCompleteHandler()">
- <fx:Script>
- <!--[CDATA[
- import mx.events.PropertyChangeEvent;
- import mx.utils.ObjectProxy;
- import vo.UserInfo;
- private var userInfo:UserInfo = new UserInfo();
- private var objectProxy:ObjectProxy;
- protected function creationCompleteHandler():void
- {
- objectProxy = new ObjectProxy( userInfo );
- objectProxy.addEventListener( PropertyChangeEvent.PROPERTY_CHANGE, onPropertyChange );
- objectProxy.userName = "EladElrom";
- objectProxy.password = "123";
- }
- private function onPropertyChange( event:PropertyChangeEvent ):void
- {
- lbl.text = event.newValue.toString();
- }
- ]]-->
- </fx:Script>
- <s:Label id="lbl" />
- </s:Application>
其中ObjectProxy类时,有一个需要你注意的就是,为了方便通知的派发,每当目标对象的任何对象改变,都会调用事件注册的侦听者。
这样有时显得过了头,例如在上面的例子中,onPropertyChange 事件是被调用了两次。
使用绑定代替直接赋值
在某种情况下不需要使用绑定,完全可以使用直接赋值来达到同样的效果,这时最好要避免使用绑定。我看到过各种各样这种类型的错误。
下面的代码是是其中一个例子:
- <?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"
- minWidth="1024" minHeight="768">
- <fx:Script>
- <!--[CDATA[
- private var text:String;
- ]]-->
- </fx:Script>
- <s:layout>
- <s:VerticalLayout/>
- </s:layout>
- <s:TextInput id="textInput2" text="{text}" />
- </s:Application>
代码中将TextInput控件的text属性绑定到一个私有变量text上。它看起来没有什么错误,对吧?我在Flex应用程序中经常看到这样
的代码。你会发现尽管这个私有的文本型text变量只需要赋值一次,但Flex 编译器会按照绑定机制自动生成相关的代码。另外,还有一些情况,在赋值后取消绑定或删除绑定代码以减少开销,但你不能在MXML类中使用<mx:Binding>标签。
根据经验,避免绑定到一个私有的变量。
在上面的例子中,你可以直接赋值:
- <s:TextInput id="textInput2" text="some text goes here" />
当使用直接赋值,你可以有效地减少内存的开销,因为编译器就不用产生不必要的绑定代码了。
经验来说,使用数据绑定时,要绑定到一个会改变而且将要改变的属性上。
忘记取消绑定或导致内存泄露
你可以在MXML类中使用<mx:Binding>标签来轻易地实现绑定的功能,但是这样的解决方法可能会带来内存开销过多。
另外就是,使用这样子的绑定方法,将来很难取消绑定。如果要将程序优化成一个高性能标准,你可以使用BindignUtils类
来进行绑定你的对象。关于BindingUtils类有两种方式:
<>bindProperty():是一个静态的方法,可用于绑定一个公有的属性。
<>bindSetter():是一个静态的方法,胜于绑定一个函数。
来看一看bindProerty方法的声明:
- public static function bindProperty(
- site:Object, prop:String,
- host:Object, chain:Object,
- commitOnly:Boolean = false,
- useWeakReference:Boolean = false):ChangeWatcher
site,host参数分别表示各自的目标和源对象。如果仅在提交 change 事件时需要调用处理函数,则设置为 true
;如果无论
是否提交 change 事件都调用处理函数,则设置为 false
。
useWeakReference参数相对于host来说是强引用还是弱引用。强引用(默认的)能阻止垃圾对host的回收,弱引用不能。
下面这个例子包含一个文本框和一个Label控件。当TextInput控件在preinitialized阶段时,preinitializeHandler()方法
被调用,里面用到了bindProperty这种方式。
- <?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"
- minWidth="1024" minHeight="768">
- <fx:Script>
- <!--[CDATA[
- import mx.binding.utils.BindingUtils;
- import mx.events.FlexEvent;
- protected function preinitializeHandler(event:FlexEvent):void
- {
- BindingUtils.bindProperty(label, "text", textInput, "text");
- }
- ]]-->
- </fx:Script>
- <s:layout>
- <s:VerticalLayout/>
- </s:layout>
- <s:TextInput id="textInput" preinitialize="preinitializeHandler(event)" />
- <s:Label id="label" />
- </s:Application>
下面是bindSetter方式的定义:
- public static function bindSetter(setter:Function, host:Object,
- chain:Object,
- commitOnly:Boolean = false,
- useWeakReference:Boolean = false):ChangeWatcher
setter参数为回调函数,当源host的chain的值改变时,都会触发这个回调函数。
下面就是利用bindSetter来进行绑定的一个例子:
- <?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"
- minWidth="1024" minHeight="768">
- <fx:Script>
- <!--[CDATA[
- import mx.binding.utils.BindingUtils;
- import mx.events.FlexEvent;
- protected function preinitializeHandler(event:FlexEvent):void
- {
- BindingUtils.bindSetter(bindingSetter, textInput, "text");
- }
- private function bindingSetter(str:String):void
- {
- label.text = str;
- }
- ]]-->
- </fx:Script>
- <s:layout>
- <s:VerticalLayout/>
- </s:layout>
- <s:TextInput id="textInput" preinitialize="preinitializeHandler(event)" />
- <s:Label id="label" />
- </s:Application>
在幕后,ChangeWatcher类被用在
BindingUtils
类里。它允许使用弱引用,所以当你将host
的
useWeakReference
值设为true时,垃圾回收器会自动地进行回收,避免内存的泄露。
当使用弱引用,并且不再使用绑定功能情况下。这时ChangeWatcher
需要设置成
unwatched的状态。为了确保内存不被浪费,这项任务是必须的。如下面的例子:
- <?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"
- minWidth="1024" minHeight="768">
- <fx:Script>
- <!--[CDATA[
- import mx.binding.utils.ChangeWatcher;
- import mx.binding.utils.BindingUtils;
- import mx.events.FlexEvent;
- private var change:ChangeWatcher;
- protected function preinitializeHandler(event:FlexEvent):void
- {
- change = BindingUtils.bindProperty( label, "text",
- textInput, "text", false, true);
- }
- protected function clickHandler(event:MouseEvent):void
- {
- change.unwatch();
- change = null;
- }
- ]]-->
- </fx:Script>
- <s:layout>
- <s:VerticalLayout/>
- </s:layout>
- <s:TextInput id="textInput"
- preinitialize="preinitializeHandler(event)" />
- <s:Label id="label" />
- <s:Button label="Stop binding" click="clickHandler(event)" />
- </s:Application>
将TestInput的text属性绑定到Label控件的text属性上,一旦你在TextInput控件输入任何
文字时,数据会拷贝到Label控件的text属性上。当你准备取消绑定时,点击'Stop Binding'
按钮。这时不再观察属性的改变,将对象设置为null以便于进行垃圾回收。
不改变默认的事件常量-propertyChange
在没有添加事件设置使用Bindable标签来进行绑定时,propertyChange作为默认的事件类型被派发。
所以一个[Bindable]标签相当于Bindable(event="propertyChange")这句话。当你没有特别指明事件的
类型时,编译器会额外创建它的setter和getter构造器代码;所以,你要定义自己的常量以免造成过多
的内存浪费。
细想下面的表达式:
- [Bindable]
- public var testData:String;
因没有设置事件的名称,编译器会将它编译成如下代码:
- [Bindable(event="propertyChange")]
- public function get testData():String
- {
- return this._1147269284testData;
- }
- public function set testData(value:String):void
- {
- var oldValue:Object = this._1147269284testData;
- if (oldValue !== value) {
- this._1147269284testData = value;
- if (this.hasEventListener("propertyChange"))
- this.dispatchEvent(mx.events.PropertyChangeEvent.createUpdateEvent
- (this, "testData", oldValue, value));
- }
- }
正如你所看到的,编译器生成的setter构造器中有派发PropertyChangeEvent事件的
代码。当你改变默认的事件名称时,Flex编译器将不再生成额外的代码,这时你要为事件
派发负责了。这样能够给你可以优化侦听者代码。若你不改变默认的设置,每一个使用
[Bindable]的属性,都会派发PropertyChangeEvent事件;而它们的侦听者都会响应。
在一个大量使用[Bindable]属性的类中,这个过程是很占用内存的。
一旦你把事件声称了自己的,比如下面例子,编译器在编译时只是做一下拷贝。
- Private var _number:Number = 0;
- [Bindable(event="changeNumberEvent")]
- public function get number():Number
- {
- return _number;
- }
- public function set number(value:Number) : void
- {
- _number = value;
- dispatchEvent(new Event("changeNumberEvent"));
- }
使用错误Bindable事件的名称
在[Bindable]标签中使用错误的事件名称,导致绑定不起作用,而且你可能还不知道这是为什么。当你在使用
[Bidnable]标签中自定义的事件名称,下面的例子好像是很好的代码:
- public static const EVENT_CHANGED_CONST:String = "eventChangedConst";
- private var _number:Number = 0;
- [Bindable(event=EVENT_CHANGED_CONST)]
- public function get number():Number
- {
- return _number;
- }
- public function set number(value:Number) : void
- {
- _number = value;
- dispatchEvent(new Event(EVENT_CHANGED_CONST));
- }
上面的代码是将一个静态常量赋值给事件的名称,并使用相同的值去派发事件。但是,当
number值改变时,绑定没有工作。原因是事件的名称是EVENT_CHANGED_CONST,而不是
这个常量的值。
所以将代码改成如下形式,才是正确的:
- public static const EVENT_CHANGED_CONST:String = "eventChangedConst";
- private var _number:Number = 0;
- [Bindable(event="eventChangedConst")]
- public function get number():Number
- {
- return
- _number;
- }
- public function set number(value:Number) : void
- {
- _number = value;
- dispatchEvent(new Event(EVENT_CHANGED_CONST));
- }
假定绑定的执行顺序
一个普遍的错误是在绑定时,假定绑定的发生是按照一定的顺序来发生。这样可能导致你的程序发生警告提示
和绑定不起作用。ActionScript中的事件是异步执行的方式。
看一下面这个例子:
- <?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"
- minWidth="1024" minHeight="768"
- creationComplete="creationCompleteHandler(event)">
- <fx:Script>
- <!--[CDATA[
- import mx.events.FlexEvent;
- [Bindable]
- public var buttonText:String = "Execution order mistake ";
- ]]-->
- </fx:Script>
- <s:layout>
- <s:HorizontalLayout />
- </s:layout>
- <s:Label id="label" text="{buttonText}" />
- <s:Button label="{label.text}" />
- </s:Application>
上面的代码可能会工作,但有时也不能工作。在Button的label属性绑定到Label控件的的text属性上时,
假定Label的text属性值已被改变了。当你编译这个程序时,编译器会给出一个警告信息。
下面是另外一个例子,它假定第一个TextInput控件已存在。这种赋值方式会生成绑定所需要的全部代码。
你必须决定这种方式或直接将(y=35)哪个是比较好的方式。
- <?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"
- minWidth="1024" minHeight="768">
- <s:TextInput id="textInput1" x="10" y="0" />
- <s:TextInput id="textInput2" x="0" y="{textInput1.x+25}" />
- </s:Application>
用绑定代替事件
在多数情况下你可以轻松地使用事件来代替绑定给一个对象进行赋值。你可以使用组件生命周期中的事件或
重写组件的方法如childrenCreated()和initializationComplete()来进行赋值。另外,你可以使用ChangeWatcher
去侦听一个数据的改变,这样做是低耦合的。在使用ChangeWatcher时,多数情况下若能手动地得到数据更改的通知,
应避免使用ChangejWatcher。例如,所有Flex的集合对象都会广播 collectionChange事件,这时你可以手动地得到
该集合对象更改的通知信息。
细想下面的例子:
- <?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"
- minWidth="1024" minHeight="768">
- <fx:Script>
- <!--[CDATA[
- import mx.events.FlexEvent;
- [Bindable]
- public var dp:Array = [ { label:"New York", data: "New York" },
- { label:"Miami Beach", data: "Miami Beach" } ];
- ]]-->
- </fx:Script>
- <mx:ComboBox id="cb" editable="false" width="100" dataProvider="{dp}" />
- </s:Application>
上面的代码看上去是flex应用程序的标准代码。但是,这时候的绑定不是必须的。你可以使用
事件的处理者直接给变量赋值,来代替绑定。
- <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"
- minWidth="1024" minHeight="768"
- creationComplete="creationCompleteHandler(event)">
- <fx:Script>
- <!--[CDATA[
- import mx.events.FlexEvent;
- public var dp:Array = [ { label:"New York", data: "New York" },
- { label:"Miami Beach", data: "Miami Beach" } ];
- protected function creationCompleteHandler(event:FlexEvent):void
- {
- cb.dataProvider = dp;
- }
- ]]-->
- </fx:Script>
- <mx:ComboBox id="cb" editable="false" width="100" />
- </s:Application>
绑定一个类同时绑定该类的属性
另外一个普遍的错误是绑定一个类同时绑定该类的属性,比如:
- package
- {
- [Bindable]
- public class CustomerVO
- {
- [Bindable]
- public var customerID:int;
- public function CustomerVO(customerID:int)
- {
- this.customerID = customerID;
- }
- }
- }
这时对customerID属性进行绑定是多余的,因为已对CustomerVO进行了绑定。这样做
不仅会在编译时带来警告,而且会生成多余的代码,影响程序的性能。像这样情况,应该
将多余的绑定删除掉。
在不支持的属性上使用双向绑定
Flex4 支持双向绑定。在创建绑定时在大括号前加@符号,而另外一个对象仍处于不被绑定
的状态。如下面的例子:
- <?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"
- minWidth="1024" minHeight="768">
- <s:TextInput text="@{someTextInput.text}"/>
- <s:TextInput id="someTextInput" y="30"/>
- </s:Application>
双向绑定在大多数情况下是工作的,但若绑定的属性是样式或效果的话,就不起作用了。
绑定到RemoteObject对象的arguments属性,HttpService、RemoteObject、WebService的
request属性也是不起作用的。看一下下面的例子:
- <?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"
- minWidth="1024" minHeight="768">
- <fx:Script>
- <!--[CDATA[
- import mx.rpc.events.FaultEvent;
- import mx.rpc.events.ResultEvent;
- protected function resultHandler(event:ResultEvent):void
- {
- // handle result
- }
- protected function faultHandler(event:FaultEvent):void
- {
- // handle fault
- }
- ]]-->
- </fx:Script>
- <fx:Declarations>
- <s:HTTPService id="service" url="http://localhost/someservice.php"
- result="resultHandler(event)"
- fault="faultHandler(event)">
- <mx:request xmlns="">
- <username>@{someTextInput.text}</username>
- </mx:request>
- </s:HTTPService>
- </fx:Declarations>
- <s:TextInput text="@{someTextInput.text}"/>
- <s:TextInput id="someTextInput" y="30"/>
- <s:Button click="service.send()" />
- </s:Application>
绑定不起作用的原因在于源和目标对象的属性必须支持绑定,而且能够支持读和写的操
作,只有这样才能支持双向绑定。
总之,数据绑定是Flex的很大的优点,但它也存在潜在的缺点。我建议在使用数据绑定
时,你要花时间考虑该绑定是不是必须的,使用得是不是完全正确。滥用或误用会带来
内存的消耗,影响程序的性能。