Search code examples
apache-flexbindingreadonlyarraycollectionbindable

Flex Binding : Unexpected behaviour


I have noticed an unexpected behaviour with binding in Flex, my code is as follow :

Application code

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" horizontalAlign="center" xmlns:Model="Model.*">
    <mx:Script>
        <![CDATA[
            import Model.DataDummy;
        ]]>
    </mx:Script>
    <mx:HBox width="100%" horizontalGap="30">
        <mx:Button id="buttonChange1" label="Change property value" click="myDummy._resetMyProperty();" />
        <mx:Label id="labelRaw" text="{'My Property=' + myDummy.MyProperty}" opaqueBackground="#DDDDDD" />
        <mx:Label id="labelFormatted" text="{'My Property formatted=' + myDummy.MyPropertyFormatted}" opaqueBackground="#DDDDDD" />
    </mx:HBox>

    <Model:MyDummy id="myDummy" />

    <mx:DataGrid id="dataGrid"
         width="100%" dataProvider="{DataDummy.Dummies}">
        <mx:columns>
            <mx:DataGridColumn dataField="MyProperty" headerText="My property" />
            <mx:DataGridColumn dataField="MyPropertyFormatted" headerText="My property Formatted" />
        </mx:columns>
    </mx:DataGrid>
    <mx:Button id="buttonChange2" click="{for each ( var d:MyDummy in DataDummy.Dummies ){d._resetMyProperty();}}" label="Change property value in DataGrid" />
</mx:Application>

Model.MyDummy class code

package Model
{
    import flash.events.EventDispatcher;

    import mx.formatters.NumberFormatter;
    import mx.utils.StringUtil;

    [Bindable]
    public class MyDummy extends EventDispatcher
    {
        /*** Constructor ***/
        public function MyDummy()
        {
            this._resetMyProperty();
        }

        /*** Properties ***/
        private var _myProperty:Number;
        public function get MyProperty():Number
        {
            return _myProperty;
        }
        public function set MyProperty(value:Number):void
        {
            if ( value !== _myProperty )
            {
                _myProperty = value;

                //var event:Event = new Event("ID_Changed");
                //this.dispatchEvent(event);
            }
        }

        //[Bindable (event="ID_Changed", type="flash.events.Event")]
        public function get MyPropertyFormatted():String
        {
            var idFormatted:String = "";

            if ( ! isNaN(this.MyProperty) )
            {
                var formatter:NumberFormatter = new NumberFormatter();
                formatter.precision = 2;
                idFormatted = formatter.format(this.MyProperty);
            }
            else
                idFormatted = MyProperty.toString();

            return StringUtil.substitute( "{0} (My property has been formatted)", idFormatted );
        }

        /*** Methods ***/
        public function _resetMyProperty():void
        {
            this.MyProperty = Math.round(Math.random() * 1000000000);
        }
    }
}

Model.DataDummy class code

package Model
{
    import mx.collections.ArrayCollection;

    public class DataDummy
    {
        private static var _dummies:ArrayCollection;
        public static function get Dummies():ArrayCollection
        {
            if ( _dummies == null )
            {
                _dummies = new ArrayCollection();
                _dummies.addItem(new MyDummy());
                _dummies.addItem(new MyDummy());
            }

            return _dummies;
        }
    }
}

The behaviour is as follow :

  • When I click on buttonChange1, _resetMyProperty is called on the instance myDummy.

    The result is that the label "labelRaw" has its text changed and the label "labelFormatted" does not have its text changed. This does happen because MyPropertyFormatted is a readonly property and that readonly properties are binded only at the initialisation of the application and not afterwards, according to Flex documentation. With this, I agree.


  • When I click on buttonChange2, resetMyProperty method is called on every MyDummy element of the ArrayCollection Model.DataDummy.Dummies (this static property is binded to the DataGrid).

    The result is that both columns of the DataGrid have their values changed, despite the fact that the DataGrid's second column is linked to the same readonly property MyPropertyFormatted of the MyDummy objects. I find this inconsistent with the previous behaviour I described.

My point is that :
1. On one hand, because I'm binding my controls to a single instance of an certain object, binding won't trigger on his readonly properties.
2. On the other hand, when I'm binding a control on a collection of the same certain objects, binding will trigger on every properties (readonly or not).

If I want binding to be triggered on readonly properties in point 1, I have to dispatch an event and precise on the readonly properties' MetaTag that their binding will be triggered according to this event (as show the commentaries in the code of the class Model.MyDummy class).

Why is this behaviour different ? I would like to precisely understand what an ArrayCollection instance's binding does that a single instance's binding does not.

Thank you for your help.


Solution

  • I suppose the right code is something like the following.

    First, our model.MyDummy class:

    package model
    {
    import flash.events.EventDispatcher;
    
    import mx.events.PropertyChangeEvent;
    import mx.formatters.NumberFormatter;
    import mx.utils.StringUtil;
    
    public class MyDummy extends EventDispatcher
    {
    
        //------------------------------------------------------------------------------
        //
        //   Constructor 
        //
        //------------------------------------------------------------------------------
    
        public function MyDummy()
        {
            resetMyProperty();
        }
    
        //------------------------------------------------------------------------------
        //
        //   Properties 
        //
        //------------------------------------------------------------------------------
    
        //--------------------------------------
        // myProperty 
        //--------------------------------------
    
        private var _myProperty:Number;
    
        [Bindable(event="propertyChange")]
        public function get myProperty():Number
        {
            return _myProperty;
        }
    
        public function set myProperty(value:Number):void
        {
            if (_myProperty == value)
                return;
            var oldPropertyValue:Number = _myProperty;
            var oldFormatted:String = myPropertyFormatted;
            _myProperty = value;
            dispatchEvent(PropertyChangeEvent.createUpdateEvent(this, "myProperty", oldPropertyValue, value));
            dispatchEvent(PropertyChangeEvent.
                createUpdateEvent(this, "myPropertyFormatted", oldFormatted, myPropertyFormatted));
        }
    
        [Bindable(event="propertyChange")]
        public function get myPropertyFormatted():String
        {
            var idFormatted:String = "";
    
            if (!isNaN(myProperty))
            {
                var formatter:NumberFormatter = new NumberFormatter();
                formatter.precision = 2;
                idFormatted = formatter.format(myProperty);
            }
            else
                idFormatted = myProperty.toString();
    
            return StringUtil.substitute("{0} (My property has been formatted)", idFormatted);
        }
    
        //------------------------------------------------------------------------------
        //
        //   Methods 
        //
        //------------------------------------------------------------------------------
    
        public function resetMyProperty():void
        {
            myProperty = Math.round(Math.random() * 1000000000);
        }
    }
    }
    

    We're firing propertyChange event to have possibility to fire collectionChangeEvent from our ArrayCollection (it listens propertyChange event automatically).

    Then our model.DataDummy class:

    package model
    {
    import mx.collections.ArrayCollection;
    import mx.events.CollectionEvent;
    
    public class DataDummy
    {
    
        //------------------------------------------------------------------------------
        //
        //   Constructor 
        //
        //------------------------------------------------------------------------------
    
        public function DataDummy()
        {
            dummies = new ArrayCollection();
            dummies.addItem(new MyDummy());
            dummies.addItem(new MyDummy());
        }
    
        //------------------------------------------------------------------------------
        //
        //   Variables 
        //
        //------------------------------------------------------------------------------
    
        [Bindable]
        public var dummies:ArrayCollection;
    }
    }
    

    We don't use statics to have advantage of data binding with [Bindable] metatag.

    And finally our main class with minimal changes:

    <mx:Application horizontalAlign="center" layout="vertical" xmlns:model="model.*" xmlns:mx="http://www.adobe.com/2006/mxml">
        <mx:Script>
        <![CDATA[
            //------------------------------------------------------------------------------
            //
            //   Event Handlers 
            //
            //------------------------------------------------------------------------------
    
            protected function buttonChange2_clickHandler(event:MouseEvent):void
            {
                for each (var d:MyDummy in dataProvider.dummies)
                {
                    d.resetMyProperty();
                }
            }
        ]]>
        </mx:Script>
        <mx:HBox horizontalGap="30" width="100%">
            <mx:Button click="myDummy.resetMyProperty();" id="buttonChange1" label="Change property value" />
            <mx:Label id="labelRaw" opaqueBackground="#DDDDDD" text="{'My Property=' + myDummy.myProperty}" />
            <mx:Label id="labelFormatted" opaqueBackground="#DDDDDD"
                text="{'My Property formatted=' + myDummy.myPropertyFormatted}" />
        </mx:HBox>
    
        <model:MyDummy id="myDummy" />
        <model:DataDummy id="dataProvider" />
    
        <mx:DataGrid dataProvider="{dataProvider.dummies}" id="dataGrid" width="100%">
            <mx:columns>
                <mx:DataGridColumn dataField="myProperty" headerText="My property" />
                <mx:DataGridColumn dataField="myPropertyFormatted" headerText="My property Formatted" />
            </mx:columns>
        </mx:DataGrid>
        <mx:Button click="buttonChange2_clickHandler(event)" id="buttonChange2" label="Change property value in DataGrid" />
    </mx:Application>
    

    As you can see all the bindings works as expected.

    P.S. [Bindable(event="propertyChange")] is an equivalent of simple [Bindable] but this way you can avoid compiler warnings on myPropertyFormatted getter. Actually, using simple [Bindable] form causes mxmlc compiler to generate dispatchEvent code by itself. And you can use pointing a particular event in [Bindable] tag to have more control. For example in our case we can fire event for myPropertyFormatted.

    P.P.S. I've changed your C#-like naming conventions to reflect actual ActionScript/MXML ones.