Pages

November 3, 2009

Bindable Metadata Events

Posted by Jeremy Mitchell
One powerful feature of Flex is data binding. On the surface, data binding is simple to implement.

1. Mark a class as bindable to activate data binding on all public properties of the class

[Bindable]
public class Order
Or, mark individual properties as bindable

public class Order
{
    public function Order()
    {
    }
   
    [Bindable]
    public var orderNumber:int;
   
    [Bindable]
    public var orderName:String;
}
2. Declare an instance of the class preceded with the [Bindable] metadata tag

[Bindable]
private var order:Order = new Order();
3. Use a bindable property as the source of a data binding association. 3 techniques exist:
  • Wrap the source property in curly brackets

    <mx:Label id="myLabel" text="{order.orderName}"/>
  • Use a Binding tag

    <mx:Binding source="order.orderName" destination="myLabel.text"/>
  • Create the data binding association at runtime

    BindingUtils.bindProperty(myLabel, "text", order, "orderName");
The most common practice includes the use of curly brackets. The following example demonstrates data binding in its simplest form.

// Value object class marked bindable
    
[Bindable]
public class Order
{
    public function Order()
    {
    }
   
    public var orderNumber:int;
    public var orderName:String;
}
<!-- Application file -->
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" 
        layout="vertical">
    
    <mx:Script>
        <![CDATA[
            import comps.Order;
            
            [Bindable]
            private var order:Order = new Order();
        ]]>
    </mx:Script>
    
    <!-- binding #-->
    <mx:Label  text="{'Order #: ' + order.orderNumber}"/> 
    
    <!-- binding #-->
    <mx:Text text="{'Order Name: ' + order.orderName}"/>
    
    <mx:HBox>
    
        <mx:Button label="Change Order #" click="order.orderNumber = Math.floor(Math.random()*50)+1"/>
        <mx:TextInput id="ti" text="Enter name..." enter="order.orderName = ti.text" />
        
    </mx:HBox>
    
</mx:Application>
In most scenarios, this approach is acceptable. However, there are times when another technique is recommended. It involves explicitly defining which event type to dispatch when a source property changes. Let's review what we did previously.

We marked an entire class as bindable

[Bindable]
public class Order
Or, marked individual properties as bindable

public class Order
{
    public function Order()
    {
    }
   
    [Bindable]
    public var orderNumber:int;
   
    [Bindable]
    public var orderName:String;
}
This is equivalent to:

[Bindable(event="propertyChange")]
public class Order
Or

public class Order
{
    public function Order()
    {
    }
   
    [Bindable(event="propertyChange")]
    public var orderNumber:int;
   
    [Bindable(event="propertyChange")]
    public var orderName:String;
}
Data binding includes a lot of code generation. This is part of it. When using the [Bindable] metadata tag without explicitly defining the event type, the default event type is used – propertyChange.

Using the default event type (propertyChange) is fine except when you mark the entire class as bindable and the class contains several public properties (see Cairngorm's ModelLocator class). In this case, each public property will dispatch the same event type (propertyChange) when their value changes. This can be problematic.

To understand why this is a problem, let's take a closer look at what happens behind the scenes when data binding is used.

Note: This is a very high-level analysis of the inner-workings of data binding. For a detailed description of data binding, check out Michael Labriola's presentation entitled Diving in the Data Binding Waters
To accomplish data binding, here's what is required of you:

First, mark a class as bindable to activate data binding on all public properties of the class:

[Bindable]
public class Order
Or, mark individual properties as bindable

public class Order
{
    public function Order()
    {
    }
   
    [Bindable]
    public var orderNumber:int;
   
    [Bindable]
    public var orderName:String;
}
By doing this, the Flex framework does the following by generating more code:
  • Turns your class into an "event dispatcher" by implementing IEventDispatcher
  • Implements the methods required of the IEventDispatcher interface (addEventListener, removeEventListener, dispatchEvent, etc)
  • Creates implicit getters and setters for each bindable property
  • Converts [Bindable] to [Bindable(event="propertyChange")]
  • Adds code to your setter method required to dispatch the propertyChange event when the property value changes.
Why does the Flex framework do all of this? In a nutshell, so your class can:
  1. Broadcast an event when a bindable property value changes
  2. Allow interested parties to register an event listener triggered when a bindable property changes
But wait, there's more! When you actually use a bindable property as the source of a data binding (via curly brackets, Binding object or BindingUtils.bindProperty), the Flex Framework generates even more code required to:
  1. Create an array of Binding objects

    One Binding object is created for each data binding association. Each Binding object is responsible for updating a destination property when the corresponding source property changes.
  2. Create an array of PropertyWatcher objects

    One PropertyWatcher object is created for each data binding association. Each PropertyWatcher object is responsible for "listening" for an event to occur that signifies a change in the source property value. The event type is determined by the value defined in the [Bindable] metadata tag. Again, if no type is defined, propertyChange is used.
Failure to specify an event type in the [Bindable] metadata tag forces every public property to dispatch a propertyChange event when their value changes, and, in turn, every PropertyWatcher is "listening" for a propertyChange event. However, only the PropertyWatcher tasked with watching the property that actually changed is required to respond to the event and notify the Binding object responsible for updating the destination property.

The extra work involved with triggering multiple PropertyWatcher objects when only ONE source property changes can be expensive and can be avoided by simply defining a unique event for each bindable property. The following example demonstrates this technique.

// Value object that dispatches unique events for each bindable property

package comps
{
    import flash.events.Event;
    import flash.events.EventDispatcher;
    
    public class Order extends EventDispatcher
    {
        public function Order()
        {
        }
       
        private var _orderNumber:int;
       
        [Bindable(event="orderNumberChange")]
        public function get orderNumber():int {
            return _orderNumber;
        }
       
        public function set orderNumber(value:int):void {
            if _orderNumber !== value ) {
                _orderNumber = value;
                // remember to dispatch the unique event
                dispatchEvent(new Event("orderNumberChange"))
            }
        }
       
        private var _orderName:String;
        
        [Bindable(event="orderNameChange")]
        public function get orderName():String {
            return _orderName;
        }
       
        public function set orderName(value:String):void {
            if _orderName !== value ) {
                _orderName = value;
                // remember to dispatch the unique event
                dispatchEvent(new Event("orderNameChange"))
            }
        }

    }
}
<?xml version="1.0" encoding="utf-8"?>
<mx:Application
    xmlns:mx="http://www.adobe.com/2006/mxml"
    layout="vertical">
   
    <mx:Script>
        <![CDATA[
            import comps.Order;
           
            [Bindable]
            private var order:Order = new Order();
        ]]>
    </mx:Script>
   
    <!-- binding #-->
    <mx:Label id="myLabel" text="{'Order #: ' + order.orderNumber}"/>
   
    <!-- binding #-->
    <mx:Text text="{'Order Name: ' + order.orderName}"/>
   
    <mx:HBox>
   
        <mx:Button label="Change Order #" click="order.orderNumber = Math.floor(Math.random()*50)+1"/>
        <mx:TextInput id="ti" text="Enter name..." enter="order.orderName = ti.text" />
       
    </mx:HBox>
   
</mx:Application>