Section 15.1. Shared Object Management

15.1. Shared Object Management

Object-oriented programmers are used to designing a set of custom classes to work together to get some job done. Each class defines objects that have their own responsibilities and collaborate with other types of objects. The state of all the objects represents the current state of an application. When I first came across shared objects, I wasn't sure how to approach working with them. They are clearly designed to share application state across Flash movies, but I encountered problems when I tried to imagine giving each shared object separate responsibilities. You can't extend the SharedObject class the way you can extend other classes like Object or MovieClip . For example, you can't get this to work:

 class VideoConferenceManager extends SharedObject {   //... } 

As I've already pointed out in Chapter 8, you can't extend the SharedObject class, because the only way to get a remote shared object is to use the SharedObject.getRemote( ) method:

 remote_so = SharedObject.getRemote("UsersData", nc.uri); 

Once I get a shared object, I can customize it by dynamically adding methods and private properties to it. But I wanted to design classes of interacting objects.

Eventually, I realized that shared objects could be treated like distributed associative arrays that are managed by other objects. When it comes to designing applications with shared objects, the challenge is writing classes that manage shared objects to get your job done.


To make a shared object part of an application, it should be controlled or contained by another object. The controlling object provides the custom behavior needed by the application and often hides the shared object within it. The object that contains or controls a shared object can treat the shared object as an internal data collection that it is solely responsible for managing. In general, one, and only one, object within each client should be responsible for updating a shared object or sending messages using it. When server-side scripting is involved, one object on the server should be responsible for managing server-side updates. However, in some cases, more than one object may need to access data in a shared object or receive notifications when a remote method is called on the shared object. In this section, I review some techniques for working with shared objects and deal with some difficult problems such as populating List and DataGrid components with shared object data. When I'm done, I hope you'll be as comfortable designing applications that use shared objects as you are using arrays and movie clips.

15.1.1. One-to-One Owner/SharedObject Relationships

Let's start with the simplest case: we'll give a single object responsibility for updating and responding to updates on a shared object as the SharedBall class did in Chapter 8. In this more general example, a client-side object can create a shared object, customize it by adding properties and methods to the shared object itself (not the data property of the shared object), and then connect it. We often refer to the object that customizes its shared object as the shared object's owner . Here is a short skeletal example of a demonstration Owner class:

 class Owner {   var nc:NetConnection;   var so:SharedObject;   function Owner (p_nc) {     nc = p_nc;     so = SharedObject.getRemote("soName", nc.uri, false);     so.owner = this;     so.onSync = function (list) {       this.owner.onSync(list);     };     so.onStatus = function (status) {       this.owner.onStatus(status);     };     so.connect(nc);   }   function onSync (list) {     trace("onSync called with list length: " + list.length);   }   function onStatus (status) {     trace("onStatus called with status code: " + status.code);   } } 

The Owner object makes sure it receives onSync( ) and onStatus( ) method calls by adding an owner property to the shared object that refers back to itself (using the keyword this ). Then it adds onSync( ) and onStatus( ) methods to the shared object, which use the owner property to call the owner's onSync( ) and onStatus( ) methods. In other words, no real work is done within the shared object. It simply passes on status and synchronization information to its owner for processing. The owner object will also be responsible for updating the shared object, but that is not shown in the preceding example.

The example uses a temporary shared object. Since a temporary shared object cannot be updated until it has been synchronized by the server, the owner needs to know when the shared object is first synchronized. It is not difficult to extend the shared object to call a method on the owner object to indicate the shared object is ready to be updated. In the following example, the shared object is modified to call the owner's onConnect( ) method when the shared object is synchronized for the first time (additions to the preceding example are shown in bold):

 class Owner {   var nc:NetConnection;   var so:SharedObject;   function Owner (p_nc) {     nc = p_nc;     so = SharedObject.getRemote("soName", nc.uri, false);  so.isConnected = false;  so.owner = this;     so.onSync = function (list) {  if (!this.isConnected) {   this.isConnected = true;   this.owner.onConnect( );   }  this.owner.onSync(list);     };     so.onStatus = function (status) {       this.owner.onStatus(status);     };     so.connect(nc);   }  function onConnect (list) {   trace("onConnect called.");   }  function onSync (list) {     trace("onSync called with list length: " + list.length);   }   function onStatus (status) {     trace("onStatus called with status code: " + status.code);   } } 

The owner object knows it is safe to update the shared object as soon as the onConnect( ) method is called. For example, it may enable components so that a user can start updating a shared object when it receives the onConnect( ) call:

 function onConnect(  ) {   deleteButton.enabled = true;   updateButton.enabled = true; } 

The owner's onSync( ) method will also be called when the shared object is synchronized for the first time.

Often no further customization of the shared object is necessary. And that's what we want! Now, we are free to define the responsibilities of our Owner class or its subclasses, without having to further customize the shared object the owner contains.

The owner object in each client can update its shared object and will be notified when the shared object is updated by other clients . If the owner object must interpret the information objects in the list array passed into the shared object's onSync( ) method, it will do so in its own onSync( ) method.

Developers working with shared objects often must write onSync( ) methods that loop though each information object in the array passed into onSync( ) . Returning to the SharedBall example in Chapter 8, each information object had to be examined to see if another movie had moved the ball. Here's a short snippet from Example 8-4:

 for (var i in list) {   if (list[i].code == "change"  list[i].code == "reject") {     this.owner.onChange(  );     break;   } } 

If you have to do something often enough, such as writing code to loop through a list, it's only natural to try to find a way out of writing yet another loop. Some developers, therefore, would rather not have the owner object implement its own onSync( ) method. They would prefer the shared object's onSync( ) method do the work of looping through the list of information objects and pass each information object to the owner for further processing. Since the code property of each information object is almost always the first thing to be checked in an onSync( ) method, it's easy to imagine calling owner methods named after possible code values: "change", "success", "reject", "delete", and "clear". In some cases, this practice can increase the number of function calls required to handle changes and make it harder to update List and DataGrid components efficiently . However, it has the advantage of providing slot-level notifications of changes to the owner object.

The following example shows one of many possible ways to notify an owner object of shared object changes:

 so.onSync = function (list) {   if (!this.isConnected) {     this.isConnected = true;     this.owner.onConnect( );   }   var len = list.length;   for (var i = 0; i < len; i++) {     var code = list[i].code;     switch (code) {       case "change":         this.owner.onSlotChanged(list[i]);         break;       case "success":         this.owner.onSlotChanged(list[i]);         break;       case "reject":         this.owner.onSlotChanged(list[i]);         break;       case "delete":         this.owner.onSlotDeleted(list[i]);         break;       case "clear":         this.owner.onClearSlots( );         break;     }   } }; 

As in the earlier example, the shared object's onSync( ) method still calls the owner's onConnect( ) method to notify it that the shared object is ready to be updated. Instead of just passing the list on to the owner's onSync( ) method, each information object in the list is checked from beginning to end based on its code property. Depending on the code value, one of three methods is called. The first and most frequently called method is onSlotChanged( ) , which is called if a slot has been created or updated by another client. It is also called if a slot change made by the owner succeeded or was rejected. Owner objects that don't want to handle certain codes like "reject" or "success" can still check the information object passed to their onSlotChanged( ) method. The onSlotDeleted( ) method is called when a slot is deleted. The onClearSlots( ) method is called when the server clears (in other words, deletes) all the slots in the shared object. It is essential to pass on and handle "clear" events when persistent shared objects are used that have a resyncDepth value other than the default of -1. Otherwise, the entire contents of the shared object may be deleted without the owner object receiving any notification.

15.1.1.1 Connecting and reconnecting

A potential weakness of all the examples shown so far is that they assume the owner object will connect to the server and to the shared object only once. If the client disconnects and then reconnects to the server, the shared object also has to be reconnected. If the owner object is disposed of and a new owner object is created, the examples presented so far will work without problem. Each time an owner object is instantiated , it is passed a connected NetConnection object that it uses to connect its shared object. But if an owner object cannot be disposed of and re-created, the initialization code for the shared object should be moved out of its constructor. There are lots of ways to inform the owner object that an application has reconnected. One simple way is to call a method and pass it a connected NetConnection object as indicated in bold:

 class Owner {   var __nc:NetConnection;   var so:SharedObject;   function Owner( ) {   }  function set nc (nc:NetConnection) {   _  _nc = nc;   so = SharedObject.getRemote("soName", nc.uri, false);   so.isConnected = false;   so.owner = this;   so.onSync = function (list) {   if (!this.isConnected) {   this.isConnected = true;   this.owner.onConnect( );   }   this.owner.onSync(list);   };   so.onStatus = function (status) {   this.owner.onStatus(status);   };   so.connect(nc);   }  function onConnect ( ) {     // The SO is ready for use. Enable GUI objects if necessary.   }   function onSync (list) {     // Handle shared object updates here.   }   function onStatus (status) {     // Handle shared object errors here.   } } 

In the preceding ActionScript 2.0 example, a setter method is used. The set nc( ) method must be called whenever a NetConnection object is connected or re-created so that the owner object can correctly initialize and connect its shared object using the NetConnection .

In all the examples we've seen so far, each shared object is customized only enough so that it passes on synchronization and status messages to its owner. The owner can be any class we like with any responsibilities we care to give it. Unfortunately, the owner object still has to do the work of setting up its shared object. Soon we'll see how to reduce the setup work that the owner has to do to a minimum.

15.1.2. One-to-Many Owner/Listeners Relationships

Since shared object data has to be sent across the network every time a shared object is updated, you should minimize data duplication as much as possible. For example, if a PeopleGrid component manages a people shared object, other components should not have to duplicate some or all of the data already available in the PeopleGrid's shared object. Somehow, they should be able to get the dataand notifications of changes to the datafrom the people shared object, too.

In other words, while one object should be responsible for updating a shared object, we want to allow more than one object to receive updates as the shared object is changed. In short, we want the SharedObject to become an event broadcaster . A number of techniques have been used to make objects into event broadcasters in Flash. You can use the ASBroadcaster object, write your own custom code, or, as of Flash MX 2004, use the mx.events.EventDispatcher mixin class. (A mixin class adds methods and properties to other objects dynamically.) In Server-Side ActionScript (SSAS), you have to write your own broadcaster code. Chapter 13 makes extensive use of the EventDispatcher option. The SharedObjectFactory class listed in Example 13-2 uses EventDispatcher to turn shared objects into event broadcasters. It can be used in the owner/shared object one-to-one scenario. And it can be used to allow more than one object to receive events and remote method calls from a shared object. In each case, it is essential that one and only one object in each client uses the SharedObjectFactory class to get a reference to a shared object. Other objects that need access to synchronization messages and remote methods must get a reference to the shared object from their owners and add themselves as listeners. Looking back at Chapter 13, here's how the VideoConference component gets access to the people_so shared object maintained by a PeopleGrid component:

 peopleGrid.nc = application.nc; videoConference.userName = application.user.userName; videoConference.people_so = peopleGrid.people_so; videoConference.nc = application.nc; 

The PeopleGrid sets up the shared object using the SharedObjectFactory class:

 __people_so = SharedObjectFactory.getRemote(path + "people", nc.uri); __people_so.addEventListener("onSync", this); __people_so.connect(__nc); 

When the VideoConference component gets a reference to the people_so shared object, it only has to set itself up as a listener in order to receive any new onSync events:

 __people_so.addEventListener("onSync", this); 

However, in some cases, it is possible that the listener will set itself up to listen to the shared object too late to receive some of the first onSync( ) calls or remote method calls. In that case, each listener can check whether the shared object has already been connected by checking its isConnected property and initialize itself accordingly :

 if (__people_so.isConnected) {   var list = [];   for (var p in __people_so.data) {     list.push({code:"change", name: p});   }   onSync({target:__people_so, list:list}); } 

The isConnected property is added by the SharedObjectFactory class and is maintained by the modified connect( ) and close( ) methods that it attaches to each shared object before returning it for use. In other words, the SharedObjectFactory class returns an already customized shared object that should not be further modified. See Example 13-3 for the listing of a test program that exercises all the features of a shared object returned from the SharedObjectFactory class. The earlier caution about connecting and reconnecting owner objects applies to shared objects as well. The isConnected property will not be reset if a NetConnection closes . Your application must have the component call connect( ) on the shared object again when the NetConnection is reestablished.

To keep your code as simple as possible, use one and only one technique for getting and customizing shared objects. If you are using Flash MX 2004 or Flash Pro, consider using SharedObjectFactory exclusively to get every shared object regardless of how you intend to use each one.


What should you do if you need to broadcast synchronization events in ActionScript 1.0? There are a number of possibilities. An extremely simple approach is to modify SharedObject.prototype . Example 15-1 shows one way to customize the SharedObject prototype so that you can still use getRemote( ) to get a shared object you could use normally or as a broadcaster after calling its init( ) method.

Example 15-1. Customizing the SharedObject class
 SharedObject.prototype._onSyncConnect = function (list) {   this.notify("onSyncConnect", list);   this.onSync = this._onSync;   this.onSync(list); }; SharedObject.prototype._onSync = function (list) {   this.notify("onSync", list); }; SharedObject.prototype.init = function ( ) {   this._mcListeners = {};   // Must have unique _name property.   this.onSync = this._onSyncConnect; }; SharedObject.prototype.notify = function (method, list) {   for (var p in this._mcListeners) {     var mc = this._mcListeners[p];     if (mc[method]) {       mc[method](this, list);     }   } }; SharedObject.prototype.addListener = function (mc) {   this._mcListeners[mc._name] = mc; }; SharedObject.prototype.removeListener = function (mc) {   delete this._mcListeners[mc._name]; }; 

The code in Example 15-1 relies on the _mcListeners object to keep track of each listener using its _name property. If each listener does not have a unique _name property, the code will not work. If there is any chance that listeners will not have a unique _name property, consider using the server-side port of the EventDispatcher class provided in Example 14-9.

However, if each listener has a unique property, a modified version of Example 15-1 can be used to provide an event broadcast mechanism. For example, if each object has a unique path property, just change _name to path in Example 15-1 to create a server-side version of the script.

Using the SharedObjectFactory class, or something similar you create, simplifies writing classes that contain their own shared objects. Look at how little work the PeopleGrid had to do to set up the people shared object:

 __people_so = SharedObjectFactory.getRemote(path + "people", nc.uri); __people_so.addEventListener("onSync", this); __people_so.connect(__nc); 

Compare that to the 14 or more lines of code in some of the earlier examples. Even better, using SharedObjectFactory means each shared object will be customized in the same way and make available one well-defined set of events.

15.1.3. Delegating Updates

Many components have to manage a lot of objects. For example, a PeopleCursors component has to manage a separate cursor for each user. In situations like that, it is often a good idea to have an owner of a shared object delegate the responsibility of updating the shared object to other objects under its control. The PeopleCursors component and similar components are a good candidate for delegation. For example, the PeopleCursors component allows each user to show everyone else where his cursor is. Each user can either hide or show his cursor at any time, and his username will appear below it. Also, not only should his cursor be visible to everyone else, but also the same movie clip representation of his cursor should follow around his system cursor so he has some indication of what other people are seeing.

How to design such a component? Assuming the changing position of each cursor is to be stored in a shared object, you have to immediately choose if you are going to create a shared object for each cursor or use one shared object for all of them. In some cases, it is simpler to use one shared object for all the cursors . If you don't, you need some mechanism for discovering the existence of each separate shared object before connecting to it. Often that requires the use of another shared object with the name of each user in it. The VideoConference component in Chapter 13 makes use of both the people shared object and a shared object for each stream.

The PeopleCursors component must manage a cursors shared object and any number of PersonalCursor movie clips. It must also provide a button that allows each user to show or hide her cursor. When a user toggles the button on, an entry in the cursors shared object must be created for her cursor, containing its current position. When the button is toggled off, the entry must be deleted. When an entry appears in the cursors shared object, a movie clip representing that cursor must be created in each PeopleCursors component. When an entry is deleted, the movie clip must also be deleted. The name of each slot in the cursors shared object will be the username of the person who has made her cursor visible as will the name of the movie clip that follows her cursor.

Two questions naturally arise from this scheme. Should the owner object running in each client be responsible for getting the current mouse position and updating the position of its user's mouse, or should updates be the job of the PersonalCursor movie clip that represents the user? Furthermore, should each PersonalCursor movie clip be a listener on the cursors shared object so that it knows where it should move when its slot is updated, or should the PeopleCursors component listen and then tell each clip where to move?

In truth, either of these options can work. However, for simplicity and performance, it is better to delegate to the PersonalCursor clip the job of updating the shared object and have the PeopleCursors component listen for cursor position changes. Here's why: The PeopleCursors component is really a coordinator . Its job should be to create the context for the PersonalCursor clips to work and then create or destroy PersonalCursor clips as needed. The PeopleCursors component connects to the cursors shared object and must listen for synchronization events so it knows what PersonalCursor clips to create and delete. Also, when the user clicks the Show/Hide Cursor button, the PeopleCursors component is responsible for creating the user's PersonalCursor clip.

On the other hand, the PersonalCursor clips should be able to create and respond to cursor-level events like moving around the Stage. So from a division of responsibilities perspective, there is nothing wrong with the owner delegating update responsibilities to another object under its control. But then why not allow each cursor to listen directly to the shared object so it can move around in response to changes in its position within the shared object? The answer is that it is inefficient. If many cursors are moving at the same time, a long list may be returned to the onSync( ) method containing many information objects that represent changes. It is not efficient for each PersonalCursor to do a linear search in the list to see if it has changed. It is much more efficient to have the owner check the list once and tell any PersonalCursor clips that have to move where to go.

Example 15-2 shows the complete listing for the PeopleCursors component. This client-side ActionScript 2.0 class should be stored in PeopleCursors.as .

Example 15-2. The PeopleCursors component
 import mx.controls.Button; import com.oreilly.pfcs.SharedObjectFactory; class com.oreilly.pfcs.framework.components.PeopleCursors extends       com.oreilly.pfcs.framework.PFCSComponent {   // Connect component class and symbol.   var className:String = "PeopleCursors";   static var symbolName:String = "PeopleCursors";   static var symbolOwner:Object = PeopleCursors;   // Default path.   var path:String = "pfcs/PeopleCursors/main/";   // Subcomponents and movie clips.   var showHideButton:Button;   var boundingBox_mc:MovieClip;   var __cursors_so:SharedObject;   var __nc:NetConnection;   var __user:Object;   //   cursors   object holds references to movie clips that represent   // each visible user cursor.   var cursors = {};   var depth:Number = 100;   // Constructor function calls UIComponent's constructor.   function PeopleCursors( ) {     super( );   }   //   init( )   is called before   createChildren( )   and before   onLoad( )   if it exists.   // In this case, we just call   UIComponent.init( )   and hide the bounding box.   function init( ) {     super.init( );     boundingBox_mc._visible = false;     boundingBox_mc._width = boundingBox_mc._height=0;     cursors = {};   }   // No drawing is required, as the button is the entire GUI for this   // component. It just needs to resize.   function draw( ) {     size( );   }   function size( ) {     super.size( );     // Uncomment the following line to have the button change width.     // showHideButton.setSize(width, 22);   }   function createChildren( ) {     var depth = 1;     createObject("Button", "showHideButton", depth++, {_x:0, _y:0});     showHideButton.enabled = false;     showHideButton.toggle = true;     showHideButton.selected = false;     showHideButton.addEventListener("click", this);     showHideButton.label = "Show Cursor";   }   function click (ev) {     if (showHideButton.selected) {       // Create the clip.       showHideButton.label = "Hide Cursor";       addCursor(__user.userName, true);     }     else {       // Delete the clip.       showHideButton.label = "Show Cursor";       removeCursor(__user.userName);     }   }   // The   PeopleCursors.nc( )   setter method uses the   nc   // to get the cursors remote shared object.   public function set nc (nc:NetConnection) {     if (!nc) {       return;     }     __nc = nc;     if (!nc.isConnected) {       trace("PeopleCursors Error: nc must be connected before use.");       return;     }     __user = _global.pfcs.getUser( );     // First get a reference to the cursor list (   cursors   ) shared object.     __cursors_so = SharedObjectFactory.getRemote(path + "cursors", nc.uri);     // Register the PeopleCursors instance as a listener of   onSync   events.     __cursors_so.addEventListener("onSync", this);     __cursors_so.setFps(3);     // Connect it.     __cursors_so.connect(__nc);   }   function onFirstSync( ) {      showHideButton.enabled = true;   }   /*   onSync( )   receives events from the   cursors   shared object and    * responds by adding new cursors, deleting cursors, or asking    * cursors to update themselves. The cursors update the   cursors   * shared object directly when they are moved.    */   function onSync (ev) {     var list = ev.list;     var len = list.length;     for (var i = 0; i < len; i++) {       var code = list[i].code;       var name = list[i].name;       switch (code) {         case "success":          // Tell this user's cursor to update itself.           cursors[name].update( );           break;         case "change":           // Someone else's cursor has moved or been created.           if (!cursors[name]) {             addCursor(name, false);           }           cursors[name].update( );           break;         case "delete":           // Someone else's cursor has been deleted.           if (cursors[name]) {             removeCursor(name);           }           break;         case "clear":            // The temporary shared object is available.           onFirstSync( );           break;        }     }   }   //   addCursor( )   attaches a PersonalCursor movie clip to this component.   function addCursor (name, homeInstance) {     cursors[name] = attachMovie("PersonalCursor", name, depth++,                                  {homeInstance:homeInstance,                                   userName:name, so:__cursors_so,                                   xOffset:Math.floor(_x), yOffset:Math.floor(_y)});   }   //   removeCursor( )   removes a PersonalCursor movie clip from this component.   function removeCursor (name) {     var myCursor = cursors[name];     myCursor.deleteClip( );     delete cursors[name];   }   // Clean up.   function close( ) {     __cursors_so.removeEventListener("onSync", this);   } } 

When the user clicks the showHideButton , his cursor must be either created or destroyed . If you look at the click( ) method, you can see that the button's selected property is used to keep track of the show or hide state and that addCursor( ) and removeCursor( ) are called to actually add and delete the cursor. The addCursor( ) method takes two parameters: name and homeInstance . If the Boolean homeInstance is TRue , this slot represents the current user's cursor, so his PersonalCursor component assumes responsibility for following his cursor and updating the shared object with its position. It will use the name passed into it as the slot name to update. Note that a reference to the shared object is passed into each cursor but is really used by only the homeInstance cursor. When someone else's PersonalCursor is created, an entry for it is created in the cursors shared object. Have a look at the onSync( ) method in Example 15-2. The PeopleCursors component keeps track of each of its PersonalCursor movie clips by name in a cursors object. In the switch statement, the "change" case first checks if a clip exists for a slot that has changed. If not, it creates a cursor, passing false as the homeInstance parameter. In either case, it calls the update( ) method of the clip directly.

Example 15-3 lists the complete client-side ActionScript 2.0 code stored in PersonalCursor.as for the PersonalCursor component.

Example 15-3. The PersonalCursor class
 class com.oreilly.pfcs.framework.components.PersonalCursor extends MovieClip {   // Connect component class and symbol.   var className:String = "PersonalCursor";   static var symbolName:String = "PersonalCursor";   static var symbolOwner:Object = PersonalCursor;   // initObject variables.   var homeInstance:Boolean; // Indicates if this is the user's cursor.   var userName:String;      // The user's username and slot name for this cursor.   var so:SharedObject;      // Shared object to update with the cursor's position.   var xOffset:Number        // Parent clip's x position.   var yOffset:Number        // Parent clip's y position.   // Note: If the parent clip is resized or moved, the xOffset and yOffset values   //       must be updated. Resizing is not provided in this version.   // Subcomponents and movie clips.   var userName_txt:TextField;   function PersonalCursor ( ) {     super( );   }   function onLoad ( ) {     userName_txt.text = userName;     if (homeInstance) {       onMouseMove = function ( ) {         so.data[userName] = {x: _root._xmouse - xOffset,                              y: _root._ymouse - yOffset};       };     }   }   function update( ) {     var cursor = so.data[userName];     _x = cursor.x;     _y = cursor.y;   }   function deleteClip( ) {     onMouseMove = null;     delete so.data[userName];     this.removeMovieClip( );   }   function setOffsets(x, y) {     xOffset = x;     yOffset = y;   } } 

There are additional advantages to the division of responsibility demonstrated in Example 15-2 and Example 15-3. If the cursor's behavior must change, it can be coded in the PersonalCursor class without touching the PeopleCursors class code at all. In fact, I'll do exactly that in Chapter 17 in order to compensate for network latency.

15.1.4. Slot Owners

In the PeopleCursors example, each user has only one cursor. What do you do for a chess game in which each user must control many different pieces? In fact, in many computer games , more than one clip may be under the control of each user. In these cases, the idea of delegating updates can be taken further. Each clip can be designed to update its own slot in a shared object with information such as its current position on the game board. The key to making it work is making sure each clip knows the name of the correct slot to update in the shared object. In the PeopleCursors example, the username was used as a slot name and passed into the object. The same idea can be extended using other slot name conventions. For example, in chess, the username and piece name can be concatenated together to provide a unique name for a piece's slot in the shared object. (This assumes that each piece is named uniquely, such as "pawn1", "pawn2", etc.) For example, using an underscore as a separator, the format could be userName _ pieceName . The key to doing efficient updates is to have the owner of the shared object maintain an object in which it can look up each piece's movie clip using its slot name, just as the PeopleCursors component could get each personal cursor by name from its internal cursors object:

 var name = list[i].name cursors[name].update(  ); 

In other words, each movie clip is saved in an object by name. The name must be the same as the name of the slot the movie clip updates in the shared object. When a change occurs in the shared object, the owner looks up the movie clip by its slot name in the object and calls a method on the movie clip.

15.1.5. Update Sequence Options

When a user does something such as move a piece on a game board or click a button that will result in a shared object update, should you show her the results of her action immediately? Or, should you wait until the update is accepted by the server and onSync( ) has been called on her copy of the shared object? Getting the update sequence right can have a big impact on the usability of a component.

In the PeopleCursors example, the user's PersonalCursor does not update its position on the Stage in response to changes in the current mouse positionit updates only its slot in the shared object. Each PersonalCursor waits for the PeopleCursors object to receive notification in its onSync( ) method and tell it to move. See the "success" case clause in Example 15-2. The resulting delay is intentionally provided to give the user a sense that what she does is not immediately reflected for all the other users. In games, where any delay is undesirable, waiting for the shared object to synchronize is a bad idea. In fact, updating the clip when its position is synchronized may be entirely unnecessary. There are really only three update sequences that are possible when a user makes a change that must be reflected in a shared object:

  • Show the update locally without waiting for the shared object to change.

  • Wait for the shared object to report the change was successful before showing the change.

  • Show the change immediately and again when the shared object synchronizes.

The PersonalCursor component implements the second option. The SharedBall class listed in Example 8-4 uses the first option. When the mouse is dragging the ball, the ball's onMouseMove( ) method checks the x and y values to make sure they aren't off the Stage and then updates both the position of the ball clip and the shared object:

 this.so.data[this.xSlot] = this._x = x; this.so.data[this.ySlot] = this._y = y; 

When the position updates are accepted by the server, the onSync( ) method is called with a list containing an information object with code set to "success". The SharedBall class simply ignores "success" messages. It updates its position only when someone else moves the ball and onSync( ) is passed a list containing an information object with a code value of "change" or "reject". A "reject" code indicates the user's attempt to move the ball was rejected by the server in favor of another client's position update.

You might think that there is never a reason to update the visible state of an application before and after the changes are synchronized in the shared object. Samuel Wan posted a nice sample application that makes good use of the third option. The ScratchPad component, made available as part of his Flash Forward 2003 NY sample files, updates the screen while the user draws on the scratch pad. When the drawing is complete, the graphic is removed from the Stage and the graphic information is sent to the server where it is stored in a shared object. When the shared object synchronizes, the original drawing is redrawn on the Stage. From the user's perspective, the two-step update sequence is a good choice. The user can draw and see immediately what the drawing looks like but is also made aware of the delay between releasing the mouse and everyone else seeing the drawing. The source files are available at:

http://www.samuelwan.com/information/archives/000157.html

Example 17-6 demonstrates a fourth option, which is attempting to anticipate an object's future position based on its current speed and direction of movement, but we save that discussion for Chapter 17.

15.1.6. Interval Update Checks

The ScratchPad component waits until the user has completely drawn a shape before sending the drawing information to the server, whereas the PersonalCursor component updates the shared object whenever the cursor moves. Sometimes, something in between immediate updates and waiting for the user to finish is required. A good example is a shared Input Text field. The field may be part of a form that multiple users can each contribute to filling in. Each shared Input Text field cannot be updated every time a user types a character, because that will reposition the cursor and make the text field unusable to anyone trying to edit or add text to it. But the text field does have to be updated as users add to it. A solution to the problem is to wait a period of time such as 3 seconds after the user edits the field before performing the update. Example 15-4 lists some sample code that uses a three-second delay whenever the text_txt field on the Stage is changed.

Example 15-4. Updating a shared object after a 3-second delay
 so = SharedObject.getRemote("SharedText", nc.uri, false); so.mc = this; so.onSync = function (list) {   this.mc.text_txt.text = this.data.text; }; so.connect(nc); var KeyListener = new Object( ); KeyListener.mc = this; KeyListener.onChanged = function ( ) {   clearInterval(this.intervalID);   this.intervalID = setinterval(this, "updateModel", 3000); }; KeyListener.updateModel = function ( ) {   clearInterval(this.intervalID);   delete this.intervalID;   this.mc.so.data.text = this.mc.text_txt.text; }; text_txt.addListener(KeyListener); 

Each time the field is changed, the timer is cleared and reset to 3 seconds. If the user changes the text in the input field before 3 seconds are up, the interval is deleted and re-created, effectively moving the update 3 seconds into the future.

Getting the sequence of updates and responses right is very important for the user experience in multiuser applications. Make sure you choose a sequence that helps your users understand what is happening and makes it easier for them to work with the component or objects you create.

15.1.7. Populating Lists and DataGrids

A common problem when working with shared objects is to find a simple and efficient way to update a List or DataGrid component with data stored in a shared object. Some techniques work up to a point but fail miserably if pressed too far. For example, a StreamList component may seem to work fine for lists of fewer than 100 items. But when the StreamList has to list several thousand items, it may bog down your entire application. Unfortunately, there is no one, ideal, simple, and efficient algorithm for updating lists and grids. The simplest methods are not as efficient as the more complex update strategies. The following section is designed to provide you with everything you need to know about adding items to the v2 list components, how to get shared object data into those list items, and how to choose the right update strategy for your application.

The v2 UI List and DataGrid components require access to a DataProvider object that stores the individual items to be displayed in the list or grid. The DataProvider is usually an array with some additional methods such as addItemAt( ) or removeItemAt( ) added to it. Each item in the DataProvider is accessed by number. By contrast, shared objects access data by slot name, so it is not possible to use a shared object's data object as a DataProvider. Somehow, shared object data must be placed in a DataProvider before it can appear in a List or DataGrid. By default, each List and DataGrid comes with its own DataProvider. Each provides methods to add items to their DataProvider. For example, you can add an item to a List very simply:

 myList.addItem("This text will appear in the list"); 

The addItem( ) method creates an item object with a label property containing the text passed into addItem( ) . You can optionally pass a second parameter, which is stored as the data property of the item:

 myList.addItem("blesser", {firstName:"Brian", lastName:"Lesser"}); 

Each time myList.addItem( ) is called, the new item is appended to myList 's DataProvider. You can also create the item object yourself without a data property if you like:

 myList.addItem({label:"blesser", firstName:"Brian", lastName:Lesser}); 

You can get the currently selected item back from a List this way with the selectedItem property:

 var item = myList.selectedItem 

And you can retrieve any item from the List's internal DataProvider by index number:

 var item = myList.getItemAt(2); 

A DataGrid component has no concept of a label, so only the last form of the addItem( ) method can be used. Every property of the item whose property name matches a grid column name is displayed in the grid.

Now that we've surveyed all the usual ways to get an item into a List or DataGrid, let's look at getting shared object data into those items.

Example 8-3, reproduced in part here, contained a shared object onSync( ) method that used the removeAll( ) and addItem( ) methods to populate a people list with shared object data:

 this.userList_so.onSync = function (list) {   this.owner.peopleList_lb.removeAll(  );   for (var p in this.data) {     this.owner.peopleList_lb.addItem(p, this.data[p]);   } }; 

In the preceding example, whenever the shared object is updated, the entire contents of the peopleList_lb list is deleted using removeAll( ) . Then, each slot of the shared object is added as an item into the list using a for...in loop. In Example 8-3, the property name of each slot is copied into the label property of each item in the list, and the contents of each slot become the data property of each item. In other words, the data property of each list item is just a reference to the contents of a shared object slot. Assuming the data in the slot is an object, then it is not copied into the people list's DataProvideronly a reference to it is placed in the DataProvider. Using a reference saves space, as the entire object in the shared object slot is not duplicated . After each onSync( ) call, the peopleList_lb list will always accurately reflect the contents of the userList_so shared object. Although Example 8-3 is extremely simple and works reliably, it is terribly inefficient and should not be used (a better approach is presented in Example 13-1). One reason it is inefficient is because the entire contents of the DataProvider are deleted and rebuilt even if only one slot in a hundred has changed. We'll improve on it later. First, we have to deal with a little problem you should avoid.

15.1.7.1 The __ID__ update problem

If we modify the addItem( ) method call so that we just pass in the slot value, we can create an endless loop of useless shared object updates that will cripple our application:

 this.owner.peopleList_lb.addItem(this.data[p]); // Never do this! 

To see why, we have to assume that each shared object slot has an object within it. For example, a slot may hold an object that contains information about users, similar to the following anonymous object:

 {userName:"blesser", firstName:"Brian", lastName:"Lesser"} 

In theory, passing the contents of a shared object slot into the addItem( ) method should be similar to the earlier example of passing in an anonymous object:

 myList.addItem({label:"blesser", firstName:"Brian", lastName:Lesser}); 

The only difference is that there is no label property in the object stored in the shared object slot. Without a label property, the item will not show up in the visual display of the List component. But this is easy to fix. Just tell the List component to use a different property name ( username ) to label list items, as follows:

 myList.labelField = "userName"; 

Now let's get to the real problem: when an item is added to the DataProvider, the DataProvider adds a hidden __ID__ property to each item. The __ID__ property contains a unique number that the List or DataGrid uses to uniquely identify each item even when the DataProvider is sorted. Now, remember that in our example, the item is also an object in a shared object slot. When the DataProvider adds the __ID__ property, the contents of the shared object slot appear to have changed, so the contents are sent to the server and onSync( ) is called. Since the __ID__ property is hidden immediately after being created, the property is not actually sent to the server, but the damage is donea never-ending flurry of useless updates occur, wasting bandwidth and often bringing your Flash movie to its knees.

One solution to the problem is to go back to adding the label and data separately in addItem( ) . Another option is to copy the information in each shared object slot into a new object in each item. For example:

 this.userList_so.onSync = function (list) {   this.owner.peopleList_lb.removeAll(  );   for (var p in this.data) {     var slot = this.data[p];     this.owner.peopleList_lb.addItem(       {label:p, firstName:slot.firstName, lastName:slot.lastName} );   } }; 

Although copying data from the slot into an anonymous object solves the problem, it is a waste of memory because it duplicates all the data in the slot. The original update from Example 8-3 is better in that at least it sets the data property of the item to point at the slot so that not everything has to be duplicated:

 this.owner.peopleList_lb.addItem(p, this.data[p]); 

Unfortunately, there is no such thing as an item.data property when a DataGrid is in use. You cannot do this:

 myDataGrid.addItem(p, this.data[p]); // Does not work! 

The safe way to add items to a DataGrid is to copy the data from the shared object slot into another object that is added to the DataGrid. For example:

 myDataGrid.addItem(   {userName:"blesser", firstName:slot.firstName, lastName:slot.lastName} ); 

Or, as an alternative, a clone( ) method that performs a shallow (i.e., non-recursive) copy is often used:

 function clone(obj) {   var copy = {};   for (var p in obj) {     copy[p] = obj[p];   }   return copy; } 

which may simplify things a little in the onSync( ) method:

 myDataGrid.addItem(clone(this.data[p])); 

To sum up, whenever you store objects in shared object slots, you have to be careful about how you get data from each shared object slot into each list item. You should not simply add the object in the shared object slot to the list as an item! If you are using a DataGrid, you should copy the object's data from the slot into each item. If you are using a List, you can assign the contents of the slot to the data property or copy the data into each item.


There is one way the contents of a shared object slot can be added directly to a List or DataGrid. The technique is a little esoteric, but it has the advantage of saving memory by avoiding duplication of data. If every object in every shared object slot already has a unique __ID__ property, the DataProvider will simply use it, and there will be no flurry of unwanted updates. To make a scheme like that work, every object in a shared object slot has to be assigned a unique number. Since __ID__ numbers are created in Flash starting at 0, you can avoid conflicts by creating __ID__ numbers on the server starting at a very high number and working down. If all this seems like a bit much, don't worry. If you just copy the data from your shared object slots into the items you are adding into a DataGrid, it will waste space but you will not have to redesign everything you have done on the server.

15.1.7.2 Improving List and DataGrid update performance

Now that we've dealt with the vagaries of getting data from shared object slots into list items, let's see how to do it more efficiently. Using the removeAll( ) and addItem( ) methods of a List or DataGrid carries with it a lot of overhead that can seriously reduce a movie's performance. Each method not only updates the DataProvider but also makes a redraw request. The List and DataGrid components don't actually waste time redrawing themselves each time they get a redraw request, but making and handling each request takes time. A much more efficient way to update a List or DataGrid is to get its DataProvider and manipulate it directly. Then, only when the DataProvider is up-to-date do you ask the List or DataGrid to redraw . The following code snippetalready discussed in Chapter 13shows one way to update the DataProvider of a List:

 function onSync (ev) {   var dp = list.dataProvider;       // Get the list's dataProvider.   dp.splice(0);                     // Delete all its items.   for (var p in userList.data) {    // Loop through the shared object.     dp.push({label:p, data:userList.data[p]});  // Add items to the dataProvider.   }   // Tell the dataProvider to send a   modelChanged   message to the List   // so the list will redraw itself.   dp.dispatchEvent({target: dp, type:"modelChanged"}); } 

By updating the DataProvider directly and calling the dispatchEvent( ) method of the DataProvider only after all the updates are complete, the time to update the List can be reduced to as little as one-tenth the time it would take using List methods. For some test results of different update methods, see:

http://flash-communications.net/technotes/mappingSharedObjectsToArrays/index.html

When updating a DataGrid's DataProvider, the object passed into dispatchEvent( ) must include an eventName property value of "updateAll" or the grid will not redraw:

 dp.dispatchEvent({target:dp, type:"modelChanged", eventName: "updateAll"}); 

Even though manipulating the DataProvider directly can produce as much as a tenfold increase in performance, sometimes even that is not enough. It can take a long time to copy all the properties of a shared object with hundreds or even a few thousand items into a DataProvider. When only a few properties have changed in a shared object, copying so many slots is wasteful .

The alternative is to make use of the list array passed into onSync( ) that identifies the slots in the shared object that have been changed or deleted. The list array contains information objects, each with a name and code property. The name is the name of the shared object slot, and the code says what has happened to the slot. Ideally, we would walk through the list and, if a code other than "delete" were found, check whether there was an item in the DataProvider representing the slot that has changed. If the DataProvider already had an item for the slot, we would update it with the current contents of the slot. If an item corresponding to the slot didn't exist, we would add one. An item for a deleted slot that did exist in the DataProvider would be found and deleted.

Unfortunately, finding an item in a DataProvider that corresponds to a slot requires doing a linear searchchecking the items one at a timeuntil the right item is found. Usually, each item is checked to see if one of its properties matches the unique name of a shared object slot. Another problem is that a linear search is itself time-consuming , and searching the DataProvider for each information object in the list passed into onSync( ) would mean performing multiple linear searches for one onSync( ) call.

If the DataProvider is always sorted on the slot name in each item, a binary search could be used, but keeping the DataProvider sorted is time-consuming and unusual.

The best approach is to search the DataProvider only once after each onSync( ) call. As each item in the DataProvider is retrieved, it must be checked against each information item in the list. If a match is foundthat is, the item corresponds to the slot an information object has a code forthen the item can be updated or deleted.


But the list passed into onSync( ) is an array and we don't want to do a linear search of it each time an item is examined. So before searching through the DataProvider, the information items in the list are placed in an object from which they can be retrieved by slot name. Example 15-5 lists three functions that together provide a much more efficient way to update the DataProvider of a DataGrid. The example code is based on some important assumptions:

  • The SharedObjectFactory class was used to get the shared object, and the onSync( ) method belongs to a component that has registered itself as an event listener on the shared object.

  • Each slot of the shared object contains a simple object. There are no nested data structures in a slot such as objects inside objects or arrays of objects.

  • The name of each slot is the unique key that will identify each item in the DataProvider.

  • The slot name is also a username that uniquely identifies each user.

  • A userName property must be added to each item in the DataProvider and will always match one slot's name.

These assumptions will not always be true, so the code presented here may have to be carefully adapted to your own needs. However, in cases in which small changes are regularly made on large shared objects, the performance improvement is worth it! The code has been extensively commented to help explain what each part does so you can adapt it to your own needs.

Example 15-5. Efficiently updating a DataGrid's DataProvider
 /*   clone(  )   makes a shallow copy of an object. It will not copy nested  * data structures such as objects within objects. It clones objects in  * shared object slots so that the copies can be updated without automatically  * updating the shared object. Copies are required when merging data from other  * objects or when a DataProvider adds an   __ID__   property. If an   __ID__   * property already exists but is hidden, it is also copied.  */ function clone(obj) {   var copy = {};   // Copy the visible properties of the object.   for (var p in obj) {     copy[p] = obj[p];   }   // Add in the hidden   __ID__   property if it exists.   if (obj.__ID__ != undefined) copy.__ID__ = obj.__ID__;   return copy; } // Deletes all the items in a dataProvider and repopulates it // with new items copied from data in each slot of a shared object. function rebuildProvider (ev) {   // Get a reference to the originating shared object.   var so = ev.target;   // Get the list's dataProvider and truncate it.   var dp = grid.dataProvider;   dp.length = 0;   // Fill the dataProvider using   Array.push( )   .   for (var slotName in so.data) {     dp.push(clone(so.data[slotName]));   }   dp.dispatchEvent({target:dp, type:"modelChanged", eventName: "updateAll"}); } //   onSync( )   receives events from the shared object that this // object is an event listener on. function onSync (ev) {   // Get the list's dataProvider.   var dp = grid.dataProvider;   // If all the contents of the shared object have been cleared (deleted)   // or the dataProvider has fewer than 20 items in it, then it is as efficient   // (or better) to truncate the provider and copy all the slots into it.   if (ev.list[0].code == "clear"  dp.length < 20) {     rebuildProvider(ev);     return;   }   // Build an object of the information items in the list, so we can   // look up each one by slot name efficiently.   var list = ev.list;   var nameLookup = {};   for (var i in list) {     var info = list[i];     nameLookup[info.name] = info;   }   var so = ev.target;   // Visit each item in the dataProvider to see if its   userName   property   // matches a slot name that was in the list passed into   onSync( )   , and   // either update or delete the item that matches.   for (var i = 0; i < dp.length; i++) {     var slotName = dp[i].userName;    // Get the   userName   property of the item.     var info = nameLookup[slotName];  // Look it up in the   nameLookup   object.     if (info) {                       // If it's in the   nameLookup   .       // If the info object   code   is "delete" remove it from the array.       if (info.code == "delete") {         dp.splice(i, 1);              // Delete the item from the list.         i--;                          // Decrement   i   to adjust for   splice( )   .       }       // Otherwise the info object   code   is "change", "success", or "reject",       // so update the array by replacing the item in it.       else {         dp[i] = clone(so.data[slotName]);       }       // Delete this entry in the   nameLookup   object so that after examining       // every item in the dataProvider, we are left only with entries that       // don't correspond to any item in the dataProvider.       delete nameLookup[slotName];     }  // end if (info)   }/  / end for loop   // Any new records that are left in the   nameLookup   object that have   // changed must be added into the dataProvider.   for (var slotName in nameLookup) {     dp.push(clone(so.data[slotName]));   }   // Tell the list that the model has changed and to redraw itself now.   dp.dispatchEvent({target: dp, type:"modelChanged"}); } 

For most applications, the strategy first used in Example 13-1getting the DataProvider, deleting everything in it, pushing new items into it, and only then having the DataProvider ask the List or DataGrid to redraw itselfworks well enough. More important, the code is simple and easy to adapt to a wide variety of requirements. If your List or DataGrid is unlikely to hold more than 50 items, that approach may be your best choice. But if you need to display a List or DataGrid with hundreds of items, you should use something like the technique shown in Example 15-5.

I hope I've answered all your burning questions regarding working efficiently with shared objects and integrating them into well-designed applications.



Programming Flash Communication Server
Programming Flash Communication Server
ISBN: 0596005040
EAN: 2147483647
Year: 2003
Pages: 203

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net