本文的目的在于向您简要介绍 clear.swc 中的一些对象,clear.swc 是开源 Clear Toolkit 框架的一部分,该框架可以从 https://sourceforge.net/projects/cleartoolkit获得。
组件库 clear.sw c包含一些增强的 Flex 组件,在过去的三年中,Farata Systems 的软件工程师已经在很多企业项目中开发和使用过这些组件。为了更好地理解本文档中的内容,我们强烈建议您检查来自 Sourceforge CVS 知识库的源代码,并检查下面提到的类的源代码。
ChangeObject 类
如果您熟悉 LCDS,您就会知道 Data Managemet Services 使用 ChangeObject,它是一个特殊的 DTO,用于在服务器和客户端之间传播更改。我们的组件也包含这样的对象,但是它不仅可以使用 LCDS 使用,还可以使用 BlazeDS 使用。
ChangeObject 类在客户端运行——它只是一个简单的存储容器,用于存储正在发生改变的记录的原始版本和新版本:
package com.farata.remoting {
[RemoteClass(alias="com.farata.remoting.ChangeObjectImpl")]
public class ChangeObject {
public var state:int;
public var newVersion:Object = null;
public var previousVersion:Object = null;
public var error:String = "";
public var changedPropertyNames:Array= null;
public static const UPDATE:int=2;
public static const DELETE:int=3;
public static const CREATE:int=1;
public function ChangeObject(state:int=0,
newVersion:Object=null, previousVersion:Object = null) {
this.state = state;
this.newVersion = newVersion;
this.previousVersion = previousVersion;
}
public function isCreate():Boolean {
return state==ChangeObject.CREATE;
}
public function isUpdate():Boolean {
return state==ChangeObject.UPDATE;
}
public function isDelete():Boolean {
return state==ChangeObject.DELETE;
}
}
每个更改的记录都可以处于 DELETE、UPDATE 或 CREATE 状态。该对象的原始版本存储在 previousVersion 属性中,而当前版本在 newVersion 属性中。这让 ChangeObject 变为 Assembler 模式的轻量级实现,该模式提供了一个简单的 API,采用标准的方法处理所有的数据更改,类似于伴随 LCDS 提供的 Data Management Services 所做的一样。
下面将要描述的 DataCollection 类会自动跟踪通过 UI 所做的更改,并发送相应的 ChangeObject 实例集合到服务器。
在服务器端,您可以通过引进外部接口将任务分开为 DAO 和 Assembler 层。外部接口是具有填充(检索)和同步(更新)功能的方法(注意下面 Java 代码中的后缀 _fill 和 _sync)。迭代 ChangeObject 实例的集合,并调用相应的持久函数:
/* Generated by ClearDB Utility (Dao.xsl) */
package com.farata.datasource;
import java.sql.*;
import java.util.*;
import flex.data.*;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.transaction.*;
import com.farata.daoflex.*;
public final class EmployeeDAO extends Employee {
public final List /*com.farata.datasource.dto.EmployeeDTO[]*/ fill{
return getEmployees_fill();
}
public final List getEmployees_sync(List items) {
Coonection conn = null;
try {
conn = JDBCConnection.getConnection("jdbc/test");
ChangeObject co = null;
for (int state=3; state > 0; state--) { //DELETE, UPDATE, CREATE
Iterator iterator = items.iterator();
while (iterator.hasNext()) { // Proceed to all updates next
co = (ChangeObject)iterator.next();
if(co.state == state && co.isUpdate())
doUpdate_getEmployees(conn, co);
if(co.state == state && co.isDelete())
doDelete_getEmployees(conn, co);
if(co.state == state && co.isCreate())
doCreate_getEmployees(conn, co);
}
}
} catch(DataSyncException dse) {
dse.printStackTrace();
throw dse;
} catch(Throwable te) {
te.printStackTrace();
throw new DAOException(te.getMessage(), te);
} finally {
JDBCConnection.releaseConnection(conn);
}
return items;
}
DataCollection 类
此对象跟踪客户端上的更改。例如,用户修改一个 DataGrid 中的数据,该 DataGrid 拥有一个用作 data provider 的对象的集合。我们已经使标准的 Flex ArrayCollection 更智能,以便其为每个更改行、新行和删除行创建一个 ChangeObject 实例的集合。DataCollection 类正是具有这样的功能。使用 Clear Data Builder 生成一个示例 CRUD 应用程序,并查看生成的示例 Flex 客户端的代码。填充 DataCollection 的代码将类似以下代码:
[Bindable]
public var collection:DataCollection ;
collection = new DataCollection();
collection.destination="myEmployeeDestination";
collection.method="getEmployees";
collection.addEventListener( CollectionEvent.COLLECTION_CHANGE, logEvent);
collection.addEventListener("fault", logEvent);
collection.fill();
When the user is ready to submit the changes to the server, the following line will send a collection of ChangeObject instances to the server:
collection.sync();
以下是 DataCollection 类中的一些相关的片段。它提供了一个 API 用于操作集合的状态。您可以查询该集合,以便发现该特定对象是新的、更新的还是已删除的。
public function isItemNew(item:Object):Boolean {
var co: ChangeObject = modified[item] as ChangeObject;
return (co!=null && co.isCreate());
}
public function setItemNew(item:Object):void {
var co:com.farata.remoting.ChangeObject = modified[item] as ChangeObject;
if (co!=null){
co.state = ChangeObject.CREATE;
}
}
public function isItemModified(item:Object):Boolean {
var co: ChangeObject = modified[item] as ChangeObject;
return (co!=null && !co.isCreate());
}
public function setItemNotModified(item:Object):void {
var co: ChangeObject = modified[item] as ChangeObject;
if (co!=null) {
delete modified[item];
modifiedCount--;
}
}
private var _deletedCount : int = 0;
public function get deletedCount():uint {
return _deletedCount;
}
public function set deletedCount(val:uint):void {
var oldValue :uint = _deletedCount ;
_deletedCount = val;
commitRequired = (_modifiedCount>0 || deletedCount>0);
dispatchEvent(PropertyChangeEvent.createUpdateEvent(this, "deletedCount", oldValue, _deletedCount));
}
private var _modifiedCount : int = 0;
public function get modifiedCount():uint {
return _modifiedCount;
}
public function set modifiedCount(val:uint ) : void{
var oldValue :uint = _modifiedCount ;
_modifiedCount = val;
commitRequired = (_modifiedCount>0 || deletedCount>0);
dispatchEvent(PropertyChangeEvent.createUpdateEvent(this, "modifiedCount", oldValue, _modifiedCount));
}
private var _commitRequired:Boolean = false;
public function set commitRequired(val :Boolean) :void {
if (val!==_commitRequired) {
_commitRequired = val;
dispatchEvent(PropertyChangeEvent.createUpdateEvent(this, "commitRequired", !_commitRequired, _commitRequired));
}
}
public function get commitRequired() :Boolean {
return _commitRequired;
}
public function resetState():void {
deleted = new Array();
modified = new Dictionary();
modifiedCount = 0;
deletedCount = 0;
}
当属性删除、插入或更新时,每个更改都可以访问:
public function get changes():Array {
var args:Array = deletes;
for ( var item:Object in modified) {
var co:com.farata.remoting.ChangeObject =
com.farata.remoting.ChangeObject(modified[item]);
co.newVersion = cloneItem(item);
args.push(co);
}
return args;
}
public function get deletes():Array {
var args:Array = [];
for ( var i :int = 0; i < deleted.length; i++) {
args.push(
new ChangeObject(
ChangeObject.DELETE, null,
ObjectUtils.cloneItem(deleted[i])
)
);
}
return args;
}
public function get inserts():Array {
var args:Array = [];
for ( var item:Object in modified) {
var co: ChangeObject = ChangeObject(modified[item]);
if (co.isCreate()) {
co.newVersion = ObjectUtils.cloneItem(item);
args.push( co );
}
}
return args;
}
public function get updates():Array {
var args:Array = [];
for ( var item:Object in modified) {
var co: ChangeObject = ChangeObject(modified[item]);
if (!co.isCreate()) {
// make up to date clone of the item
co.newVersion = ObjectUtils.cloneItem(item);
args.push( co );
}
}
return args;
}
此集合也应该处理与服务器的通信,并调用填充和同步方法:
public var _method : String = null;
public var syncMethod : String = null;
public function set method (newMethod:String):void {
_method = newMethod;
if (syncMethod==null)
syncMethod = newMethod + "_sync";
}
public function get method():String { return _method; }
protected function createRemoteObject():RemoteObject {
var ro:RemoteObject = null;
if( destination==null || destination.length==0 )
throw new Error("No destination specified");
ro = new RemoteObject();
ro.destination = destination;
ro.concurrency = "last";
ro.addEventListener(ResultEvent.RESULT, ro_onResult);
ro.addEventListener(FaultEvent.FAULT, ro_onFault);
return ro;
}
public function fill(... args): AsyncToken {
var act:AsyncToken = invoke(method, args);
act.method = "fill";
return act;
}
protected function invoke(method:String, args:Array):AsyncToken {
if( ro==null ) ro = createRemoteObject();
ro.showBusyCursor = true;
var operation:AbstractOperation = ro.getOperation(method);
operation.arguments = args;
var act:AsyncToken = operation.send();
return act;
}
protected function ro_onFault(evt:FaultEvent):void {
CursorManager.removeBusyCursor();
if (evt.token.method == "sync") {
modified = evt.token.modified;
modifiedCount = evt.token.modifiedCount;
deleted = evt.token.deleted;
}
dispatchEvent(evt);
if( alertOnFault && !evt.isDefaultPrevented() ) {
var dst:String = evt.message.destination;
if( dst==null || (dst!=null && dst.length==0) )
try{ dst = evt.target.destination; } catch(e:*){};
var ue:UnhandledError = UnhandledError.create(null, evt,
DataCollection, this, evt.fault.faultString,
"Error on destination: " + dst);
ue.report();
}
}
public function sync():AsyncToken {
var act:AsyncToken = invoke(syncMethod, [changes]);
act.method = "sync";
act.modified = modified;
act.deleted = deleted;
act.modifiedCount=modifiedCount;
if (writeThrough)
resetState();
return act;
}
}
}
使用资源文件进行数据样式化
资源是一些小文件,可以作为属性附加到 UI 组件以及 DataGrid 列。我们将使用资源编程称为数据样式化 (data styling)。
同样,从检查由 Clear Data Builder 生成的示例项目的代码开始。您可以在文件夹 flex_src/com/farata/resource 下找到一些资源文件。
查看 YesNoCheckBoxResource.mxml 的代码:
<?xml version="1.0" encoding="utf-8"?>
<fx:CheckBoxResource
xmlns="com.farata.resources" xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:resources="com.theriabook.resources.*"
offValue = "N"
onValue = "Y"
textAlign="center"
>
</fx:CheckBoxResource>
这不正像一个样式吗?这也可以特定于场景。这使将 on/off 值 Y/N 更改为 Д/Н (在俄语中为 Да/Нет ,西班牙语中为 Si/No)很容易。
我们希望您把这样的资源当作独立于应用程序组件的实体。这样的功能不是类似于 CSS 吗?
事实上,这比 CSS 更复杂,因为这个资源是样式和属性的混合。下一个代码示例仍是另一个称之为 StateComboBoxResource.mxml 资源。它演示了在这样的资源(我们也称之为 Business Style Sheet)中使用属性(即 DataProvider)。这样的资源可以包含一列值,比如州的名称和缩写。
<?xml version="1.0" encoding="utf-8"?>
<fx:ComboBoxResource
xmlns="com.farata.resources" xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:resources="com.theriabook.resources.*"
dropdownWidth="160"
width="160"
>
<fx:dataProvider>
<mx:Array>
<mx:Object data="AL" label="Alabama" />
<mx:Object data="AZ" label="Arizona" />
<mx:Object data="CA" label="California" />
<mx:Object data="CO" label="Colorado" />
<mx:Object data="CT" label="Connecticut" />
<mx:Object data="DE" label="Delaware" />
<mx:Object data="FL" label="Florida" />
<mx:Object data="GA" label="Georgia" />
<mx:Object data="WY" label="Wyoming" />
</mx:Array>
</fx:dataProvider>
</fx:ComboBoxResource>
另一个资源的示例包含对远程目标的引用,用于自动检索来自(比如) DBMS 的动态数据。
<?xml version="1.0" encoding="utf-8"?>
<fx:ComboBoxResource
xmlns="com.farata.resources" xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:resources="com.theriabook.resources.*"
width="160"
dropdownWidth="160"
destination="Employee"
keyField="DEPT_ID"
labelField="DEPT_NAME"
autoFill="true"
method="getDepartments"
>
</fx:ComboBoxResource>
事实上,您无法从此代码判断数据来自于 DBMS 或来自其他地方。该数据完全独立于与此特定资源相关的 ComboBox 对象的实例,可以在全局范围内进行缓存(如果该数据只需要检索一次)或者根据框架缓存规范进行缓存。如果您要开发一个业务框架,您可以允许(比如)查询对象对每个应用程序加载一次,或者对每个视图加载一次。此灵活性在基于 Singleton 的架构框架中是不存在的。根据此资源文件,您只能说从远程目标返回的数据叫做 Employee,既可以是类名,也可以是类工厂(class factory)。您也可以看到,方法 getDepartments() 将返回包含 DEPT_ID 和 DEPT_NAME (它们将使用本章之前描述过的增强的 ComboBox 使用)的数据。
现在我们需要建立一种机制将这样的资源附加到 Flex UI 组件。为了讲述一个使用资源的 Combobox,向其添加一个资源:
private var _resource:Object;
public function get resource():Object
{
return _resource;
}
public function set resource(value:Object):void {
_resource = value;
var objInst:* = ResourceBase.getResourceInstance(value);
if(objInst)
objInst.apply(this);
}
此资源属性允许您写入如下内容:
<fx:ComboBox resource=”{DepartmentComboResource}”
每个 clear.swc 中包含的增强 UI 组件都包含这样的属性。由于界面不允许这样的 setter 和 getter 的默认实现,而且由于 ActionScript 不支持多重继承,将此资源属性的实现包含到每个控件的最简便方法,就是使用语言编译时指令 #include,该指令将外部文件(比如 resource.as)的内容包含到您的组件的代码中。
#include “resource.as”
查看 Clear Data Builder 生成的代码 Employee_getEmployee_GridFormTest.mxml。您将发现一些使用资源的示例:
<fx:DataGrid dataProvider="{collection}" doubleClick="vs.selectedIndex=1" doubleClickEnabled="true" editable="true" height="100%" horizontalScrollPolicy="auto" id="dg" width="100%">
<fx:columns>
<fx:DataGridColumn dataField="emp_id" editable="false"
headerText="Emp Id"/>
…
<fx:DataGridColumn dataField="dept_id" editable="false"
headerText="Department"
resource="{com.farata.resources.DepartmentComboResource}"/>
<fx:DataGridColumn dataField="state" editable="false"
headerText="State"
resource="{com.farata.resources.StateComboResource}"/>
<fx:DataGridColumn dataField="bene_health_ins" editable="false"
headerText="Health"
resource="{com.farata.resources.YesNoCheckBoxResource}"/>
<fx:DataGridColumn dataField="bene_life_ins" editable="false"
headerText="Life"
resource="{com.farata.resources.YesNoCheckBoxResource}"/>
<fx:DataGridColumn dataField="bene_day_care" editable="false"
headerText="Day Care"
resource="{com.farata.resources.YesNoCheckBoxResource}"/>
<fx:DataGridColumn dataField="sex" editable="false"
headerText="Sex"
resource="{com.farata.resources.SexRadioResource}"/>
</fx:columns>
</fx:DataGrid>
使用资源真的可以极大地提高您的工作效率。
DataForm 组件
几乎每个企业应用程序都使用表单(form)。Flex 拥有 Form 类,应用程序程序员将其绑定到代表数据模型的对象。但是最初的 Form 类并不能跟踪数据更改。
由于 DataCollection 非常智能,可以自动跟踪客户端数据更改,我们设法增强了 Form 类,使它的工作方式类似于 DataGrid/DataCollection。而且,它也拥有 dataProvider。
我们的 DataForm 控件和其 DataCollection 类型的模型是绑定的,并且它自动跟踪所有与 ChangeObject 类(通过远程数据服务实现)兼容的更改。
第二个改进在数据验证领域。DataForm 不仅可以验证单个表单项,而且可以验证整个表单。我们的 DataForm 在表单内部而不是外部全局对象中存储其验证程序。此表单成为一个自给自足的黑盒。
从事原型开发的软件开发人员希望在被问及要将哪个控件放到数据表单上时,不需要给出明确的答案。这种表单的第一版可以使用 TextInput 控件,但是下一版可以使用 ComboBox 代替。
第三个改进应该简化 UI 原型开发。我们提出了一个 UI 中立的数据表单项,该表单项将不是特定的,比如“我是一个 TextInput”或“我是一个 ComboBox”。相反,开发人员将可以使用容易附加的资源,使用一般的数据项创建原型。
DataForm 是 FlexForm 的子类,其代码实现双向绑定,并且包含一个新的属性 dataProvider。其函数 validateAll() 支持数据验证。此 DataForm 组件将正确响应数据更改,将它们传递到其 data provider。
查看 DataForm 类的代码(可在 Sourceforge CVS 知识库中获得)。注意设置器 dataProvider,它总是将提供的数据包装到一个集合中。这样可以确保我们的 DataForm 像 DataGrid 一样支持远程数据服务。它检查值的数据类型。它将 Array 包装到 ArrayCollection 中,而 XML 变为 XMLListCollection。如果您需要更改存储表单数据的支持集合,只需要将集合变量指向新数据。
如果单个对象作为 dataProvider 提供,将其转变为一个元素的数组,然后转变为一个集合对象。Model 的一个实例提供了一个很好的例子,它是一个知道如何分派有关其属性更改的事件的 ObjectProxy 。
有时候,应用程序开发人员需要呈现不可编辑的表单,因此 DataForm 类定义了 readOnly 属性。底层数据的更改被传递给方法 collectionChangeHandler() 中的表单。该数据既可以在 dataProvider 中更改,也可以从 UI 更改,而且 DataForm 确保每个可见的 DataFormItem 对象 (items[i]) 知道该更改。这是在函数 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];
}
}
}
同样,我们需要将数据包装到一个集合中,以支持 LCDS 中的 DataCollection 或者 DataService。
技术上讲,DataForm 类是一个 Vbox,按两列排列其子类,并自动对齐表单项的标签。但是我们希望 DataForm允许嵌套,或包含一些表单项,这些表单项同时也是 DataForm 对象的实例。递归函数 enumerateChildren() 循环表单的子项,如果它发现一个 DataFormItem,它只是将其添加到数组项。但是如果该子项是一个容器,则该函数会循环其子项,并将它们添加到相同的项数组。最后,属性项包含所有必须填充的 DataFormItem。
DataForm 有一个函数 validateAll(),它在 Flex 框架中位于Validator类中。在那里,验证功能对表单元素是外部的,您需要给出一组附加了特定表单字段的验证程序。
增强验证
似乎 Flex 中的验证设计都基于一个假设:软件开发人员主要将验证应用于表单,而且每个 validator 类仅依赖于附加到它的一个字段。比如,您有一个拥有两个电子邮件字段的表单。Flex 框架强制您创建 EmailValidator 对象的两个实例——每个字段一个实例。
实际上,您也可能需要处理基于多个字段之间关系的验证条件。您还可能需要突出显示多个字段中的无效值。例如,将日期验证程序设置到一个字段,以检查输入的数据是否位于开始和结束日期字段指定的时间区间内。如果该日期无效,则您可能想要突出显示所有表单字段。
换句话说,您可能不仅需要验证对象属性,而且还可能需要在函数中写入一些验证规则。这些规则不仅可以和UI控件关联,也可以和底层数据关联,例如在 DataGrid 的行中显示的数据。
然而,另一个问题是,当 UI 控件自动创建时,如果是视图状态,Flex Validator 有它的局限性。如果 validator 可以在 UI 控件内部存在,则一切都会简单得多,这样它们会随同宿主控件自动添加到视图状态中。
在客户端上有方便的验证方法是企业 Flex 框架的一个重要方面。
现在我们考虑一个用于在银行或者保险公司中开新账户的 RIA。该业务流程通常从填写一个长长的申请表的多个部分开始。在 Flex 中,这样的应用可能变成具有(比如)五个表单(总共 50 个字段)的自定义组件的 ViewStack。这些自定义组件和 validator 事实上存储在独立的文件中。纸制表格的每个部分都可以表示为 Accordion 或其他 navigator 中一部分的内容。这样您总共有 50 个 validator,但是实际上,如果应用程序开发人员决定将一个字段从一个自定义组件移动到另一个,则他需要在代码中进行适当的更改,以便让旧的 validator 和重新部署的字段同步。
如果一些表单字段通过视图状态使用会怎么样?您如何验证这些移动的目标?如果 currentState=“Details”时要添加三个字段,则需要手动在状态部分 Details 编写 AddChild语句。
假设这 50 个 validator 中 40 个是永久的,而其他 10 个只使用一次。但是即使这 40 个 valdator,您也不想同时使用,因此您需要创建(比如)两个每个拥有 12 个元素的数组,并根据视图状态更改添加/删除临时 validator。
虽然 Flex 好像将 validator 和验证字段分开,但是这不是真正的分开,而是一种紧密的结合。
一位经理曾对一位软件开发人员说,“不要给我一个问题,给我一个解决方案”。OK,这就是解决方案。
我们要澄清一点——我们的目标不是替换现有的 Flex 验证例程,而是为您提供一个可用于表单和基于列表的控件的可选方案。
我们希望可以拥有一个具有 5 个自定义组件的 ViewStack,每个组件拥有一个 DataForm,其元素可以访问所有 50 个字段,但是每个组件只验证自己的 10 个字段。换句话说,所有 5 个表单将可以访问同一个 50 个字段的 dataProvider。如果在开户期间,用户在第一个表单的字段 age 中输入 65,则第 5 个表单会显示一些字段,这些字段拥有开养老金计划账户的选项,而这些字段对年轻的客户是不可见的。
这就是每个表单需要能够访问所有数据的原因,但是当您只需要验证当前在屏幕上可见的字段时,您应该可以针对此特殊的 DataForm 来完成验证。
Clear 组件库包含 ValidationRule 类。我们在这里将不详细讨论这个类,而是给您展示一个代码片段,该片段展示了将 validator 嵌入到 DataGridColumn 内是多么容易。
<fx:DataGridColumn dataField="PHONE" headerText="Phone Number"
formatString="phone" >
<fx:validators>
<mx:Array>
<mx:PhoneNumberValidator wrongLengthError="Wrong
length, need 10 digit number"/>
</mx:Array>
</fx:validators>
</fx:DataGridColumn>
不用说这不是一个原始的 Flex DataGridColumn,而是派生的。
结束语
本文只简单介绍了 Clear 组件库的主要元素。在下一篇文章中,我们将查看对 ComboBox 或 CheckBox 这些小组件所做的增强。
我们也计划录制并发布一些屏幕视频,演示 clear.swc 的各种组件的使用。在即将出版的 O’Reilly 的新书《Enterprise Development with Flex》中,将更详细地描述创建 Flex 组件的增强库的过程。
本文最初在 Yakov Fain 的博客 上发表,并且在作者的许可下出版。