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 want to update the form when the data model changes, you have to manually program it using the curly braces syntax in one direction and BindingUtils.bindProperty() in another.

Flex 4 introduces a new feature: two-way binding. Add an @ sign to the binding expression (@{expression}) and notifications about data modifications are sent in both directions—from the 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 doesn’t have 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 the 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 a 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 the selected row are displayed in a form. Back in Chapter 1, you saw an example of this: double-clicking a grid row in Figure 1-19 opened up a form that displayed the details for 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, which also registers event listeners with the model.

Flex offers the Form class, which an application programmer binds 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 a means of tracking the data changes.

It would be nice if the Form control (bound to its model of type DataCollection) could support similar functionality, with automatic tracking of all changes compatible with the 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 on what can be improved in the validation process, see the section 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. Besides needing to have uniform controls, software developers working on prototypes would appreciate not being 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 may use a ComboBox instead. You want to come up with some UI-neutral creature (call it a data form item) that will allow a lack of specificity, 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 68). 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, as explained in the next sections. This DataForm component will properly respond to data changes, propagating them to its data provider.

Example 68. 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 example code. 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 that 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) that knows how to dispatch events about changes of its properties.

Once in a while, application developers need to render noneditable 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 or 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 0 of the collection, because the form always has one object with data that is bound to the form. Such a design resembles the functionality of the data variable of the Flex DataGrid, which 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; in the Flex framework, it 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 compared to the original Flex Form object, which relies on external validators.

The DataFormItem Component

The DataFormItem, an extension of the 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. The design of new Flex 4 components has also been shifted toward separation between their UI and functionality.

At least 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 but checkboxes and radio buttons. For these controls (and whatever else you may need), just use resources. Forms also use combo boxes. The earlier section DataGrid with Resources showed 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 functionality somewhat similar to that of BindingUtils to support two-way binding and resolve circular references. The DataFormItem has two major functions:

  • Attach an individual control internally 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 the 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 the 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 an application developer decides to use a chart as a form item, for example, 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 a better itemEditor that can be used for the 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 neither of these ways is engaged, a default TextInput control will be created.

The previous code looks somewhat similar to the original FormItem, but it adds new powerful properties to the component that represents the 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 69. It starts with defining properties, as in DataGrid: dataField, valueName, and itemEditor. The DataGridItem can create an itemEditor from a String, an Object, or a class factory. It also defines an array validator, which will be described later in this chapter.

Example 69. 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;
        }
    }
}

You’ll see in the example code 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 the provided value from the data attribute of the UI control used 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 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 value of the variable type. The guessControlFromFormat() is a function that can be extended based on the application needs, but in the previous code, it just uses a NumericInput component if the currency format was requested and DateField if the date format has been specified. If an 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 commitProperties() 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 whether the itemEditor is 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 70 demonstrates how an application developer could use the DataForm, the DataFormItem, and resources. Please note that by default, DataFormItem renders a TextInput component.

Example 70. 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 application (Clear Data Builder’s version) from Chapter 1. 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 DataForm and DataGrid with validators.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.137.93