Flex: Dynamic binding using a mapping file

Introduction

Binding can be an time consuming task in Flex. If you have a domain model and the DTO’s for your components, you need an easy way to bind the two together. Here you have two options. The first is to write alot of BindingUtils statements in either models. And finally you can write a mapping file in xml, create a custom binder class which uses the mapping file and bind the two models together.

I have chosen the last option, because this gives me some more flexibility handling the bindings.

Requirements

  • Flex Builder 3
  • Tutorial code (See below for the files: the xml, xsd and binder.as)

Actual coding

To make this idear happen, I needed several things. First, I needed my two models (the real domain model and the DTO for the view components) which needed to be bound together. Prerequisite for these models is that they have a single point of entry to be able to naviagate to all objects within the model.
Secondly, a mapping file. I have created a easy to use mapping file in which I configure the binding rules.
And finally, the class that does the actual binding based upon the mapping file and both models.

Code samples

The is not much more to say about this, so here I will show you samples of all code.

Binding file

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<mapping xmlns="http://www.example.org/binding" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.example.org/binding assets/binding.xsd ">

  <binding id="myList" bidirectional="true">
    <sourceBinding topLevelObject="myDomainRoot" object="myDomainRoot.subDomainObj" field="myList" isCollection="true"/>
    <targets>
      <targetBinding topLevelObject="myDTORoot" object="customer.address" field="myList" />
      <targetBinding topLevelObject="myDTORoot" object="company" field="anotherList" />
    </targets>
  </binding>

</mapping>

XSD file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?xml version="1.0" encoding="UTF-8"?>
<schema id="mapping" xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.example.org/binding" xmlns:tns="http://www.example.org/binding" elementFormDefault="qualified">
  <element name="mapping">
    <complexType>
      <choice minOccurs="1" maxOccurs="unbounded">
        <element name="binding">
          <complexType>
            <sequence>
              <element name="sourceBinding" minOccurs="1" maxOccurs="1">
                <complexType>
                  <attribute name="topLevelObject" type="string" />
                  <attribute name="object" type="string" />
                  <attribute name="field" type="string" />
                  <attribute name="isCollection" type="string" />
                </complexType>
              </element>
              <element name="targets" minOccurs="1" maxOccurs="1">
                <complexType>
                  <sequence>
                    <element name="targetBinding" minOccurs="1" maxOccurs="unbounded">
                      <complexType>
                        <attribute name="topLevelObject" type="string" />
                        <attribute name="object" type="string" />
                        <attribute name="field" type="string" />
                      </complexType>
                    </element>
                  </sequence>
                </complexType>
              </element>
            </sequence>
            <attribute name="id" type="string" />
            <attribute name="bidirectional" type="string" />
          </complexType>
        </element>
      </choice>
    </complexType>
  </element>
</schema>

Mapping explenation
A binding element represents a complete bi-rectional binding
A sourceBinding element represents the source end object and property of the required binding. TopLevelObject indicates the root of the model. The object (for example rootObject.subObject) represents the navigation to the object that needs to facilitate the binding. If the topLevelObject and the first object in the property Object are the same, this is ignored in the navigation. It just means that the model has no nested objects, but only properties.
A targetBinding element represents the target to be bound. Same rules as the source binding apply.

Binding class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import flash.utils.ByteArray;

    import mx.binding.utils.BindingUtils;
    import mx.logging.ILogger;
    import mx.logging.Log;
    import mx.logging.LogEventLevel;
    import mx.logging.targets.TraceTarget;

    /**
     * This class provides the binding capability between
     * 2 models. The binding is read from a XML file and
     * used in the binding process.
     *
     * @author http://nl.linkedin.com/in/marcdekwant
     */

    public class Binder {

        [Embed( source='/assets/bindings.xml', mimeType="application/octet-stream" )]
        private var _rawFile:Class

        // The from file read xml representation of the bindingfile
        private var _bindings:XML;
        // The original domain model
        private var _model:*;
        // The target model (DTO's related to the UI components)
        private var _dto:*;

        private var logger:ILogger  =  Log.getLogger( "Binder" );

        /**
         * Constructor
         *
         * @param dto the target domain model
         * @param model the source domain model
         */

        public function Binder( dto:*, model:* ) {
            this._dto  =  dto;
            this._model  =  model;
            var file:ByteArray  =  new _rawFile();
            var str:String  =  file.readUTFBytes( file.length );
            _bindings  =  new XML( str );
            initLogging();
            createBindings();
        }

        /**
         * Initialize the logging
         * TODO refactor to controller or application level
         */

        private function initLogging():void {
            var logTarget:TraceTarget  =  new TraceTarget();
            logTarget.level  =  LogEventLevel.ALL;
            logTarget.filters  =  [ "*", "nl.dialoog.binding.*" ];
            logTarget.includeCategory  =  true;
            logTarget.includeDate  =  true;
            logTarget.includeLevel  =  true;
            logTarget.includeTime  =  true;
            Log.addTarget( logTarget );
        }

        /**
         * This method retrieves a property from a
         * given object. THis method gives the binding
         * class the capability to chain objects.
         *
         * @param _objTarget The object
         * @param _innerObj the property of the targetObject
         * @return the innerObj from the targetobj
         */

        private function resolveNestedObject( _objTarget:*, _innerObj:String ):* {
            return _objTarget[ _innerObj ];
        }

        /**
         * This method resolves the property from an object chain to
         * be bound to another property from an object chain.
         *
         * @param _obj The baseObject (domain model root, or DTO root)
         * @param _xmlObj the target to be found in the root
         * @return the target object to be bound.
         */

        private function resolveSourceObject( _obj:*, _xmlObj:XML ):* {
            var _objArray:Array  =  _xmlObj.@object.split( "." );
            for ( var i:int  =  0; i < _objArray.length; i++ ) {
                if ( _xmlObj.@topLevelObject != _xmlObj.@object ) {
                    _obj  =  resolveNestedObject( _obj, _objArray[ i ]);
                }
            }
            return _obj;
        }

        /**
         *  Actual method that hold the logic in creating bindings
         *  based upon the binding xml file.
         */

        private function createBindings():void {
            logger.debug( "Start create bindings..." );
            var _objSource:*  =  null;
            var _objTarget:*  =  null;
           
            // Set the default namespace from the binding.xml, in order to process the
            // mapping file. "" relates to the xmlns="" attribute in the root element
            default xml namespace = _bindings.namespace("");
           
            // traverse over all <binding> elements
            for each ( var xmlBinding:XML in _bindings.binding ) {
                logger.debug( "Create binding for " + xmlBinding.@id + " bi-directional:" + xmlBinding.@bidirectional );
                var sourceBinding:XML  =  xmlBinding.sourceBinding[ 0 ];
                _objSource  =  resolveSourceObject( this._model, sourceBinding );
                logger.debug( "Collection binding..." );
                // traverse over all <targetBinding> elements
                for each ( var xmlTargetBinding:XML in xmlBinding.targets.targetBinding ) {
                    logger.debug( "Construction of bindings from " + sourceBinding.@object + "." + sourceBinding.@field + " to " + xmlTargetBinding.@object + "." + xmlTargetBinding.@field );
                    _objTarget  =  resolveSourceObject( this._dto, xmlTargetBinding );
                    // A collection requires a slightly different binding then properties
                    // because the ArrayCollection contains binding mechanism in its own class.
                    if ( sourceBinding.@isCollection == "true" ) {
                        logger.debug( "Collection binding..." );
                        _objTarget[ xmlTargetBinding.@field ][ "source" ]  =  _objSource[ sourceBinding.@field ][ "source" ];
                    } else {
                        logger.debug( "Property binding..." );
                        var _sourceStr:String  =  sourceBinding.@field;
                        var _targetStr:String  =  xmlTargetBinding.@field;
                        BindingUtils.bindProperty( _objSource, _sourceStr, _objTarget, _targetStr );
                        BindingUtils.bindProperty( _objTarget, _targetStr, _objSource, _sourceStr );
                    }
                }
            }
            logger.debug("Bindings created...");
        }

    }
}

Sample Application file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"
  creationComplete="onCreationComplete();">

     <mx:Script>
      <![CDATA[
        import mx.binding.utils.BindingUtils;
     
         [Bindable]
         private var myDomainRoot:MyDomainRoot;
         [Bindable]
           private var myDTORoot:MyDTORoot;          
       
        private function onCreationComplete():void {
          myDomainRoot= new MyDomainRoot();
          myDTORoot = new MyDTORoot();  
          var _binder:Binder = new Binder(myDTORoot,myDomainRoot);
        }
       
      ]]>
     </mx:Script>
     
</mx:Application>

Kind regards,

Marc de Kwant

Tags: , , , , , ,

Leave a Reply