Loading...

Tuesday, November 3, 2009

Flex Tips - Using Bindable Metadata Events

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>

Labels: , ,

Sunday, November 23, 2008

Flex Basics - Data Binding

There are many reasons to choose Flex as your development platform for web-based software applications. Everyone is already familiar with Adobe's claim of a superior user experience delivered via Flex's rich client (aka fat or thick client). However, developers and project sponsors alike are drawn to features of the Flex SDK which promote rapid development of feature-rich, robust applications.

Data binding is a great example of one of these features.

Exposed through the MXML API, data bindings allow developers to "bind" the value of one object property (destination property) to the value of another object property (source property) using MXML or MXML + inline ActionScript.

<mx:Binding source="source.text" destination="destination.text" />

<mx:TextInput id="source" />
<mx:TextInput id="destination" />

OR 

<mx:TextInput id="source" />
<mx:TextInput id="destination" text="{source.text}" />
By utilizing data bindings, any change to the value of the source property is immediately reflected in the destination property. It is important to note that data bindings are "one way". Source property changes affect the destination property value but not vice versa.

One benefit of using the <mx:Binding> tag instead of inline ActionScript is the ability to bind a property to multiple source properties. If any of the source properties change so does the destination property.

<mx:Binding source="source.text" destination="destination.text"/>
<mx:Binding source="source2.text" destination="destination.text"/>

<mx:TextInput id="source"/>
<mx:TextInput id="source2"/>

<mx:TextInput id="destination"/>
However, both methods allow you to perform operations on a source property value or derive a value from the combination of multiple source properties. See Binding Expressions discussed later.

Note: a source property can only be identified for data binding if the object to which it belongs has an id property.

Although it is possible to bind the text property of one TextInput control to the text property of another TextInput control as demonstrated in the previous example, data binding is not limited to such a scenario.

Properties of visual elements may be bound to non-visual elements and vice versa. In fact, an predominant Flex architecture, MVC, relies on the ability to bind visual elements in the "view" to non-visual elements in the "model".

Also, you could bind the text property of a TextInput control to the selectedIndex property of a ComboBox control if you so choose.

Data bindings are not limited to the binding of "like" properties or "like" elements.

Note that data bindings are, however, limited to properties of identical data types or scenarios where type casting is possible. For example, you can bind a property with a data type String (i.e. text property) to a property with a data type of Int (i.e. selectedIndex) since an Int can be implicitly cast to a String. However, the reverse is not true - a property with a data type of Int cannot be bound to a property with a data type of String.

Data binding could also be accomplished by using ActionScript to add an event listener to the source object which listens for a specified event and calls a specified event handler method responsible for changing the destination object's property value to the source object's property value.

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" creationComplete="init()">

<mx:Script>
  <![CDATA[
    private function init():void {
      source.addEventListener(KeyboardEvent.KEY_UP,updateValue);
    }
  
    private function updateValue(event:KeyboardEvent):void {
      destination.text = source.text;
    }
  ]]>
</mx:Script>

<mx:TextInput id="source" />
<mx:TextInput id="destination" />
MXML data binding just allows you to do it easier and with less code.

While some may refer to such capabilities as nothing more than "syntactic sugar", most developers can appreciate the convenience and accessibility demonstrated by Flex's MXML implementation of data bindings.

Keep in mind that using MXML to bind data binds the properties to one another at compile time. If you want to bind data at runtime based on a condition, you will have to use ActionScript.

Binding Expressions

A data binding is the simplest form of a binding expression.

More advanced binding expressions combine the use of data bindings with ActionScript operations. Since binding expressions define the value of an MXML attribute, they must return a value and be wrapped in curly braces. Without curly braces, your binding expression will be interpreted as a literal value.

Binding expression are most commonly used to accomplish the following:
  1. Bind the value of one object property to the property of another object (simple data binding)
  2. String concatenation that includes one or more binded properties
  3. Calculations on one or more binded properties
  4. Conditional operations on one or more binded properties
Simple data binding:

<mx:ComboBox id="c" dataProvider="{myArray}" />

<mx:ViewStack id="v" selectedIndex="{c.selectedIndex}">
  <mx:Canvas>
    <mx:Label text="1"/>
  </mx:Canvas>
  <mx:Canvas>
    <mx:Label text="2"/>
  </mx:Canvas>
</mx:ViewStack>

OR 

<mx:ComboBox id="c" dataProvider="{myArray}" />

<mx:Binding source="c.selectedIndex" destination="v2.selectedIndex" />

<mx:ViewStack id="v2">
  <mx:Canvas>
    <mx:Label text="1"/>
  </mx:Canvas>
  <mx:Canvas>
    <mx:Label text="2"/>
  </mx:Canvas>
</mx:ViewStack>
String concatenation:

<mx:TextInput id="fname" />
<mx:TextInput id="lname" />

<mx:Label text="{'First Name: ' + fname.text}" />
<mx:Label text="{'Full Name: ' + fname.text + ' ' + lname.text}" />

OR

<mx:TextInput id="fname" />

<mx:Binding source="{'First Name: ' + fname.text}" destination="destination.text" />

<mx:Label id="destination"/>
Calculations:

<mx:NumericStepper id="quantity" />

<mx:TextInput id="price" />

<mx:Label text="{'Total: ' + quantity.value * Number(price.text)}" />

OR 

<mx:NumericStepper id="quantity" />

<mx:TextInput id="price" />

<mx:Binding source="{'Total: ' + quantity.value * Number(price.text)}" destination="destination.text" />

<mx:Label id="destination"/>
Conditional:

<mx:NumericStepper id="quantity" />

<mx:Label text="{(quantity.value % 2) ? 'Odd' : 'Even'}" />

OR 

<mx:NumericStepper id="quantity" />

<mx:Binding source="{(quantity.value % 2) ? 'Odd' : 'Even'}" destination="destination.text" />

<mx:Label id="destination"/>

Labels: , ,