Home  >  

Building an Enterprise Framework - Enterprise Development with Flex, Part 2

Author photo
AddThis Social Bookmark Button

Note: This is excerpted from Chapter 3 of the Rough Cuts version of Enterprise Development with Flex. This book is still in progress, and you can get access to it now.

Rough Cuts is a service from Safari Books Online that gives you early access to content on cutting-edge technologies -- before it's published. It lets you literally read the book as it is being written.

Check out Part 1 of this chapter.

Table of Contents, Part 2

The Base Class for Resources

DataGrid with Resources

Data Forms

The DataForm Component

The DataFormItem Component

The Base Class for Resources

Example 3.11, “Class ResourceBase” depicts the class ResourceBase, which serves as a base class for all resources for all components. This class can tell properties from styles. In Chapter 2, Selected Design Patterns you learned about a class factory that accepts a class or a function name to create instances of objects. We applied that same technique here: With ResourceBase, resource instance can be created from a class factory or a class.

Technically, the ResourceBase class applies specified values as either properties or resources.

Example 3.11. Class ResourceBase

package com.farata.resources {
    import com.farata.controls.TextInput;

    import flash.system.ApplicationDomain;

    import mx.core.ClassFactory;
    import mx.core.UIComponent;
    import mx.utils.StringUtil;

    public dynamic class ResourceBase {
          public var resourceProps:Array = [];
          public var resourceStyles:Array = [];

    public function load(source:Object):void {
          for each(var propName:String in resourceProps) {
               try   {
                     if( source[propName])
                          this[propName]= source[propName] ;
               }
               catch (e:Error) {}
          }
          for each(var styleName:String in resourceStyles){
               try   {
                     if(source.getStyle(styleName))
                          this[styleName] = source.getStyle(styleName);
               }
               catch (e:Error){}
          }
    }

    public function apply(target:Object):void               {
          try {
               for each(var propName:String in resourceProps)
                     if (this[propName]!=undefined)
                          target[propName] = this[propName];
          } catch (e:Error) {
               var error:String = mx.utils.StringUtil.substitute(
            "Incompatible resource class. Can not apply
             property {0} of {1} to {2}",
              [propName,this.toString(), target.toString()] );
             throw new Error(error);
          }
          try {

               for each(var styleName:String in resourceStyles)
                     if(this[styleName])
                          target.setStyle(styleName, this[styleName]);
    }

    public static function getResourceInstance(value:Object,
                                styleOwner:Object=null):*   {
          var resClass:Object;
          if(value is Class) {
               resClass = Class(value);
               if (styleOwner) {
                     try  {
                          var result:* = new resClass(styleOwner);
                          return result;
                     }
                     catch (e:Error) {
                          return new resClass();
                     }
               }
               else
                     return new resClass();
          }
          else if(value is ResourceBase)
               return value;
          else if(value is ClassFactory)
               return ClassFactory(value).newInstance();
          else  if (value != null)   {
               var v:String = String(value).replace(/{/,"");
               v = v.replace(/}/,"");
               resClass = ApplicationDomain.currentDomain.getDefinition(v);
               if (styleOwner) {
                     try  {
                          var result2:* = new resClass(styleOwner);
                          return result2;
                     }
                     catch (e:Error) {
                          return new resClass();
                     }
               }
               else
                     return new resClass();
          }
    }
    public function get itemEditor() : UIComponent {
          return new TextInput();
    }
}
}

When application programmers design a resource for a particular type of Flex UI control, they simply extend it from a ResourceBase class (or build a MXML component based on it) and specify the names of the variables and their default values, if need be.

The ResourceBase class relies on two arrays: resourceProps and resourceStyles. When application developers create concrete resources, they also must populate these arrays. Example 3.12, “Sample ComboBoxResource” illustrates implementation of a sample class called ComboBoxResource. Note how the array resourceProps is populated with the data in the constructor.

Example 3.12. Sample ComboBoxResource

package com.farata.resources {
    import mx.core.IFactory;
    import mx.core.UIComponent;
    import mx.styles.CSSStyleDeclaration;
    import mx.styles.StyleManager;
    import com.farata.controls.ComboBox;

    dynamic public class ComboBoxResource extends ResourceBase {
          public var autoFill :Boolean = false;
          public var keyField : String = "data";
          public var destination:String=null;
          public var dropdownWidth : int = 0;
          public var editable:Boolean = false;
          public var itemRenderer:IFactory = null;
          public var labelFunction : Function = null;
          public var labelField : String = "label";
          public var dataField : String = "label";
          public var method : String = null;
          public var width:int=−1;
          public var dataProvider : Object;

          public function ComboBoxResource(styleOwner:Object=null) {
               resourceProps.push("autoFill", "keyField", "destination",
              "dropdownWidth", "editable","itemRenderer", "labelField",
             "labelFunction","method", "dataProvider", "width");

               var sd:CSSStyleDeclaration =
                  StyleManager.getStyleDeclaration(".comboBoxResource");
               if (!sd)        {
                     sd = new CSSStyleDeclaration();
                     StyleManager.setStyleDeclaration(".comboBoxResource",
                   sd, false);
                     sd.setStyle("paddingBottom", 0);
                     sd.setStyle("paddingTop", 0);
               }
               if ( styleOwner!= null )
                     load( styleOwner );
          }
          override public function get itemEditor() :UIComponent {
                     return new ComboBox();
          }
    }
}

This class has to be written once for your enterprise framework, and after that any junior programmer can easily create and update resources such as DepartmentComboResource or StateComboResource shown earlier in this chapter in Example 3.9, “StateComboBoxResource with hard-coded states” and Example 3.10, “Sample DepartmentComboResource configured for a remote destination”.

Similarly to CSS, resources should be compiled into a separate SWF file. They can be loaded and reloaded during the runtime, and you can find out more about class loaders in Chapter 7.

DataGrid with Resources

The most interesting part about these resources is that you can attach them not only to regular, but also to such dynamic controls as DataGridColumn. For example, the next code snippet instructs the DataGridColumn (it was also enhanced and is available in clear.swc) to turn itself into a ComboBox and populate itself based on the configured resource DepartmentComboResource shown in Example 3.9, “StateComboBoxResource with hard-coded states”.

<fx:DataGridColumn dataField="DEPT_ID" editable="false"
headerText="Department"
resource="{com.farata.resources.DepartmentComboResource}"/>

A resource attached to a DataGridColumn not only sets a column's properties but also identifies the item renderer and editor for this column.

As discussed in Chapter 2, Selected Design Patterns, class factories become extremely powerful if you use them as item renderer for a data grid column. Using this methodology, you can also encapsulate a number of properties and styles in the object provided by the factory. For example, you can enable the support of resources on the enhanced DataGridColumn object by adding the code fragment in Example 3.13, “Enabling resources support in DataGridColumn”:

Example 3.13. Enabling resources support in DataGridColumn

private var _resource:Object;
        public function set resource(value:Object):void{
          _resource = ResourceBase.getResourceInstance(value, this);
               if(labelFunction==null) {
                     getLabelFunctionByResource(_resource, this);
               }
        }

        public function get resource():Object{
          return _resource;
        }
        public static function getLabelFunctionByResource(resourceRef:Object,
                                                      column:Object):void {
               var resource:ResourceBase = resourceRef as ResourceBase;
               if (resource) {
                     if(resource.hasOwnProperty("destination") &&
                                                 resource["destination"])
                          CollectionUtils.getCollection(
                                function(ev:Event, collection:Object):void {
                                     collectionLoaded(collection, column);
                                },
                                resource.destination,
                                resource.method
                          );
                     else if (resource.hasOwnProperty("dataProvider") &&
                                              resource["dataProvider"]) {
                          collectionLoaded(
                                resource.dataProvider,
                                column,
                                safeGetProperty(resource, "labelField", "label"),
                                safeGetProperty(resource, "keyField", "data")
                          );
                     }
               }
          }
          private static function collectionLoaded(collection:Object, column:Object,
                          labelField:String = null, dataField:String = null):
void {
               if (null == collection) return;
               labelField =
                     labelField ?
                          labelField :
                          (column["labelField"] != null ?
                                column.labelField :
                                (column.resource.labelField ?
                                     column.resource.labelField : "label"));

               if (!dataField)
                     dataField = column.resource.keyField ?
                                     column.resource.keyField : column.dataField;

               collection = CollectionUtils.toCollection(collection);

               const options:Dictionary = new Dictionary();

               // copy only when collection is non empty
               if (collection != null && collection.length > 0 ) {
                     const cursor:IViewCursor = collection.createCursor();
                     do {
                          options[cursor.current[dataField]] =
                                                   cursor.current[labelField];
                     } while(cursor.moveNext())
               }

               column.labelFunction = function(data:Object, col:Object):String {
                     var key:* = data is String || data is Number ? data :
                                                            data[col.dataField];
                     var res:String = options[key];
                     return res != null ? res : '' + key;
               };
          }

Suppose you have a DataGrid and a ComboBox with the values 1, 2, and 3 that should be displayed as John, Paul, and Mary. These values are asynchronously retrieved from a remote DBMS. You can't be sure, however, if John, Paul, and Mary will arrive before or after the DataGrid getst populated. The code above extends the DataGridColumn with the property resource and checks if the application developer supplied a labelFunction. If not, the code tries to "figure out" the labelFunction from the resource itself.

If resource has the destination set and the method is defined as in DepartmentComboResource in Example 3.9, “StateComboBoxResource with hard-coded states”, the code loads the Collection and after that, it creates the labelFunction (see collectionLoaded() method) based on the loaded data.

The resource may either come with a populated dataProvider as in Example 3.8, “A CheckBox resource”, or the data for the dataProvider may be loaded from the server. When the dataProvider is populated, the collectionLoaded() method examines the dataProvider's data and creates the labelFunction. The following code attaches a labelFunction on the fly as a dynamic function that gets the data and by the key finds the text to display on the grid.

column.labelFunction = function(data:Object, col:Object):String {
          var key:* = data is String || data is Number ? data :
                                               data[col.dataField];
                         var res:String = options[key];
          return res != null ? res : '' + key;
     };

This closure uses the dictionary options defined outside. The code above this closure traverses the data provider and creates the following entries in the dictionary:

1, John

2, Paul

3, Mary

Hence the value of the res returned by this label function will be John, Paul, or Mary.

These few lines of code provide a generic solution for the real-life situations that benefit from having asynchronously loaded code tables that can be programmed by junior developers. This code works the same way translating the data value into John and Mary, Alaska and Pennsylvania, or department names.

Note

With resources, the properties and styles of UI controls become available not only to developers who write these classes, but to outsiders in a similar to CSS fashion. The examples of resources from the previous section clearly show that they are self-contained easy to understand artifacts that can be used by anyone as a business style sheets.

You can create a resource as a collection of styles, properties, event listeners that also allow providing a class name to be used with it. You can also create a class factory that will be producing instances of such resources.

Technically, any resource is an abstract class factory that can play the same role as XML-based configurable properties play in the Java EE world. But this solution requires compilation and linkage of all resources, which makes it closer to configuring Java objects using annotations. Just to remind you, in Flex, CSS also get compiled.

To summarize, resources offer the following advantages:

  • They are compiled and work fast.

  • Because they are simple to understand, junior programmers can work with them.

  • You can inherit one resource from another, and Flex Builder will offer you context-sensitive help and Flex compiler will help you to identify data type errors.

  • You can attach resources to a DataGridColumn and use them as a replacement of item renderers.

Resources are a good start for automation of programming. In Chapter 6, you'll get familiar with yet another useful Flex component called DataCollection, a hybrid of ArrayCollection and RemoteObject, which yet another step toward reducing manual programming.

Data Forms

In this section you'll continue adding components to the enterprise framework. It's hard to find an enterprise application that does not use forms, which makes the Flex Form component a perfect candidate for possible enhancements. Each form has some underlying model object, and the form elements are bound to the data fields in the model. Flex 3 supports only one-way data binding: Changes on a form automatically propagate to the fields in the data model. But if you wanted to update the form when the data model changes, you had to manually program it using the curly braces syntax in one direction and BindingUtils.bindProperty() in another.

Flex 4 introduces new feature: two-way binding. Add an @ sign to the binding expression @{expression} and notifications about data modifications are sent in both directionsfrom form to the model and back. Although this helps in basic cases where a text field on the form is bound to a text property in a model object, two-way binding has not much use if you'd like to use data types other than String.

For example, two-way binding won't help that much in forms that use standard Flex <mx:CheckBox> component. What are you going to bind here? The server side application has to receive 1 if the CheckBox was selected and 0 if not. You can't just bind its property selected to a numeric data property on the underlying object. To really appreciate two-way binding, you need to use different set of components, similar to the ones that you have been building in this chapter.

Binding does not work in cases when the model is a moving target. Consider a typical master-detail scenario: The user double-clicks on a row in a DataGrid and details about selected row are displayed in a form. Back in Chapter 1, Comparing Selected Flex Frameworks you saw an example of this: Doubling-clicking a grid row in Figure 1.19, “Removing the filter to keep generated Java code” opened up a form that displayed details of the employee selected in a grid. This magic was done with the enhanced form component that you are about to review.

The scenario with binding a form to a datagrid row has to deal with a moving model; the user selects another row. Now what? The binding source is different now and you need to think of another way of refreshing the form data.

When you define data binding using an elegant and simple notation with curly braces, the compiler generates additional code to support it. But in the end, an implementation of the Observer design pattern is needed, and "someone" has to write the code to dispatch events to notify registered dependents when the property in the object changes. In Java, this someone is a programmer, in Flex it's the compiler that also registered event listeners with the model.

Flex offers the Form class that an application programmer bind to an object representing the data model. The user changes the data in the UI form, and the model gets changed too. But the original Form implementation does not have means of tracking the data changes.

It would be nice if the Form control (bound to its model of type a DataCollection) could support similar functionality, with automatic tracking of all changes compatible with ChangeObject class that is implemented with remote data service. Implementing such functionality is the first of the enhancements you'll make.

The second improvement belongs to the domain of data validation. The enhanced data form should be smart enough to be able to validate not just individual form items, but the form in its entirety too. The data form should offer an API for storing and accessing its validators inside the form rather than in an external global object. This way the form becomes a self-contained black box that has everything it needs. (For details of what can be improved in the validation process, see the section called “Validation”.)

During the initial interviewing of business users, software developers should be able to quickly create layouts to demonstrate and approve the raw functionality without waiting for designers to come up with the proper pixel-perfect controls and layouts.

Hence, your third target will be making the prototyping of the views developer friendly. Beside the need to have uniform controls, software developers working on prototypes would appreciate if they should not be required to give definitive answers as to which control to put on the data form. The first cut of the form may use a TextInput control, but the next version should use a ComboBox instead. You want to come up with some UI-neutral creature (call it a data form item) that will allow not be specific, like, "I'm a TextInput", or "I'm a ComboBox". Instead, developers will be able to create prototypes with generic data items with easily attachable resources.

The DataForm Component

The solution that addresses your three improvements is a new component called DataForm (Example 3.14, “Class DataForm”). It's a subclass of a Flex Form, and its code implements two-way binding and includes a new property dataProvider. Its function validateAll()supports data validation explained in the next sections. This DataForm component will properly respond to data changes propagating them to its data provider.

Example 3.14. Class DataForm

package com.farata.controls{
import com.farata.controls.dataFormClasses.DataFormItem;

import flash.events.Event;

import mx.collections.ArrayCollection;
import mx.collections.ICollectionView;
import mx.collections.XMLListCollection;
import mx.containers.Form;
import mx.core.Container;
import mx.core.mx_internal;
import mx.events.CollectionEvent;
import mx.events.FlexEvent;
import mx.events.ValidationResultEvent;

public dynamic class DataForm extends Form{
    use namespace mx_internal;
    private var _initialized:Boolean = false;
    private var _readOnly:Boolean = false;
    private var _readOnlySet:Boolean = false;

    public function DataForm(){
          super();
          addEventListener(FlexEvent.CREATION_COMPLETE, creationCompleteHandler);
    }

    private var collection:ICollectionView;
    public function get validators() :Array {
          var _validators :Array = [];
          for each(var item:DataFormItem in items)
               for (var i:int=0; i < item.validators.length;i++)    {
                     _validators.push(item.validators[i]);
               }
          return _validators;
    }
    public function validateAll(suppressEvents:Boolean=false):Array {
          var _validators :Array = validators;
          var data:Object = collection[0];
          var result:Array = [];
          for (var i:int=0; i < _validators.length;i++) {
               if ( _validators[i].enabled ) {
                     var v : * = _validators[i].validate(data, suppressEvents);
                     if ( v.type != ValidationResultEvent.VALID)
                          result.push( v );
               }
          }
          return result;
    }
[Bindable("collectionChange")]
    [Inspectable(category="Data", defaultValue="undefined")]

    /**
     *  The dataProvider property sets of data to be displayed in the form.
     *  This property lets you use most types of objects as data providers.
     */
    public function get dataProvider():Object{
          return collection;
    }

    public function set dataProvider(value:Object):void{
          if (collection){
               collection.removeEventListener(CollectionEvent.COLLECTION_CHANGE,
                                                       collectionChangeHandler);
          }

          if (value is Array){
               collection = new  ArrayCollection(value as Array);
          }
          else if (value is ICollectionView){
               collection = ICollectionView(value);
          }
          else if (value is XML){
               var xl:XMLList = new XMLList();
               xl += value;
               collection = new XMLListCollection(xl);
          }
          else{
               // convert it to an array containing this one item
               var tmp:Array = [];
               if (value != null)
               tmp.push(value);
               collection = new ArrayCollection(tmp);
          }

          collection.addEventListener(CollectionEvent.COLLECTION_CHANGE,
                                               collectionChangeHandler);
          if(initialized)
               distributeData();
    }

    public function set readOnly(f:Boolean):void{
          if( _readOnly==f ) return;
          _readOnly = f;
          _readOnlySet = true;
          commitReadOnly();
    }

    public function get readOnly():Boolean{
          return _readOnly;
    }

     /**
     *  This function handles CollectionEvents dispatched from the data provider
     *  as the data changes.
     *  Updates the renderers, selected indices and scrollbars as needed.
     *
     *  @param event The CollectionEvent.
     */
    protected function collectionChangeHandler(event:Event):void{
          distributeData();
    }

    private function commitReadOnly():void{
          if( !_readOnlySet ) return;
          if( !_initialized ) return;
          _readOnlySet = false;
          for each(var item:DataFormItem in items)
               item.readOnly = _readOnly;
    }

    private function distributeData():void {
          if((collection != null) && (collection.length > 0)) {
               for (var i:int=0; i<items.length; i++)       {
                     DataFormItem(items[i]).data = this.collection[0];
               }
          }
    }

    private var items:Array = new Array();
    private function creationCompleteHandler(evt:Event):void{
          distributeData();
          commitReadOnly();
    }

    override protected function createChildren():void{
          super.createChildren();
          enumerateChildren(this);
          _initialized = true;
          commitReadOnly();
    }
    private function enumerateChildren(parent:Object):void{
          if(parent is DataFormItem){
               items.push(parent);
          }
          if(parent is Container){
               var children:Array = parent.getChildren();
               for(var i:int = 0; i < children.length; i++){
                     enumerateChildren(children[i]);
               }
          }
    }
 }
}

Let's walk through the code of the class DataForm. Examine the setter dataProvider in the code above. It always wraps up the provided data into a collection. This is needed to ensure that the DataForm supports working with remote data services the same way as DataGrid does. It checks the data type of the value. It wraps an Array into an ArrayCollection, and XML turns into XMLListCollection. If you need to change the backing collection that stores the data of a form, just point the collection variable at the new data.

If a single object is given as a dataProvider, turn it into a one-element array and then into a collection object. A good example of such case is an instance of a Model, which is an ObjectProxy (see Chapter 2, Selected Design Patterns) that knows how to dispatch events about changes of its properties.

Once in a while, application developers need to render non-editable forms, hence the DataForm class defines the readOnly property.

The changes of the underlying data are propagated to the form in the method collectionChangeHandler(). The data can be modified either in the dataProvider on from the UI and the DataForm ensures that each visible DataFormItem object (items[i]) knows about it. This is done in the function distributeData():

private function distributeData():void {
          if((collection != null) && (collection.length > 0)) {
               for (var i:int=0; i<items.length; i++)       {
                     DataFormItem(items[i]).data = this.collection[0];
               }
          }
    }

This code always works with the element zero of the collection, because the form has always one object with data that is bound to the form. Such a design resembles the functionality of the data variable of the Flex DataGrid that for each column provides a reference to the object that represents the entire row.

Again, we need the data to be wrapped into a collection to support DataCollection or DataService from LCDS.

Technically, a DataForm class is a VBox that lays out its children vertically in two columns and automatically aligns the labels of the form items. This DataForm needs to allow nesting – containing items that are also instances of the DataForm object. A recursive function enumerateChildren() loops through the children of the form, and if it finds a DataFormItem, it just adds it to the array items. But if the child is a container, the function loops through its children and adds them to the same items array. In the end, the property items contains all DataFormItems that have to be populated.

Notice that the function validateAll()is encapsulated inside the DataForm, while in Flex framework is located in the class Validator. There, the validation functionality was external to Form elements and you'd need to give an array of validators that were tightly coupled with specific form fields.

Our DataForm component is self sufficient – its validators are embedded inside and reusing the same form in different views or applications is easier comparing to original Flex Form object that relies on external validators.

The DataFormItem Component

The DataFormItem, an extension of Flex FormItem, is the next component of the framework. This component should be a bit more humble than its ancestor though. The DataFormItem should not know too much about its representation and should be able to render any UI component. Design of new Flex 4 components has also been shifted toward separation between their UI and functionality.

At least a half of the controls on a typical form are text fields. Some of them use masks to enter formatted values like phone numbers. The rest of the form items most likely are nothing else but check boxes, and radio buttons. For these controls (and whatever else you may need) just use resources. Forms also use combo boxes. The "the section called “DataGrid with Resources”" section showned you how class factory based resources can be used to place combo boxes and other components inside the DataGrid. Now you'll see how to enable forms to have flexible form items using the same technique.

The DataFormItem is a binding object that is created for each control placed inside the DataForm. It has somewhat similar to BindingUtils functionality to support two-way binding and resolves circular references. The DataFormItem has two major functions:

  • Attach an individual control inside to the instance of DataFormItemEditor to listen to the changes in the underlying control.

  • Create a UI control (either a default one, or according to requested masked input or resource).

The first function requires the DataFormItem control to support the syntax of encapsulating other controls as it's implemented in FormItem, for example:

<lib:DataFormItem dataField="EMP_ID" label="Emp Id:">
  <mx:TextInput/>
</lib:DataFormItem>

In this case the DataFormItem performs binding functions; in Flex framework, <mx:FormItem> would set or get the value in the encapsulated UI component, but now the DataFormItem will perform the binding duties. Assignment of any object to the dataField property item of the DataFormItem will automatically pass this value to the enclosed components. If, say an application developer decides to use a chart as a form item, the data assigned to the DataFormItem will be given for processing to the chart object. The point is that application developers would use this control in a uniform way regardless of what object is encapsulated in the DataFormItem.

The second function, creating a UI control, is implemented with the help of resources, which not only allow specifying the styling of the component, but also can define what component to use. If you go back to the code of the class ResourceBase, you'll find there a getter itemEditor that can be used for creation of controls. Actually, this gives you two flexible ways of creating controls for the form: either specify a resource name, or specify a component as itemEditor=myCustomComponent. If none of these ways is engaged a default TextInput control will de created.

The code above looks somewhat similar to the original FormItem, but it adds new powerful properties to the component that represents form item. The data of the form item is stored in the EMP_ID property of the data collection specified in the dataProvider of the DataForm. The label property plays the same role as in FormItem.

The source code of the DataFormItem component is shown in Example 3.15, “Class DataFormItem”. It starts with defining properties as in DataGriddataField, valueName and itemEditor. The DataGridItem can create an itemEditor from a String, an Object or a class factory. It also defines an array validator that will be described later in this chapter.

Example 3.15. Class DataFormItem

package com.farata.controls.dataFormClasses {
    import com.farata.controls.DataForm;
    import csom.farata.controls.MaskedInput;
    import com.farata.core.UIClassFactory;
    import com.farata.resources.ResourceBase;
    import com.farata.validators.ValidationRule;

    import flash.display.DisplayObject;
    import flash.events.Event;
    import flash.events.IEventDispatcher;
    import flash.utils.getDefinitionByName;

    import mx.containers.FormItem;
    import mx.events.FlexEvent;
    import mx.validators.Validator;

    dynamic public class DataFormItem extends FormItem {
          public function DataFormItem()    {
               super();
          }

        private var _itemEditor:IEventDispatcher; //DataFormItemEditor;

        [Bindable("itemEditorChanged")]
        [Inspectable(category="Other")]
        mx_internal var owner:DataForm;

        private var _dataField:String;
        private var _dataFieldAssigned:Boolean = false;
        private var _labelAssigned:Boolean = false;
        private var _valueName:String = null;
        private var _readOnly:Boolean = false;
        private var _readOnlySet:Boolean = false;

        public function set readOnly(f:Boolean):void{
          if( _readOnly==f ) return;
          _readOnly = f;
          _readOnlySet = true;
          commitReadOnly();
         }

        public function get readOnly():Boolean {
          return _readOnly;
        }

        public function set dataField(value:String):void{
          _dataField = value;
          _dataFieldAssigned = true;
        }

        public function get dataField():String{
          return _dataField;
        }

        override public function set label(value:String):void {
          super.label = value;
          _labelAssigned = true;
        }

        public function set valueName(value:String):void{
          _valueName = value;
          }

        public function get valueName():String {
          return _valueName;
        }

        override public function set data(value:Object):void{
          super.data = value;
          if(_itemEditor)
               if (_itemEditor["data"] != value[_dataField])
                     _itemEditor["data"] = value[_dataField];

               for ( var i : int = 0; i < validators.length; i++) {
                     if ( validators[i] is ValidationRule && data)
                          validators[i]["data"]= data;
                     validators[i].validate();
               }
        }

        override protected function createChildren():void{
          super.createChildren();
          if(this.getChildren().length > 0) {
               _itemEditor = new DataFormItemEditor(this.getChildAt(0), this);
               _itemEditor.addEventListener(Event.CHANGE, dataChangeHandler);
               _itemEditor.addEventListener(FlexEvent.VALUE_COMMIT,
                                                          dataChangeHandler);
              }
        }

        public function get itemEditor():Object {
          return _itemEditor;
        }

        private var _validators :Array = [];

        public function get validators() :Array {
               return _validators;
          }
          public function set validators(val :Array ): void {
               _validators = val;
          }

        public var _dirtyItemEditor:Object;

        public function set itemEditor(value:Object):void{
               _dirtyItemEditor = null;
         if(value is String){
                var clazz:Class = Class(getDefinitionByName(value as String));
            _dirtyItemEditor = new clazz();
          }
          if(value is Class)
               _dirtyItemEditor = new value();
          if(value is UIClassFactory)
               _dirtyItemEditor = value.newInstance();
          if(value is DisplayObject)
               _dirtyItemEditor = value;
       }

       private function dataChangeHandler(evt:Event):void{
               if (evt.target["data"]!==undefined)  {
                     if (data != null) {
                     data[_dataField] = evt.target["data"];
                     }
               }
          }

        private var _resource:Object;
        public function set resource(value:Object):void{
          _resource = ResourceBase.getResourceInstance(value);
          invalidateProperties();
        }

        public function get resource():Object{
          return _resource;
        }

        private function commitReadOnly():void{
          if( _itemEditor==null ) return;
          if( !_readOnlySet ) return;
          if( Object(_itemEditor).hasOwnProperty("readOnly") )
          {
               Object(_itemEditor).readOnly = _readOnly;
               _readOnlySet = false;
          }
        }

        override protected function commitProperties():void{
          super.commitProperties();
          if(itemEditor == null) //no child controls and no editor from resource
          {
                     var control:Object = _dirtyItemEditor;
                     if(!control && getChildren().length > 0)
                          control = getChildAt(0);  //user placed control inside
                     if(!control)
                          control = itemEditorFactory(resource as ResourceBase);

                     if(resource)
                          resource.apply(control);
                     if( (control is MaskedInput) && hasOwnProperty
("formatString"))
                          control.inputMask = this["formatString"];

                     addChild(DisplayObject(control));
                     //Binding wrapper to move data back and force
                     _itemEditor = new
                            DataFormItemEditor(DisplayObject(control),this);
                     _itemEditor.addEventListener(Event.CHANGE, dataChangeHandler);
                     _itemEditor.addEventListener(FlexEvent.VALUE_COMMIT,
                                                          dataChangeHandler);
              } else
               control = itemEditor.dataSourceObject;

         commitReadOnly();

          for ( var i : int = 0; i < validators.length; i++) {
               var validator : Validator = validators[i] as Validator;
               validator.property = (_itemEditor as DataFormItemEditor).valueName;
               validator.source = control;
               if ( validator is ValidationRule && data)
                     validator["data"]= data;
               validator.validate();
          }
        }
          protected function itemEditorFactory(resource : ResourceBase =
                                                         null):Object{
               var result:Object = null;
               if (resource && ! type)
                     result = resource.itemEditor;
               else {
                     switch(type)    {
                     case "checkbox":
                          result = new CheckBox();
                          if (!resource) {
                                resource = new CheckBoxResource(this);
                                resource.apply(result);
                          }
                          break;
                     case "radiobutton":
                          result = new RadioButtonGroupBox();
                          if (!resource) {
                                resource = new RadioButtonGroupBoxResource(this);
                                resource.apply(result);
                          }
                          break;
                     case "combobox":
                          result = new ComboBox();
                          if (!resource) {
                                resource = new ComboBoxResource(this);
                                resource.apply(result);
                          }
                          break;
                     case "date":
                          result = new DateField();
                          if (formatString) (result as DateField).formatString =
                                                                  formatString;
                          break;
                     case "datetime":
                          result = new DateTimeField();
                          if (formatString) (result as DateTimeField).formatString =
                                                                  formatString;
                          break;
                     case "mask":
                          result = new MaskedInput();
                          break;
                     }
               }
               if(result == null && formatString)
                     result = guessControlFromFormat(formatString);
               if(result == null)
                     result = new TextInput();
               return result;
          }

          protected function guessControlFromFormat(format:String):Object{
               var result:Object = null;
               if(format.toLowerCase().indexOf("currency") != −1)
                     result = new NumericInput();
               else if(format.toLowerCase().indexOf("date") != −1){
                     result = new DateField();
                     (result as DateField).formatString = format;
               }
               else{
                     result = new MaskedInput();
                     (result as MaskedInput).inputMask = format;
               }
               return result;
          }
    }
}

Read the code above, and you'll see that you can use an instance of a String, an Object, a class factory or a UI control as an itemEditor property of the DataFormItem. The function createChildren() adds event listeners for CHANGE and VALUE_COMMIT events, and when any of these events is dispatched, the dataChangeHandler() pushes provided value from the data attribute of the UI control use in the form item into the data.dataField property of the object in the underlying collection.

The resource setter allows application developers to use resources the same way as it was done with a DataGrid earlier in this chapter.

The function commitReadonly() ensures that the readOnly property on the form item can be set only after the item is created.

The function itemEditorFactory() supports creation of the form item components from a resource, based on the specified type property, requests a control to be created based on a format string. The guessControlFromFormat() is a function that can be extended based on the application needs, but in the code above it just uses a NumericInput component if the currency format was requested and DateField if the date format has been specified. If unknown format was specified, this code assumes that the application developer needs a mask hence the MaskedInput will be created.

Remember that Flex schedules a call to the function commitProperty() to coordinate modifications to component properties when a component is created. It's also called as a result of the application code calling invalidateProperties(). The function commitProperties() checks if the itemEditor is not defined. If it is not, it'll be created and the event listeners will be added. If the itemEditor exists, the code extracts from it the UI control used with this form item.

Next, the data form item instantiates the validators specified by the application developers. This code binds all provided validators to the data form item.

for ( var i : int = 0; i < validators.length; i++) {
               var validator : Validator = validators[i] as Validator;
               validator.property = (_itemEditor as DataFormItemEditor).valueName;
               validator.source = control;
               if ( validator is ValidationRule && data)
                     validator["data"]= data;
               validator.validate();
          }

The next section discusses the benefits of hiding validators inside the components and offers a sample application that shows how to use them and the functionality of the ValidationRule class. Meanwhile, Example 3.16, “Code fragment that uses DataForm and DataFormItem” demonstrates how an application developer could use the DataForm, DataFormItem, and resources. Please note that by default, DataFormItem renders a TextInput component.

Example 3.16. Code fragment that uses DataForm and DataFormItem

<lib:DataForm dataProvider="employeeDAO">
    <mx:HBox>
          <mx:Form>
               <lib:DataFormItem dataField="EMP_ID" label="Emp Id:"/>
               <lib:DataFormItem dataField="EMP_FNAME" label="First Name:"/>
               <lib:DataFormItem dataField="STREET" label="Street:"/>
               <lib:DataFormItem dataField="CITY" label="City:"/>
               <lib:DataFormItem dataField="BIRTH_DATE" label="Birth Date:"
                                    formatString="shortDate"/>
               <lib:DataFormItem dataField="BENE_HEALTH_INS" label="Health:"
                    resource="{com.farata.resources.YesNoCheckBoxResource}"/>
               <lib:DataFormItem dataField="STATUS" label="Status:"
                    resource="{com.farata.resources.StatusComboResource}"/>
          </mx:Form>

          <mx:Form>
               <lib:DataFormItem dataField="MANAGER_ID" label="Manager Id:"/>
               <lib:DataFormItem dataField="EMP_LNAME" label="Last Name:"/>
               <lib:DataFormItem dataField="STATE" label="State:"
                    resource="com.farata.resources.StateComboResource"/>
               <lib:DataFormItem dataField="SALARY" label="Salary:"
                    formatString="currency" textAlign="right"/>
               <lib:DataFormItem dataField="START_DATE" label="Start Date:"
                    formatString="shortDate"/>
               <lib:DataFormItem dataField="BENE_LIFE_INS" label="Life:"
                    resource="{com.farata.resources.YesNoCheckBoxResource}"/>
               <lib:DataFormItem dataField="SEX" label="Sex:"
                    resource="{com.farata.resources.SexComboResource}"/>
          </mx:Form>

          <mx:Form>
               <lib:DataFormItem dataField="DEPT_ID" label="Department:"
               resource="{com.farata.resources.DepartmentComboResource}"/>
               <lib:DataFormItem dataField="SS_NUMBER" label="Ss Number:"
            itemEditor="{com.theriabook.controls.MaskedInput}" formatString="ssn"/>
               <lib:DataFormItem dataField="ZIP_CODE" label="Zip Code:"
                                                    formatString="zip"/>
               <lib:DataFormItem dataField="PHONE" label="Phone Number:"
           itemEditor="{com.theriabook.controls.MaskedInput}" formatString="phone">

          <lib:validators>
               <mx:Array>
                     <mx:PhoneNumberValidator  wrongLengthError="keep typing"/>
               </mx:Array>
          </lib:validators>
         </lib:DataFormItem>
          <lib:DataFormItem dataField="TERMINATION_DATE"
                   label="Termination Date:" formatString="shortDate"/>
          <lib:DataFormItem dataField="BENE_DAY_CARE" label="Day Care:"
                  resource="{com.farata.resources.YesNoCheckBoxResource}"/>
          </mx:Form>
    </mx:HBox>
</lib:DataForm>

This code is an extract from the Café Townsend (Clear Data Builder's version) from Chapter 1, Comparing Selected Flex Frameworks. Run the application Employee_getEmployees_GridFormTest.mxml, double-click on a grid row and you'll see the DataForm in action. In the next section of this chapter you'll see other working examples of the DataForm and DataGrid with validators.

The final section of this chapter, part 3, will be published next week.

Read more from Yakov Fain. Yakov Fain's Atom feed

Comments

2 Comments

Derby said:

This may surprise you to know that the Pacific region has its fair share of web talent. Having just been surrounded by most of them for the past couple of days I can attest to the fact This year was my first WebDU, and I'm still learning Courses from home

Turner said:

The best way of course would be to build test them through the learning process

Leave a comment


Tag Cloud

Question of the Week: Dream App

If you had an unlimited budget and unlimited resources what application would you build and why would you build it?

Answer

Latest Features

Recommended for You

@InsideRIA on Twitter

Archives

  • Or, visit our complete archive.  

About This Site

Welcome to the premiere community site for all things RIA sponsored by O'Reilly Media and Adobe Systems Incorporated.