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 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
, 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:
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.
18.218.137.93