Once the transport technology has been selected, you need to try to remove the complexity of the data access and persistence layer. The Data Management Services that come with LCDS provide an excellent model for automation of this task. But you can develop your own framework based on the open source products, and in the following sections, you’ll learn how to re-create all the necessary components for a data persistence framework.
To offer functionality similar to that of LCDS in our framework, we need to create the following data management components:
Data transfer objects
ChangeObject
Assembler
A change-tracking collection
A destination-aware collection
In the following sections, we’ll offer you Farata Systems’ version of such components. If you like them, get their source code in the CVS repository at SourceForge and use them as you see fit. We also encourage you to enhance them and make them available for others in the same code repository.
Using data transfer objects (DTOs) is very important for architecting automated updates and synchronization. In Flex/Java RIA, there are at least two parties that need to have an “exchange currency”: ActionScript and Java. Each of these parties has their own contracts on how to support the data persistence. Let’s concentrate on the ActionScript part first.
In the Café Townsend sample, the data objects responsible for the
exchange between Java and ActionScript are EmployeDTO.java and EmployeeDTO.as (see a fragment of
EmployeeDTO.as in Example 6-2). The Java side sends instances of EmployeDTO
objects, which are automatically
re-created as their ActionScript peers on the frontend.
Example 6-2. Employee.DTO.as
/* Generated by Clear Data Builder (ActionScriptDTO_IManaged.xsl) */ package com.farata.datasource.dto { import flash.events.EventDispatcher; import flash.utils.Dictionary; import flash.utils.ByteArray; import mx.events.PropertyChangeEvent; import mx.core.IUID; import mx.utils.UIDUtil; [RemoteClass(alias="com.farata.datasource.dto.EmployeeDTO")] [Bindable(event="propertyChange")] public dynamic class EmployeeDTO extends EventDispatcher //implements IManaged { // Internals public var _nulls:String; // Properties private var _EMP_ID : Number; private var _MANAGER_ID : Number; ... public function get EMP_ID() : Number{ return _EMP_ID; } public function set EMP_ID( value : Number ):void{ var oldValue:Object = this._EMP_ID; if (oldValue !== value) { this._EMP_ID = value; dispatchUpdateEvent("EMP_ID", oldValue, value); } } public function get MANAGER_ID() : Number{ return _MANAGER_ID; } public function set MANAGER_ID( value : Number ):void{ var oldValue:Object = this._MANAGER_ID; if (oldValue !== value) { this._MANAGER_ID = value; dispatchUpdateEvent("MANAGER_ID", oldValue, value); } } public function get properties():Dictionary { var properties:Dictionary = new Dictionary(); properties["EMP_ID"] = _EMP_ID; properties["MANAGER_ID"] = _MANAGER_ID; return properties; } public function set properties(properties:Dictionary):void { _EMP_ID = properties["EMP_ID"]; _MANAGER_ID = properties["MANAGER_ID"]; ... } private var _uid:String; public function get uid():String { return _uid; } public function set uid(value:String):void { _uid = value; } public function EmployeeDTO() { _uid = UIDUtil.createUID(); } public function newInstance() : * { return new EmployeeDTO();} private function dispatchUpdateEvent(propertyName:String, oldValue:Object, value:Object):void { dispatchEvent( PropertyChangeEvent.createUpdateEvent(this, propertyName, oldValue, value) ); } public function clone(): EmployeeDTO { var x:EmployeeDTO = new com.farata.datasource.dto.EmployeeDTO(); x.properties = this.properties; return x; } } }
The class starts with a [RemoteClass]
metadata tag that instructs the
compiler that this class should be
marshaled and re-created as its peer com.farata.datasource.dto.
EmployeeDTO
on the server
side.
This class is an event dispatcher and any changes to its members
will result in the update event, which allows you to perform easy
tracking of its properties’ changes by dispatching appropriate events.
This feature is also important for the UI updates if the DTOs are bound
to UI controls, such as a DataGrid
.
Note that all the properties in this class are getter/setter
pairs: they can’t remain public variables, because we want the dispatchUpdateEvent()
method to be called
every time the variable’s value is being changed.
In addition to the functional properties like EMP_ID
and EMP_FNAME
, the class also contains a setter
and getter for the uid
property; this
qualifies the class as an implementer of the IUID interface. Existence
of a uid
property allows easy
indexing and searching of records on the client.
However, implementing uid
as a
primary key on the server side is crucial in order to ensure
synchronization and uniqueness of updates. Usually uid
represents the primary key from a database
table. The other function often required by automatic persistence
algorithms is getChangedPropertyNames()
, in order to teach
DTO to mark updated properties (Example 6-3).
Example 6-3. EmployeeDTO.java
package com.farata.datasource.dto; import java.io.Serializable; import com.farata.remoting.ChangeSupport; import java.util.*; import flex.messaging.util.UUIDUtils; public class EmployeeDTO implements Serializable, ChangeSupport { private static final long serialVersionUID = 1L; public String _nulls; // internals public long EMP_ID; public long MANAGER_ID; ... public Map getProperties() { HashMap map = new HashMap(); map.put("EMP_ID", new Long(EMP_ID)); map.put("MANAGER_ID", new Long(MANAGER_ID)); ... return map; } // Alias names is used by code generator of CDB in the situations // if select with aliases is used, i.e. // SELECT from A,B a.customer cust1, b.customer cust2 // In this case plain names on the result set would be cust1 and cust2, // which would complicate generation of the UPDATE statement. // If you don't use code generators, there is no need to add aliasMap // to your DTOs public static HashMap aliasMap = new HashMap(); public String getUnaliasedName(String name) { String result = (String) aliasMap.get(name); if (result==null) result = name; return result; } public String[] getChangedPropertyNames(Object o) { Vector v = new Vector(); EmployeeDTO old = (EmployeeDTO)o; if (EMP_ID != old.EMP_ID) v.add(getUnaliasedName("EMP_ID")); if (MANAGER_ID != old.MANAGER_ID) v.add(getUnaliasedName("MANAGER_ID")); ... String [] _sa = new String[v.size()]; return (String[])v.toArray(_sa); } }
To better understand how changes are kept, take a look at the
internals of the ChangeObject
class, which stores all
modifications performed on the DTO. It travels between the client and
the server.
ChangeObject
is a special DTO
that is used to propagate the changes between the server and the client.
The ChangeObject
class exists in the
Data Management Services of LCDS, and is shown in Example 6-4. On the client side, it is just a simple
storage container for original and new versions of a record that is
undergoing some changes. For example, if the user changes some data in a
DataGrid
row, the instance of the
ChangeObject
will be created, and the
previous version of the DTO that represents this row will be stored
along with the new one.
Example 6-4. ChangeObject.as
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; } } }
As you can see, every changed record can be in a DELETE
, UPDATE
, or CREATE
state. The original version of the
object is stored in the previousVersion
property and the current one
is in the newVersion
. That turns the
ChangeObject
into a lightweight
implementation of the Assembler pattern, which offers a simple API to
process all the data changes in a standard way, similar to what’s done
in the Data Management Services that come with LCDS.
The Java counterpart of the ChangeObject
(Example 6-5) should have few extra convenience
generic methods. All specifics are implemented in a standard way in the
EmployeeDTO
.
Example 6-5. ChangeObjectImpl.java
Package com.theriabook.remoting; import java.util.*; public class ChangeObjectImpl { public void fail() { state = 100; } public void fail(String desc) { // TODO Auto-generated method stub fail(); error = desc; } public String[] getChangedPropertyNames() { // TODO Auto-generated method stub changedNames = newVersion.getChangedPropertyNames(previousVersion); return changedNames; } public Map getChangedValues() { if ((newVersion==null) || (previousVersion==null)) return null; if(changedValues == null) { if(changedNames == null) changedNames = getChangedPropertyNames(); if (newMap == null) newMap = newVersion.getProperties(); changedValues = new HashMap(); for(int i = 0; i < changedNames.length; i++) { String field = changedNames[i]; changedValues.put(field, newMap.get( field)); } } return Collections.unmodifiableMap(changedValues); } public Object getPreviousValue(String field) { if (previousMap == null) previousMap = previousVersion.getProperties(); return previousMap.get( field ); } public boolean isCreate() { return state == 1; } public boolean isDelete() { return state == 3; } public boolean isUpdate() { return state == 2; } public void setChangedPropertyNames(String [] columns) { changedNames = columns; changedValues = null; } public void setError(String s) { error = s; } public void setNewVersion(Object nv) { newVersion = (ChangeSupport)nv; changedValues = null; } public void setPreviousVersion(Object o) { previousVersion = (ChangeSupport)o; } public void setState(int s) { state = s; } //---------------------- E X T E N S I O N S-------------------------- public int state = 0; public ChangeSupport newVersion = null; public ChangeSupport previousVersion = null; public String error =""; protected Map newMap = null; protected Map previousMap = null; protected String[] changedNames = null; protected Map changedValues = null; }
In Core J2EE Patterns, the Transfer Object Assembler means a class
that can build DTOs from different data sources (see http://java.sun.com/blueprints/corej2eepatterns/Patterns/TransferObjectAssembler.html).
In Flex/Java RIA, the Assembler
class
would hide from the Flex client actual data sources used for data
retrieval. For example, it can expose the method getEmployees()
for retrieval of the EmployeeDTO
objects that are actually
retrieved from more than one data source.
For simplicity, the method getEmployees()
shown in Example 6-6 delegates the processing to a single
Data Access Object (DAO), but this does not have to be the case, and the
data required for population of the list of EmployeeDTO
s can be coming from several data
sources.
Similarly, for data updates the client calls the sync()
method without knowing the specifics;
the DAO class or classes take care of the data persistence.
In the example framework, you’ll build an
Assembler
class similar to what Adobe recommends
creating in the case of using LCDS. The instances of ChangeObject
are used for communication
between Flex and the Java Assembler
class, which in turn will use them for communication with DAO
classes.
The Assembler pattern cleanly separates the generic
Assembler
’s APIs from specifics of the DAO
implementation.
Example 6-6. EmployeeAssembler.java
package com.farata.datasource; import java.util.*; public final class EmployeeAssembler{ public EmployeeAssembler(){ } public List getEmployees() throws Exception{ return new EmployeeDAO().getEmployees(); } public final List getEmployees_sync(List items){ return new EmployeeDAO().getEmployees_sync(items); } }
The two main entry points (data retrieval and updates) will show you how easy it is to build a DAO adapter.
First, you need to separate the task into the DAO and Assembler
layers by introducing methods with fill (retrieve)
and sync (update) functionality. The complete
source code of the EmployeeDAO
class
is included in the code samples accompanying this book, and the relevant
fragments from this class follow in Example 6-7.
Example 6-7. Fill and sync fragment from EmployeeDAO.java
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 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; } public final List /*com.farata.datasource.dto.EmployeeDTO[]*/ getEmployees_fill() { String sql = "select * from employee where dept_id=100"; ArrayList list = new ArrayList(); ResultSet rs = null; PreparedStatement stmt = null; Connection conn = null; try { conn = JDBCConnection.getConnection("jdbc/test"); stmt = conn.prepareStatement(sql); rs = stmt.executeQuery(); StringBuffer nulls = new StringBuffer(256); while( rs.next() ) { EmployeeDTO dto = new dto.EmployeeDTO(); dto.EMP_ID = rs.getLong("EMP_ID"); if( rs.wasNull() ) { nulls.append("EMP_ID|"); } dto.MANAGER_ID = rs.getLong("MANAGER_ID"); if( rs.wasNull() ) { nulls.append("MANAGER_ID|"); } ... dto.uid = "|" + dto.EMP_ID; list.add(dto); } return list; } catch(Throwable te) { te.printStackTrace(); throw new DAOException(te); } finally { try {rs.close(); rs = null;} catch (Exception e){} try {stmt.close(); stmt = null;} catch (Exception e){} JDBCConnection.releaseConnection(conn); } }
As you can see in Example 6-7, the
implementation of the fill
method is
really straightforward. Review the code of the sync
method, and you’ll see that it iterates
through the collection of ChangeObject
s; calls their methods isCreate()
, isUpdate()
, and isDelete()
; and calls the corresponding
function in the DAO class. These functions are shown in the
example.
Implementation of the insert
and
delete
statements is based on new or old versions
wrapped inside ChangeObject
. Example 6-8 calls the method
getNewVersion()
to get the data for
insertion in the database and getPreviousVersion()
for delete.
Example 6-8. Create and delete fragment from EmployeeDAO.java
private ChangeObject doCreate_getEmployees(Connection conn, ChangeObject co) throws SQLException{ PreparedStatement stmt = null; try { String sql = "INSERT INTO EMPLOYEE " + "(EMP_ID,MANAGER_ID,EMP_FNAME,EMP_LNAME, DEPT_ID,STREET,CITY,STATE,ZIP_CODE,PHONE, STATUS,SS_NUMBER,SALARY,START_DATE,TERMINATION_DATE, BIRTH_DATE,BENE_HEALTH_INS,BENE_LIFE_INS, BENE_DAY_CARE,SEX)"+ " values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; stmt = conn.prepareStatement(sql); EmployeeDTO item = (EmployeeDTO) co.getNewVersion(); stmt.setLong(1, item.EMP_ID); stmt.setLong(2, item.MANAGER_ID); ... if (stmt.executeUpdate()==0) throw new DAOException("Failed inserting."); co.setNewVersion(item); return co; } finally { try { if( stmt!=null) stmt.close(); stmt = null;} catch (Exception e){// exception processing goes here} } } private void doDelete_getEmployees(Connection conn, ChangeObject co) throws SQLException{ PreparedStatement stmt = null; try { StringBuffer sql = new StringBuffer ("DELETE FROM EMPLOYEE WHERE (EMP_ID=?)"); EmployeeDTO item = (EmployeeDTO) co.getPreviousVersion(); stmt = conn.prepareStatement(sql.toString()); stmt.setLong(1, item.EMP_ID); if (stmt.executeUpdate()==0) throw new DataSyncException(co, null, Arrays.asList(new String[]{"EMP_ID"})); } finally { try { if( stmt!=null) stmt.close(); stmt = null; } catch (Exception e){} } }
To form the update statement, you need both the previous and the
new versions of the data available inside ChangeObject
instances (Example 6-9).
Example 6-9. Update fragment from EmployeeDAO.java
private void doUpdate_getEmployees(Connection conn, ChangeObject co) throws SQLException{ String updatableColumns ",EMP_ID,MANAGER_ID,EMP_FNAME,EMP_LNAME, DEPT_ID,STREET,CITY,STATE,ZIP_CODE, PHONE,STATUS,SS_NUMBER,SALARY,START_DATE, TERMINATION_DATE,BIRTH_DATE,BENE_HEALTH_INS, BENE_LIFE_INS,BENE_DAY_CARE,SEX,"; PreparedStatement stmt = null; try { StringBuffer sql = new StringBuffer("UPDATE EMPLOYEE SET "); EmployeeDTO oldItem = (EmployeeDTO) co.getPreviousVersion(); String [] names = co.getChangedPropertyNames(); if (names.length==0) return; for (int ii=0; ii < names.length; ii++) { if (updatableColumns.indexOf("," + names[ii] +",")>=0) sql.append((ii!=0?", ":"") + names[ii] +" = ? "); } sql.append( " WHERE (EMP_ID=?)" ); stmt = conn.prepareStatement(sql.toString()); Map values = co.getChangedValues(); int ii, _jj; Object o; _jj = 0; for (ii=0; ii < names.length; ii++) { if (updatableColumns.indexOf("," + names[ii] +",")>=0) { _jj++; o = values.get(names[ii]); if ( o instanceof java.util.Date) stmt.setObject( _jj,DateTimeConversion.toSqlTimestamp((java.util.Date)o) ); else stmt.setObject( _jj, o ); } } _jj++; stmt.setLong(_jj++, oldItem.EMP_ID); if (stmt.executeUpdate()==0) throw new DataSyncException(co, null, Arrays.asList(new String[]{"EMP_ID"})); } finally { try { if( stmt!=null) stmt.close(); stmt = null; } catch (Exception e){} } } }
You can either manually write the code shown in Examples 6-2 to 6-9, or use the Clear Data Builder for automated code generation.
The code in the examples is generic and can be either generated for the best performance or parameterized for Java frameworks such as Spring or Hibernate.
It’s time to establish an ActionScript collection that will have two important features:
It will know how to keep track of changes to its data.
It will be destination-aware.
Such a collection would keep track of the data changes made from
the UI. For example, a user modifies the data in a DataGrid
that has a collection of some objects
used as a data provider. You want to make a standard Flex ArrayCollection
a little smarter so that it’ll
automatically create and maintain a collection of ChangeObject
instances for every modified,
new, and deleted row.
We’ve developed a class DataCollection
that will do exactly this
seamlessly for the application developer. This collection also
encapsulates all communications with the server side via RemoteObject
, and it knows how to notify other
users about the changes made by you if they are working with the same
data at the same time.
Shown in Example 6-10, this
collection stores its data in the property source
, the array of ChangeObjects
in modified
, and the name of the remote
destination in destination
. Every
time the data in the underlying collection changes, this collection
catches the COLLECTION_CHANGE
event, and based on
the event’s property kind
(remove
, update
,
add
) removes or modifies the data in the collection.
To support undo functionality, all modified objects are stored in the
properties deleted
and modified
.
Example 6-10. DataCollection.as—take 1
package com.farata.collections { [Event(name="propertyChange", type="mx.events.PropertyChangeEvent")] [Bindable(event="propertyChange")] public class DataCollection extends ArrayCollection { public var destination:String=null; protected var ro:RemoteObject = null; public var deleted:Array = new Array(); public var modified:Dictionary = new Dictionary(); public var alertOnFault:Boolean=true; private var trackChanges:Boolean=true; // The underlying data of the ArrayCollection override public function set source(s:Array):void { super.source = s; list.addEventListener(CollectionEvent.COLLECTION_CHANGE, onCollectionEvent); resetState(); refresh(); } // collection's data changed private function onCollectionEvent(event:CollectionEvent) :void { if (!trackChanges) return; switch(event.kind) { case "remove": for (var i:int = 0; i < event.items.length; i++) { var item:Object = event.items[i]; var evt:DynamicEvent = new DynamicEvent("itemTracking"); evt.item = item; dispatchEvent(evt); if (evt.isDefaultPrevented()) break; var co:ChangeObject = ChangeObject(modified[item]); var originalItem:Object=null; if (co == null) { // NotModified originalItem = item; } else if (!co.isCreate()) { // Modified originalItem = co.previousVersion; delete modified[item]; modifiedCount--; } else { // NewModified delete modified[item]; modifiedCount--; } if (originalItem!=null) { deleted.push(originalItem); deletedCount = deleted.length; }; } break; case "add": for ( i = 0; i < event.items.length; i++) { item = event.items[i]; evt = new DynamicEvent("itemTracking"); evt.item = item; dispatchEvent(evt); if (evt.isDefaultPrevented()) break; modified[item] = new ChangeObject (ChangeObject.CREATE, cloneItem(item), null); modifiedCount++; } break; case "update": for (i = 0; i < event.items.length; i++) { item = null; var pce:PropertyChangeEvent = event.items[i] as PropertyChangeEvent; if ( pce != null) { item = pce.currentTarget; //as DTO; if( item==null ) item = pce.source; evt = new DynamicEvent("itemTracking"); evt.item = item; dispatchEvent(evt); if (evt.isDefaultPrevented()) break; } if (item != null) { if(modified[item] == null) { if (item.hasOwnProperty("properties")) { var oldProperties:Dictionary = item["properties"]; oldProperties[pce.property] = pce.oldValue; var previousVersion:Object = cloneItem(item, oldProperties) } else { previousVersion = ObjectUtil.copy(item); previousVersion[pce.property] = pce.oldValue; } modified[item] = new ChangeObject(ChangeObject.UPDATE, item, previousVersion); modifiedCount++; } co = ChangeObject(modified[item]); if (co.changedPropertyNames == null) { co.changedPropertyNames = []; } for ( i = 0; i < co.changedPropertyNames.length; i++ ) if ( co.changedPropertyNames[i] == pce.property) break; if ( i >= co.changedPropertyNames.length) co.changedPropertyNames.push(pce.property); } } break; } // to be continued }
For our DataCollection
to
really be useful for developers, it has to offer an API for querying and
manipulating its state. Developers should be able to query the
collection to find out whether this particular object is new, updated,
or removed. The modified
variable of
DataCollection
is a reference to
ChangeObject
’s, and each ChangeObject
instance can “introduce” itself
as new, updated, or removed. Hence we are adding the methods listed in
Example 6-11 to the DataCollection
.
Example 6-11. Adding more methods to DataCollection
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: 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; }
The DataCollection
can “tell”
if any of its objects are new, removed, or updated; keeps the counts of
modified and deleted objects; and knows if a commit (saving changes) is
required.
All the changes are accessible as the properties deletes
, inserts
, and updates
. The property changes
will get you the entire collection of
the ChangeObject
s (Example 6-12).
Example 6-12. Adding more properties to DataCollection
public function get changes():Array { var args:Array = deletes; for ( var item:Object in modified) { var co: ChangeObject = 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; }
This collection should also take care of the communication with
the server and call the fill()
and
sync()
methods. Because the DataCollection
internally uses Flex remoting,
it’ll create the instance of the RemoteObject
with result and fault
handlers.
The application developer will just need to create an instance of
DataCollection
, then specify the name
of the remote destination and the remote method to call for data
retrieval and update.
As you saw in Example 1-27:
collection = new DataCollection(); collection.destination="com.farata.Employee"; collection.method="getEmployees"; ... collection.fill();
The fill()
method here invokes
the remote method getEmployees()
. If
the sync()
method is not specified,
its default name will be getEmployees_sync()
. After the code fragment
in Example 6-13 is added
to DataCollection
, it’ll be able to
invoke a remote object on the server after creating the instance of
RemoteObject
in the method createRemoteobject()
. The method
fill()
calls invoke()
, which in turn creates an instance of
the remote method using getOperation()
on the remote object.
Example 6-13. Adding destination awareness to DataCollection
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; return act; } } }
Let’s recap what you’ve done. You subclassed ArrayCollection
and created the DataCollection
class that remembers
all the changes to the underlying collection in the form of ChangeObject
instances. Each ChangeObject
“knows” if it’s there because the
user modified, removed, or added a new object to the collection. The
DataCollection
internally creates a
RemoteObject
based on the name of the
destination and calls the sync()
method, passing the collection of ChangeObject
s to it for persistence on the
server. Data retrieval is performed by calling DataCollection.fill()
.
52.15.160.43