William has posted 6 posts at DZone. View Full User Profile

Data-centric Flex Development with the Tide framework

11.05.2009
| 7794 views |
  • submit to reddit

Flex provides various means of exchanging data with a remote server that virtually allows you to integrate a Flex application with any kind of server technology. It's quite easy to retrieve some data from a server, bind it to UI components and send back the modified data to the server, all this using RemoteObject, Web services or any other technique. So why would you want to bother with yet another layer or framework ?

To illustrate, I will start by a simple Flex application. The application displays two lists : a list of people and a list of friends that is a subset of the first list. A search button simulates a refresh of the 'all people' list by updating its data provider, exactly as if it had been deserialized by a Flex RemoteObject from a remote service result.

First we define the data object :

[Bindable]
[RemoteClass(alias="com.myapp.Person")]
public class Person
{
public var personId:Number;
public var firstName:String;
public var lastName:String;
}

And then the application :

<mx:Application 
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns="*"
layout="absolute">

<mx:Script>
<![CDATA[
private function search():void {
dgAll.dataProvider = allPeopleReceivedFromServer;
}
]]>
</mx:Script>

<mx:VBox>
<mx:Button label="Search" click="search()"/>

<mx:HBox>
<mx:Panel title="All People">
<mx:List id="dgAll" dataProvider="{allPeopleList}"
fontSize="18" height="300"/>
</mx:Panel>

<mx:Panel title="Friends" width="50%">
<mx:List id="dgFriends" dataProvider="{friendsList}"
fontSize="18" height="300"/>
</mx:Panel>
</mx:HBox>
</mx:VBox>

<Person id="p1" personId="1" firstName="John" lastName="Smith"/>
<Person id="p2" personId="2" firstName="Jean" lastName="Dupont"/>
<Person id="p3" personId="3" firstName="Gianni" lastName="Ferrari"/>
<Person id="p4" personId="4" firstName="Johan" lastName="Schmidt"/>

<mx:ArrayCollection id="allPeopleList" source="{[p1, p2, p3, p4]}"/>

<mx:ArrayCollection id="friendsList" source="{[p2]}"/>

<mx:ArrayCollection id="allPeopleReceivedFromServer">
<Person personId="1" firstName="John" lastName="Smith"/>
<Person personId="5" firstName="Juan" lastName="Lopez"/>
<Person personId="2" firstName="Jean" lastName="Dupond"/>
<Person personId="4" firstName="Johan" lastName="Schmidt"/>
</mx:ArrayCollection>
</mx:Application>

This is probably neither the most interesting nor the most complex Flex application ever written, but still it's a rather common use case. Here is what it looks like before clicking on 'Search' and after having selected 'Jean Dupont' in the left list :

And here is what it looks like after a click on 'Search' :

Or try it yourself with the swf here.

Two things can be noticed immediately :

  • The selected row of the 'All people' list is always lost after the click on 'Search', yet 'Jean Dupond' is still displayed in the list.
  • 'Jean Dupond' is displayed with a 'd' in the left list because it has been updated from the server, but is still displayed with a 't' in the right list. However it is supposed to be the same person.
  •  

    We can hardly think of a simpler application, but we already have a inconsistency on the screen after one click. Imagine a product having different prices in different parts of the screen, or other critical inconsistencies confusing your end users or breaking your business logic.

    So what happened here ?

    Remoting consists in serializing and deserializing objects in a particular format to transmit them on the network. The deserialization process always creates new instances of the received objects, so if you call the same RemoteObject operation ten times, you will get ten different instances of the same result object. This is what the example simulates by returning the ArrayCollection 'allPeopleReceivedFromServer' containing different instances of the same persons.

    After we update the data provider of the left list with the collection received from the RemoteObject, the person instances in the left list become different than those in the right list, even when they represent the same thing.

    Although we have used data binding between the lists and the collections, it is not able to detect that the person has changed any more. Data binding works by listening PropertyChangeEvents on object instances and updating properties of other objects accordingly. As the source instances are now different, there is no way data binding will be able to trigger the changes correctly on the target objects.

     

    What can we do to fix this ?

    The first obvious solution is to refresh the right list by issuing another remote call. It seems a big waste of resouces though, as the user never requested a update of its friends list, and in general it is always better to limit the number of remote calls. Also if the person was displayed in many other places, we would have to track all these places and update them correctly.

     

    If you think about this, that would be a lot easier if we could keep the initial instance of 'Jean Dupont' and simply update it with the new server data. Let's try a different implementation of the 'search' method (still simulating that we handle the result of a remote call) :

    private function search():void {
    var coll:IList = IList(dgAll.dataProvider);
    for (i = 0; i < coll.length; i++) {
    var e:Object = coll.getItemAt(i);
    var found:Boolean = false;
    for (j = 0; j < allPeopleReceivedFromServer.length; j++) {
    var me:Object = allPeopleReceivedFromServer.getItemAt(j);
    if (me.personId == e.personId) {
    found = true;
    break;
    }
    }
    if (!found)
    coll.removeItemAt(i--);
    }
    for (var j:int = 0; j < allPeopleReceivedFromServer.length; j++) {
    me = allPeopleReceivedFromServer.getItemAt(j);
    found = false;
    for (var i:int = 0; i < coll.length; i++) {
    e = coll.getItemAt(i);
    if (me.personId == e.personId) {
    e.firstName = me.firstName;
    e.lastName = me.lastName;
    found = true;
    break;
    }
    }
    if (!found)
    coll.addItemAt(me, j);
    }
    }

    Basically we merge the received collection into the existing collection, by removing, adding or updating its elements. This is obviously a bit more complex, but this method could relatively easily be refactored as a reusable utility method.

    If you run the previous example after this change, you can check that the left list does not lose its selection any more, and that the instance in the right list is correctly updated.

    As additional benefits of this solution, we gain a slightly better performance because only modified data are updated on the display (though it's not really visible with only four rows), and we could even apply a nice looking data effect on the list :

        <mx:List id="dgAll" dataProvider="{allPeopleList}" 
    variableRowHeight="true" fontSize="18" width="100%" height="300"
    itemsChangeEffect="{DefaultListEffect}"/>

    The new swf here

    That's not so bad for a first step, but we have overlooked an important thing. We have started from a clean state where the two lists initially shared the same person instances. In a real application, these two lists would have been initialized by two different server calls, and thus would have already contained two different instances of our 'Jean Dupond', so our fix would have changed nothing.

    What about a better solution ?

    Ideally we would have to keep a local cache of all the entity instances that we have already received. Each time we get data from the server, we match it against the cache and update the existing instance when there is one instead of using the new one.

    Once we can ensure that we have a unique local instance for each 'physical' object instance, it becomes highly unlikely that we get inconsistencies due to partial refreshes, and we can safely use data binding everywhere to display our data.

    Such a cache implementation is at the core of the data management part of the Tide framework, so we are going to see how to use it here.

    The main requirement of Tide is that data classes have to implement mx.core.IUID and provide a correct implementation of the uid property. Correct simply means that two instances of the same physical object should have the same uid. This uid property is in fact not specific to Tide and is used in many places in the Flex framework, but Tide heavily relies on it as it serves as the key in the data cache. A trivial implementation could make use of the database identifier to build the uid value (though it's not perfect, it's at least what the Flex 3 documentation suggests, see here).

    So let's modify the Person class :

    [Bindable]
    [RemoteClass(alias="com.myapp.Person")]
    public class Person implements IUID
    {
    public var personId:Number;
    public var firstName:String;
    public var lastName:String;

    public function set uid(uid:String):void {
    }
    public function get uid():String {
    return "person:" + personId;
    }
    }

    And the application :

    <mx:Application 
    xmlns:mx="http://www.adobe.com/2006/mxml"
    xmlns="*"
    layout="absolute"
    creationComplete="init()">

    <mx:Script>
    <![CDATA[
    import org.granite.tide.Context;
    import org.granite.tide.Tide;
    import mx.collections.IList;
    import mx.effects.DefaultListEffect;

    public var _context:Context = Tide.getInstance().getContext() as Context;


    private function init():void {
    _context.meta_mergeExternalData(allPeopleList, dgAll.dataProvider);
    _context.meta_mergeExternalData(friendsList, dgFriends.dataProvider);
    }

    private function search():void {
    _context.meta_mergeExternalData(allPeopleReceivedFromServer, dgAll.dataProvider);
    }

    private function nameLabel(person:Person):String {
    return person.firstName + " " + person.lastName;
    }
    ]]>
    </mx:Script>

    <mx:VBox>
    <mx:HBox>
    <mx:Button label="Search" click="search()"/>
    </mx:HBox>

    <mx:HBox width="500">
    <mx:Panel title="All people" width="50%">
    <mx:List id="dgAll"
    variableRowHeight="true" fontSize="18" width="100%" height="300"
    labelFunction="nameLabel"
    itemsChangeEffect="{DefaultListEffect}">
    <mx:dataProvider>
    <mx:ArrayCollection/>
    </mx:dataProvider>
    </mx:List>
    </mx:Panel>

    <mx:Panel title="Friends" width="50%">
    <mx:List id="dgFriends"
    variableRowHeight="true" fontSize="18" width="100%" height="300"
    labelFunction="nameLabel"
    itemsChangeEffect="{DefaultListEffect}">
    <mx:dataProvider>
    <mx:ArrayCollection/>
    </mx:dataProvider>
    </mx:List>
    </mx:Panel>
    </mx:HBox>
    </mx:VBox>

    <mx:ArrayCollection id="allPeopleList">
    <Person personId="1" firstName="John" lastName="Smith"/>
    <Person personId="2" firstName="Jean" lastName="Dupont"/>
    <Person personId="3" firstName="Gianni" lastName="Ferrari"/>
    <Person personId="4" firstName="Johan" lastName="Schmidt"/>
    </mx:ArrayCollection>

    <mx:ArrayCollection id="friendsList">
    <Person personId="2" firstName="Jean" lastName="Dupont"/>
    </mx:ArrayCollection>

    <mx:ArrayCollection id="allPeopleReceivedFromServer">
    <Person personId="1" firstName="John" lastName="Smith"/>
    <Person personId="5" firstName="Juan" lastName="Lopez"/>
    <Person personId="2" firstName="Jean" lastName="Dupond"/>
    <Person personId="4" firstName="Johan" lastName="Schmidt"/>
    </mx:ArrayCollection>
    </mx:Application>

    Compiling and running this application requires to include the granite.swc library from the GraniteDS 2.1 RC1 distribution here http://sourceforge.net/projects/granite/files/granite/granite-2.1.0.RC1/graniteds-2.1.0.RC1.zip/download.

    To properly simulate remote calls, the two lists are initialized with empty collections. Then all the work is done by the method Context.meta_mergeExternalData. As its name suggests, it merges the server data in the local cache, reusing caches entity instances when available.

    Finally here is the last version of the application using the Tide remoting API, that already integrates this merge process :

    <mx:Application 
    xmlns:mx="http://www.adobe.com/2006/mxml"
    xmlns="*"
    layout="absolute"
    preinitialize="Tide.getInstance().initApplication()"
    creationComplete="init()">

    <mx:Script>
    <![CDATA[
    import mx.collections.IList;
    import mx.effects.DefaultListEffect;
    import org.granite.tide.Context;
    import org.granite.tide.Tide;
    import org.granite.tide.TideResponder;
    import org.granite.tide.remoting.RemoteObjectProxy;

    [In]
    public var peopleService:RemoteObjectProxy;


    private function init():void {
    peopleService.findAllPeople(new TideResponder(null, null, null, allPeopleList));
    peopleService.findFriends(new TideResponder(null, null, null, friendsList));
    }

    private function search():void {
    peopleService.findAllPeople(new TideResponder(null, null, null, allPeopleList));
    }

    private function nameLabel(person:Person):String {
    return person.firstName + " " + person.lastName;
    }
    ]]>
    </mx:Script>

    <mx:VBox>
    <mx:HBox>
    <mx:Button label="Search" click="search()"/>
    </mx:HBox>

    <mx:HBox width="500">
    <mx:Panel title="All people" width="50%">
    <mx:List id="dgAll"
    variableRowHeight="true" fontSize="18" width="100%" height="300"
    labelFunction="nameLabel"
    itemsChangeEffect="{DefaultListEffect}">
    <mx:dataProvider>
    <mx:ArrayCollection id="allPeopleList"/>
    </mx:dataProvider>
    </mx:List>
    </mx:Panel>

    <mx:Panel title="Friends" width="50%">
    <mx:List id="dgFriends"
    variableRowHeight="true" fontSize="18" width="100%" height="300"
    labelFunction="nameLabel"
    itemsChangeEffect="{DefaultListEffect}">
    <mx:dataProvider>
    <mx:ArrayCollection id="friendsList"/>
    </mx:dataProvider>
    </mx:List>
    </mx:Panel>
    </mx:HBox>
    </mx:VBox>
    </mx:Application>

    A more complete implementation is available here with the sources here.

    Associated to data binding, the Tide entity cache is very powerful and can be used to transparently update the displayed data from any external source. The merge method can be applied to all result handlers of RemoteObject, but also makes extremely easy to update the display in near real-time from data objects received asynchronously from a messaging channel.

    The Tide framework provides much more than this data cache, like paging or concurrent updates detection, but everything is based on this foundation. This is also a very different approach than Adobe LiveCycle Data Services or other Flex data services solutions.

    Contrary to these solutions that completely handle persistence from the client application to the database and bypass the service layer, Tide lets all persistence, transactions and security aspects to your service layer and simply ensures that the client has a clean and consistent view of the data model.

    It can be used with any server side technology supporting the AMF protocol, though obviously it is even more integrated when using GraniteDS as a remoting/messaging provider with a JEE/JPA backend.

    Published at DZone with permission of its author, William Draï.

    (Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

    Comments

    devaraj ns replied on Mon, 2009/11/30 - 7:03am

    i need a clean version of GraniteDS implementation with Flex through which i can use Comet for Data Push. can you post a source on your blog or send me the source my mail id is nsdevaraj [at] gmail [dot] com

    Comment viewing options

    Select your preferred way to display the comments and click "Save settings" to activate your changes.