8. Client-server data management
Managed entity state
The Tide framework includes a client-side entity cache where each managed entity exists only once for each Tide context.
A particular entity instance can be in two states :
- Stable: the instance has not been modified until it was received from the server
- Dirty : the instance has been modified
The current entity state can be accessed with :
entity.meta_dirty
The meta_dirty property is bindable, so it could be used for example to enable/disable a button.
A dirty state is also available at the context level, so context.meta_dirty can be accessed to determine if any entity in the context has been modified locally since the last server update.
<mx:Button label="Save" click="entityService.save()" enabled="{tideContext.meta_dirty}"/>
Note that this dirty state is only reliable when using optimistic locking. This is the only way for Tide to know that an entity instance has been actually updated on the server because its version number has increased.
In a typical client/server interaction, here is what happens :
1. The Flex application retrieves an entity instance, for example with a version number 0. This instance is considered stable.
2. The user modifies data on the client, possibly with bidirectional data binding. The version number stays 0, the client state becomes dirty.
3. The user clicks on a save button. The Flex application calls a service and retrieves the result. The server has incremented the version number to 1, so Tide overwrites the cached instance on the client and it is considered as stable again.
The main issue with this programming model is to implement a cancel button after 2. If you use bidirectional data binding, the client view of the entity instance has already become dirty. Tide provides a simple way of restoring the last stable state :
private function restore():void { Managed.resetEntity(entity); }
Conflict detection and resolution
If you look at the previous process in 3 steps, we assume that nobody else has changed the data the user is working on between 1 and 3. In highly concurrent environments with read-write data, there are possibilities that someone else has modified the entity on the server between step 1 and step 3.
In this case, there are two cases : either you just rely on optimistic locking, or use data push (see next chapter) so all Flex clients are updated in near real-time.
With normal optimistic locking, the remote service call at step 3 will trigger a OptimisticLockException. Tide provides a built-in exception handler to handle this case : it will extract the entity argument of the exception, compare its state with the client state and dispatch a conflict event TideDataConflictEvent on the Tide context when it's not identical. The exception handler can be enabled with :
Tide.getInstance().addExceptionHandler(OptimisticLockExceptionHandler);
When data push is used, an entity instance can be updated with data received from the server at any time. If the current user was working on this instance, it is obviously not desirable that his work is overwritten without notice. So similarly to the previous case, Tide will determine that an incoming data from another user session is in conflict with the local data and dispatch a TideDataConflictEvent on the Tide context.
What can you do with this event ? Basically there are two possibilities : accept the server-side state or keep the client state. Here is an example of a conflict handler defined in a Flex application, generally in the main mxml :
<mx:Application ...
preinitialize="Tide.getInstance().initApplication()"
creationComplete="init();">
<mx:Script>
<![CDATA[
private function init():void {
Tide.getInstance().getEjbContext().addEventListener(
TideDataConflictsEvent.DATA_CONFLICTS, conflictsHandler);
}
private var _conflicts:Conflicts;
private function conflictsHandler(event:TideDataConflictsEvent):void {
_conflicts = event.conflicts;
Alert.show("Keep local state ?", "Data conflict",
Alert.YES|Alert.NO, null, conflictsCloseHandler);
}
private function conflictsCloseHandler(event:CloseEvent):void {
if (event.detail == Alert.YES)
_conflicts.acceptAllClient();
else
_conflicts.acceptAllServer();
}
]]>
</mx:Script>
...
</mx:Application>
If you look at the ASDoc for Conflicts, there are a few properties that give more details about the conflicts and make possible to present a better alert message to the user.
Integration with client conversations
When using client conversations, the situation becomes a bit more complex as each conversation has its own entity cache. That means that when a user modifies some data in a conversation context, the change is not immediately visible in others even if they contain the same entity instance.
The Tide context object provides a few methods to deal with such situations :
tideContext.meta_mergeInParentContext()
This will merge all stable state of the current conversation context in its parent context and all its children. In the common case where you don't use nested conversations, that just means that the changes are merged in the global context as well as all other conversations.
If you use nested conversations, the merge will be done only in the direct parent context and all its children, but not in the global context.
tideContext.meta_mergeInGlobalContext()
This will merge all stable state of the current conversation context in its parent context, in the global context and in all children contexts recursively.
