Test Driven Development (TDD) - eladelrom

advertisement
Flex data binding pitfalls:
10 common misuses and mistakes
@EladElrom
@EladElrom
•
•
•
•
•
Associate Dev Director @ Sigma Group
Senior Flash Engineer & Lead
Technical Writer
FlashAndTheCity Organizer
Adobe Community Professional
DataBinding - old topic yet still relevant
Data binding—the process of passing the data in one object to another object
automatically—is one of the most used and useful features when building Flex and
Adobe AIR applications. At the same time, however, data binding can slow
application initialization and cause frustration when developers don't fully
understand how the mechanism works. It is a good idea to make sure you are using
it correctly and only when needed.
TOC
Flex data binding pitfalls: common misuses and mistakes
1. Missing silent errors
2. Trying to bind a class that does not include the
IPropertyChangeNotifier interface
3. Using binding in place of direct assignment
4. Forgetting to unbind and risking memory leaks
5. Not changing the default propertyChange event constant
6. Using the wrong bindable event name
7. Assuming an execution order for binding
8. Using binding in place of events
9. Binding a class and its properties
10. Using two-way data binding for unsupported properties
Lastly, Turbo Binding
Mistake #1: Missing silent errors
Missing Silent Errors
There are cases where the binding operation just does not seem
to work, and you end up frustrated and unsure of what to do.
Exceptions and errors that are thrown by binding expressions, or
in binding functions called within the binding framework, are
silently captured. As a result, you will not see a runtime
exception as you might expect in the debug version of Flash
Player. Not only does the binding not work, but no errors are
shown.
Why Are Errors being Silently
Captured?
The code that implements the binding mechanism requires several
conditions to be met before the binding will occur. The binding mechanism
will swallow any errors to prevent runtime exceptions from being thrown at
runtime. This is a good thing since you do not want to see these (possibly)
unexpected errors in your application.
Binding Error Example
I have added an xml variable
binding to a Label component.
The code would have worked fine;
however, I have set the xml
variable to null during the preinitialization of the component.
The event was dispatched at the
beginning of the component
initialization sequence, so the
Label component was not set yet.
The xml variable gets set to null,
so there is no name property on
the xml object. If you run this
application, you'll notice that
binding does not occur and the
errors have been silently
captured.
Debugging Binding
Although errors are captured silently,
there are still things you can do to
figure out what is going on.
Debugging with the
BindingManager.as and Binding.as
code is not easy since the binding
classes are not available to you unless
you download the entire Flex SDK.
Instead, you can set a break point
and drill down to the related binding
objects to find out what went wrong.
In this case, you would find that the
value of the XML object is set to null,
and that is why the binding has
failed.
Binding and BindingManager
Take a look at the Binding.as and BindingManager.as classes. The
code has many if and try… catch statements that ensure conditions
are met for performing a valid binding. Here are some of the error
cases that can be thrown when using binding:
* 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
If any of these errors occur, the binding manager catches them
silently and will not perform the binding.
Debugging Binding
Another approach, which is more intuitive, is to use the debugBinding
method in the BindingManager class. You set the component and
property you want to watch, and then you can see the errors that were
fired and silently captured.
Null Errors Examples
Runtime errors can occur normally in many scenarios.
1.
2.
a Label component is initialized that contains a data binding
expression, the target property may not yet be set and execution of
the binding expression will result in an Error #1009 – Null has no
properties runtime exception.
Similarly, some data binding expressions are only valid when the user
of your application performs an action such as selecting an item in a
List. If your data binding expression references the selectedItem
property of the List, it will be null until the user actually clicks an
item in the List and selects it.
Debugging Binding
In the example code I left the following line of code commented:
BindingManager.debugBinding("label.text");
Uncomment this line and run the application in
debug mode; you can see the binding errors in
the Console view.
Debugging Binding Console
Mistake #2
Trying to bind a
class that does not include
the IPropertyChangeNotifier
interface
IPropertyChangeNotifier Marker
Classes that implement the IPropertyChangeNotifier marker interface must dispatch
events for properties in the class, and any nested classes are publicly exposed as
properties. As a result, you can find out when properties have changed in the class.
For instance, take a look at the UIComponent class signature. UIComponent indeed
implements dispatchPropertyChangeEvent, which will dispatch an event once
properties have changed.
ValueObject
Now consider the following class that holds
user information:
If you try to bind the text property of a Label to one of the properties of the UserInfo
class, it will not work!
Reason
Because the code is trying to
bind a class that does not
implement
IPropertyChangeNotifier, the
binding mechanism will not
work.
In this case, you'll see the
following message in the
Problems view (see Figure):
Solution
attach the [Bindable] tag to the class. This will enable all public properties of the class
for data binding. The Flex compiler will generate a public getter and setter for you
which will contain all of the code necessary to make data binding work. Alternatively,
you can attach the [Bindable] tag to specific properties of the class if you don't want
to enable all properties for data binding.
ObjectProxy Class
Data binding requires that the class to which you are binding implements
the IPropertyChangeNotifier interface. Otherwise, the object is not
bindable.
However, classes/properties or variables, such as primitive variables, that
are not marked [Bindable] do not implement that interface. If it is your own
class, all you have to do is add the [Bindable] metadata tag. If it's not your
class you wish to bind with, or you want to add binding functionality during
runtime you can use the ObjectProxy class. ObjectProxy wraps a nonbindable class and dispatches a PropertyChangeEvent when any of the
properties of the class are changed, enabling objects in your application to
listen for property changes.
Below is an example of using
ObjectProxy. I create a new
instance of ObjectProxy and
pass the object I want to watch,
in this case UserInfo. I then add
an event listener and track
changes to an item in UserInfo.
ObjectProxy caveat
When using ObjectProxy is that in order to facilitate assignment
notification, registered listeners will be invoked every time any
property on the target object changes. That can potentially
introduce a significant overhead, in fact in our example the
onPropertyChange is called twice since two changes occurred.
Mistake #3: Using binding in place
of direct assignment
What’s wrong with the code below?
Answer
The code defines a TextInput control with a text property binding to
the text private variable. It looks harmless enough, right?
I have seen these types of tags often in Flex applications. The Flex
compiler generates code to allow the binding. You will find that
although you do not need to bind the text String since it is a one-time
assignment, the compiler still generates code to accommodate
binding of the property.
Additionally, there are cases where you want to unbind after the
assignment or remove the binding code to reduce overhead, but you
will not be able to do so using the <mx:Binding> tag in MXML.
“-keep” generated mxmlc code
class BindableProperty
{
/*
* generated bindable wrapper for property someTextInput (public)
* - generated setter
* - generated getter
* - original public var 'someTextInput' moved to '_2040386569someTextInput'
*/
[Bindable(event="propertyChange")]
public function get someTextInput():spark.components.TextInput
{
return this._2040386569someTextInput;
}
public function set someTextInput(value:spark.components.TextInput):void
{
var oldValue:Object = this._2040386569someTextInput;
if (oldValue !== value)
{
this._2040386569someTextInput = value;
if (this.hasEventListener("propertyChange"))
this.dispatchEvent(mx.events.PropertyChangeEvent.createUpdateEvent(this,
"someTextInput", oldValue, value));
}
}
/*
* generated bindable wrapper for property someText (private)
* - generated setter
* - generated getter
* - original private var 'someText' moved to '_1504842817someText'
*/
[Bindable(event="propertyChange")]
private function get someText():String
{
return this._1504842817someText;
}
private function set someText(value:String):void
{
var oldValue:Object = this._1504842817someText;
if (oldValue !== value)
{
this._1504842817someText = value;
if (this.hasEventListener("propertyChange"))
this.dispatchEvent(mx.events.PropertyChangeEvent.createUpdateEvent(this, "someText", oldValue, value));
}
}
}
package
{
[ExcludeClass]
public class _TesterWatcherSetupUtil
implements mx.binding.IWatcherSetupUtil2
{
public function _TesterWatcherSetupUtil()
{
super();
}
public static function init(fbs:IFlexModuleFactory):void
{
import Tester;
(Tester).watcherSetupUtil = new _TesterWatcherSetupUtil();
}
public function setup(target:Object,
propertyGetter:Function,
staticPropertyGetter:Function,
bindings:Array,
watchers:Array):void
{
// writeWatcher id=0 shouldWriteSelf=true
class=flex2.compiler.as3.binding.PropertyWatcher shouldWriteChildren=true
watchers[0] = new mx.binding.PropertyWatcher("someText",
{
propertyChange: true
}
,
// writeWatcherListeners id=0 size=1
[
bindings[0]
]
,
propertyGetter
);
// writeWatcherBottom id=0 shouldWriteSelf=true
class=flex2.compiler.as3.binding.PropertyWatcher
watchers[0].updateParent(target);
Rule of thumb
1. Avoid using binding to a private
variables as much as possible.
2. Do not use data binding unless
the property to which you are
binding can or will change.
Direct Assignment
In the example I showed you previously you can use direct assignment to
set the value:
<s:TextInput text="some text goes here" />
When you use direct assignment, you significantly reduce your overhead
because the compiler does not create unnecessary binding code.
Mistake #4
Forgetting to unbind and risking memory leaks
BindingUtils
You can use the <mx:Binding> tag in MXML or curly
braces to easily implement binding; however, there is
overhead associated with these methods.
Additionally, you cannot unbind properties using these
techniques. If you are optimizing a high-performance
application, you can use the BindingUtils class to bind
your objects.
There are two ways to use the BindingUtils class:
* The bindProperty() method is a static method used to bind a public
property.
* The bindSetter() method is a static method used to bind a setter
function.
bindProperty
public static function bindProperty(
site:Object, prop:String,
host:Object, chain:Object,
commitOnly:Boolean = false,
useWeakReference:Boolean = false):ChangeWatcher
site = destination
host = source
You set commitOnly to true when the handler is to be called only
on committing change events; set it to false (the default) when the
handler is to be called on both committing and non-committing
change events.
A strong reference (the default) prevents the host from being
garbage-collected; a weak reference does not.
bindProperty Example
The example includes
a text input and a
simple text
component. When
the TextInput control
is preinitialized,
preinitializeHandler()
is called, which uses
the bindProperty
method.
bindSetter
Here is the bindSetter method signature:
public static function bindSetter(setter:Function, host:Object,
chain:Object,
commitOnly:Boolean = false,
useWeakReference:Boolean = false):ChangeWatcher
The setter parameter specifies the setter method to invoke with an argument
of the current value of chain when that value changes. Here again, host
represents the source object, and chain represents the property name. The
commitOnly and useWeakReference parameters work as with bindProperty.
bindSetter Example
Behind the scenes, the
ChangeWatcher class is
used in the BindingUtils
class. It allows weak
references, so when you set
useWeakReference to true
the host will automatically
be picked up by the
garbage collector, avoiding
potential memory leaks.
Default (just like event
listeners) is false!
Avoid Memory leak
When you don’t set the weak references to true, the
ChangeWatcher object needs to be unwatched after
you are finished with it. It is your responsibility to
handle that task, which will ensure there are no
memory leaks.
The best way to handle that is to assign the static
method returned by bindProperty to a ChangeWatcher
variable (since the static method returns a
ChangeWatcher instance you can assign it to a
variable). When you do not need to bind the object
anymore you can use the unwatch method
Example
The TextInput text
property is binding to the
Label text property, and
once you type text in the
TextInput, the data will be
copied to the text property
in the Label component.
When you are ready to
unbind, click Stop Binding.
This will unwatch the
properties and set the
object to null so it will be
picked up during garbage
collection.
Mistake #5
Not changing the default
propertyChange event constant
Default type
When you use the Bindable tag without adding an event setting,
propertyChange is the default event type that will be dispatched.
[Bindable] = Bindable(event="propertyChange")
The compiler creates additional code for the setter and getter
when you do not specify the event string.
It is recommended that you add your own name constant to
avoid this extra overhead.
Compiler creates a lot of code
As you can see the mxmlc creates a generated setter which contains code that
dispatches the PropertyChangeEvent.
If you don't change the default constant, every [Bindable] property will dispatch the
propertyChange event and the listening code must interrogate the event to determine
exactly which property changed. This is especially costly process if a class has a large
number of [Bindable] properties.
Do it yourself
Once you set the event name yourself, such as in the example
below, the compiler just copies the code over.
When you change the default constant, the Flex compiler will NOT generate code
and you are responsible for dispatching the event yourself. This allows you to
optimize the listening code.
Mistake #6
Using the wrong bindable event name
What’s wrong with this code?
Using the wrong event name in the [Bindable] tag can cause your
application to not bind your property, and you will not even know
why! When you use the [Bindable] tag with a custom name, the
example below looks like a good idea:
Answer
The code above assigns a static property to the event name, and then uses the same
assignment to dispatch the event. However, when the value changes, the binding does
not appear to work. The reason is that the event name will be
EVENT_CHANGED_CONST and not the value of the variable.
Correct code:
Mistake #7
Assuming an execution order for binding
* This one is a very common one!
Common Mistake
Assuming that binding occurs in a synchronous execution
order. This can cause your application to generate
warnings and not bind your property.
Events in ActionScript are executed in an asynchronous
manner.
Example
The code above may work;
however, it may not. It
assumes that the Label text
property was already set
since the value of label.text
in the button component is
binding to the Label.text
property. If you compile this
application you'll also get a
compile time warning.
Compile time error
Another Example
Below is another example. It assumes that the value of the first
TextInput control is already set. This type of assignment also causes the
compiler to generate all the code needed for binding. You have to
decide if that code is needed or if a direct assignment (y=35) is a better
choice.
Mistake #8
Using binding in place of events
Explanation
There are many cases where you can write your code easily without data
binding and instead use events to assign a value.
You can set a value using the appropriate component lifecycle event or
overriding component’s methods such as childrenCreated() or
initializationComplete() to do the a value assignment.
Additionally, there are times when you can use ChangeWatcher to listen
to changes in data, which is less costly. When using ChangeWatcher keep
in mind that there are many cases where you can even avoid using the
ChangeWatcher since you can manually get notification.
For instance, all collections in Flex have a collectionChange event
broadcasted, which you can use to manually get change notification
within the collection object.
Example
It looks like
pretty standard
code for a Flex
application.
However, the
type of binding
illustrated here
is not needed.
Better way…
use direct assignment in an
event handler:
Mistake #9
Binding a class and its properties
Example
Another common mistake is to set a class to be bindable and then make each
property in the class bindable as well; for example:
Compile time warnings
The [Bindable] tag is not needed for the CustomerID property because the class is
already marked as bindable, which makes every property in the class bindable. This
creates compile time warnings (see Figure) and writing the extra code wastes time.
Unless you specify an Event name the tag is redundant and should be removed.
Mistake #10
Using two-way data binding for unsupported
properties
Example
Flex 4 supports two-way data binding. You can create the binding by adding @ in front
of one of the curly braces, while leaving the other field unbound. For example, if you
run the application below and type a value in one text field the value is mirrored in the
other text field:
Two-way databinding fails
Two-way data binding works in most cases; however, it does not work with
style or effect properties. It also does not work with the arguments property
for RemoteObject or the request property for HttpService, RemoteObject, or
WebService.
TurboBinding
Q&A
@EladElrom
Download