A Simple Remote Shared Object Editor

This article is based on a lecture/demonstration I did during an Authoring for New Media class at Ryerson University. It seemed to help explain how remote shared objects work so I have written it up as an article here. During the original lecture/demonstration I was asked a number of questions and have tried to work the answers into the text of this article. Also, in the middle of the demonstration some students with wireless equipped laptops started using their own instances of the shared object editor I was demonstrating. Aside from the laughter that ensued when everyone realised the screen projection we were all looking at was changing without my doing anything, having multiple clients updating the shared object at the same time really helped clarify how remote shared objects work. So I have also provided a Web page with two separate shared object editors on it that you can use and some notes on things to try with them.

The original demonstration was designed to show how to:

  1. display shared object data in a DataGrid - especially as shared object data is updated or deleted;
  2. add, update, and delete data in a shared object using a simple form;
  3. view shared object synchronization messages as changes occur in the shared object.

Note: the code shown here is for demonstration purposes and was not designed to be dropped into a production-quality application. However, with a little effort it may be useful for developing a shared object debugging and test client or as a starting point for part of a communication component.

Here is the source FLA in a Zip archive: remoteSharedObjectEditor.zip The archive also includes a Web page (editor.html) that contains two SWFs on the same page so you can work with two clients at the same time.

How to Use the Remote Shared Object Editor

The Remote Shared Object Editor is a very simple application. It allows to you to add, update, and delete slots in a remote shared object. Each slot has a slot name and contains a value. Or, in more object-oriented terms, each "property" of the shared object has a property name and value. The editor only supports storing a string value in each slot. Here is a screen capture of the editor after one slot has been added to a shared object:

The TextArea displays information about the application as it runs. It shows output from the NetConnection's onStatus() method indicating the movie successfully connected to a FlashCom application instance as well as output from the streams_so remote shared object's onSync() method. From the output you should be able to see that onSync() has been called twice. onSync() was called when the shared object connected to the server and the second time after the movie added a slot to the shared object named webcam6.

Using the editor is fairly simple:

If you would like to try the demonstration before reading further here is a page with two SWFs on it.

Setting Up the FLA

Since this is only a demonstration, the FLA I'll create will be very simple. There is no preloader and the movie only has two labeled frames: Init and Main. The Init frame will contain the code that initializes the movie and connects to the sharedObjectEditor application on a server. The Main frame will contain the DataGrid, Button, TextInput, and other components that make up the shared object editor. Common to all frames of the movie is a TextArea component.

An Alternative to trace()

It is often difficult to understand and debug what is going on in a communications application using trace() statements in the Flash IDE. Often it is more convenient to simply display messages in each test client using a TextArea component. Placing a TextArea component on the stage and creating a function that appends text to it is all that is needed. The following writeln() function can be placed on the main timeline on frame one:

function writeln(msg){
   msgArea.text += msg + "\n";
   msgArea.vPosition = msgArea.maxVPosition;
   msgArea.redraw(); // Fixes TextArea scrolling bug.
}

Let's have a quick look at the basic layout of the FLA file. There are four layers. The TextArea has been placed on the Messages layer and given the instance name msgArea. It will be available to display messages when the movie initializes and while editing the shared object in the "Main" frame. The writeln() function is located in the scripts layer in the first frame. The other two scripts on frame 9 and 20 are just stop() statements.

If you try following along with this article and create the FLA from scratch yourself then it is a good idea to test each change as you go. You should test that the writeln() function works by adding a statement like this after the writeln() function in the first frame and run the movie:

writeln("Flash Player version: " + getVersion());

When I run the movie in this state on my machine the following text appears in the msgArea:

Flash Player version: WIN 7,0,19,0

Making a Connection

I've already mentioned that the Shared Object Editor will connect to an application named sharedObjectEditor. If you haven't already, create a sharedObjectEditor directory in a test server's applications folder. It does not need a main.asc file but be sure that the server you run it on is either protected from the public Internet or that the application will not accept connections from anyone else's Flash movies. See the section on SWF Origin Restrictions here:

http://www.macromedia.com/devnet/mx/flashcom/articles/firewalls_proxy06.html

With that out of the way, I'm going to quickly review the process of using a NetConnection object to connect to a FlashCom application instance. There are three steps:

  1. create the NetConnection object;
  2. define an onStatus() method for it;
  3. call the NetConnection's connect() method.

Here is the code to be added to the Init frame (below the writeln() function):

//Create the NetConnection object:
nc = new NetConnection();

//Define an onStatus() method:
nc.onStatus = function(info){
   writeln("nc.onStatus> info.code: " + info.code);
   writeln("nc.onStatus> info.description: " + info.description);
   if(info.code == "NetConnection.Connect.Success") gotoAndPlay("Main");
}

// Execute the connect() method:
nc.connect("rtmp:/sharedObjectEditor");

For some readers it will be worth reviewing how and why the onStatus() method works because it may help set the stage for other callback methods.

The onStatus() method must be defined before attempting to connect to the server. The onStatus() method is a callback function. That is, it is not called directly by any code I plan to write. Instead it is called by Flash some time after connect() is called when the result of the connection attempt is known. A connection attempt may take some time before the connection is established or fails for some reason. So the connect() method simply starts the connection process off. When the result of the connection attempt is known, the onStatus() method is called and passed an object containing information about what happened. The object is usually called an information object so it is common to define an onStatus() method this way:

 nc.onStatus = function(info){
   //...
}

The information object can be referred to using the variable named info. There is no rule, only habit, that says the variable must be named info. It could also be named infoObject:

 nc.onStatus = function(infoObject){
   //...
}

The info object always has a code, level, and description property that contains text strings describing what happened to the connection attempt. Since I'm writing a test/sample application I'll just write out the code and description properties of the info object:

nc.onStatus = function(info){
   writeln("nc.onStatus> info.code: " + info.code);
   writeln("nc.onStatus> info.description: " + info.description);
   if(info.code == "NetConnection.Connect.Success") gotoAndPlay("Main");
}

If I run the movie on a machine without a FlashCom server running on it I'll eventually get this type of output:

Flash Player version: WIN 7,0,19,0
nc.onStatus> info.code: NetConnection.Connect.Failed
nc.onStatus> info.description: undefined

Now if I start up the FlashCom server on my machine without a sharedObjectEditor directory I'll see two messages in the msgArea:

Flash Player version: WIN 7,0,19,0
nc.onStatus> info.code: NetConnection.Connect.Rejected
nc.onStatus> info.description: [ Server.Reject ] : (_defaultRoot_, _defaultVHost_) : Application (sharedObjectEditor) is not defined.
nc.onStatus> info.code: NetConnection.Connect.Closed
nc.onStatus> info.description: undefined

Note that onStatus() was actually called twice. The first time the info.code property contained the string "NetConnection.Connect.Rejected" and the second time it contained the string "NetConnection.Connect.Closed"

If I finally add a sharedObjectEditor directory to FlashCom's applications directory I see this in the msgArea:

Flash Player version: WIN 7,0,19,0
nc.onStatus> info.code: NetConnection.Connect.Success
nc.onStatus> info.description: Connection succeeded.

So the info.code property tells me what happened some time after I called connect() and when the code property contains the string "NetConnection.Connect.Success" it means the connection attempt was successful and a connection to the server has been established.

The last line of the onStatus() method checks to see if a connection has been established and moves the playhead to the "Main" label if it has. Now that the movie can connect to FlashCom its time to go back to Flash and build the remote shared object editor's user interface on the Main frame.

Building the User Interface

I've already got a TextArea in the upper half of my Flash movie. All I need to do is drag some more components from the Components panel into the movie. I'll need a DataGrid (I'm using Flash Professional), two Label, two TextInput, and two Button components. Here's what my FLA looks like after placing, customizing, and naming each component:


I didn't give either Label an instance name. The other components are named: slotGrid, slotNameInput, slotValueInput, addButton, and deleteButton. Now if I run the movie - just to test it. I should see all the components appear after a brief delay while the movie connects to the server. After the connection is established code in the nc.onStatus() method will move the playhead to the "Main" frame and the output in the msgArea should still say:

Flash Player version: WIN 7,0,19,0
nc.onStatus> info.code: NetConnection.Connect.Success
nc.onStatus> info.description: Connection succeeded.

Responding to Button Clicks and Grid Selections

To make the editor work as advertised, I need to know when a button is clicked or an item is selected in the DataGrid. Since I'm using the version 2 UI components that shipped with Flash 2004 Pro, I have to use each component's addEventListener() method to make sure each component will deliver a message somewhere when it is clicked or changes. Since this is a simple demonstration program I simply defined a click() and change() method right on the main timeline to receive click and change event messages:

addButton.addEventListener("click", this);
deleteButton.addEventListener("click", this);
slotGrid.addEventListener("change", this);

function change(ev){
   var item = ev.target.selectedItem;
   slotNameInput.text = item.slotName;
   slotValueInput.text = item.slotValue;
}

function click(ev){
   switch(ev.target){
      case addButton:
         writeln('Add Slot');
         // Add or update a slot in the remote shared object.
      break;
      case deleteButton:
         writeln('Delete Slot');
         // Delete a slot in the remote shared object.
      break;
   }
}

The click() function receives click events from both buttons. To respond to individual buttons I use a switch statement to see if the ev.target property of the event is addButton or deleteButton. Then I write out a message saying which button was pressed. Later I'll add code that actually changes the shared object.

The change() function gets the selected item from the DataGrid's dataProvider and displays the value of the slotName and slotValue properties in the TextInput components. How do I know there will be slotName and slotValue properties in each DataGrid item? The short answer is because I plan to put them there as we'll see a little later.

If you are building an FLA while you read along, perhaps by cutting and pasting code, make sure everything is working by running the movie and pressing the two buttons a few times. You should see output like this:

Flash Player version: WIN 7,0,19,0
nc.onStatus> info.code: NetConnection.Connect.Success
nc.onStatus> info.description: Connection succeeded.
Add Slot
Add Slot
Delete Slot
Delete Slot
Delete Slot

I can't emphasize enough the value of making small changes and testing that they work properly before moving on to adding more code.

Creating and Connecting the Remote Shared Object

When I gave the lecture/demonstration this article is based on, I was interested in helping my students understand remote shared objects so they could use them to contain stream names. In an application they were working on, every time a stream is published the stream name must appear as a property (or slot) name in a shared object so every client knows what streams are available. In the following code I've kept the remote shared object name and variable name that I used originally. Here is the code from the Main frame that was added right after the UI code above. Please have a quick look at it now before I explain it in detail following the listing. Also, I'll improve on this code later.

streams_so = SharedObject.getRemote("streams", nc.uri);

streams_so.onSync = function(infoList){
   // Just show the info objects in the infoList array.
   writeln("streams_so.onSync> " + infoList);
   for (var i = 0; i < infoList.length; i++){
      var info = infoList[i];
      for (var p in info){
         writeln(i + "> " + p + ": " + info[p]);
      }
   }
   // NOW, update the datagrid with the data in the shared object:
   slotGrid.removeAll();
   for (var p in streams_so.data){
      slotGrid.addItem({slotName: p, slotValue: streams_so.data[p] });
   }
}
streams_so.connect(nc);

Creating a Remote Shared Object

The first line in the listing above creates a remote shared object that seems to be named "streams" and that I can refer to using the streams_so variable. I want to look at the statement that creates the shared object in detail so here it is again:

streams_so = SharedObject.getRemote("streams", nc.uri);

After this statement executes streams_so refers to a remote shared object but it is not connected to the shared object on the server and can't be used yet. It is just a shared object that can be further customized before connecting to the server. But before I do more with it, lets look at the SharedObject.getRemote() method a little more closely. The SharedObject in SharedObject.getRemote() is a class. The SharedObject class is always available so that we can create a remote shared object whenever we need one by calling a method named getRemote(). Its a little different than how we create a new NetConnection object:

nc = new NetConnection();

We don't use the keyword new to get a remote shared object. Instead we have to call a class method. If you are not sure how a class method is different from an instance method check out Colin Moock's book Essential ActionScript 2.0. Now let's look at what we pass into getRemote(). The first parameter is the path of the shared object. You can think of it as the shared object's name. The path is always in the form of a partial URI so that paths such as "public/myStreamNames" would be a valid name or path for a remote shared object. The second parameter is the URI for the application instance we plan to connect the shared object to. In the Init frame a NetConnection was created that we can refer to using the nc variable. NetConnection objects have a property that contains the rtmp address they are connected to so that is used as a second parameter. If that doesn't make sense, It is as though this statement was executed:

streams_so = SharedObject.getRemote("streams", "rtmp:/sharedObjectEditor");

Later, I'll use the NetConnection object to actually connect the streams_so to the server this way:

streams_so.connect(nc);

But first, and most importantly, we need to create an onSync() callback method on the streams_so shared object.

The onSync() Method

Remote shared objects are unlike regular ActionScript objects in that many Flash movies may be connected to them at the same time. When one movie changes a shared object all the copies of the shared object in all the other movies are updated by the server. For example when a movie starts to publish a stream named "webcam6" it might announce the availability of the stream by adding a new property to the shared object this way:

streams_so.data["webcam6"] = "available";

Note: the data object is always used to access the slots of a shared object. The streams_so shared object is not used directly. Shortly after a new property of the shared object is added in one movie the server is notified of the change and sends on to all the other movies a message that causes them to add a webcam6 property to their copy of the shared object. Following along with the stream name example, you might expect that all the other movies would then create a NetStream object in order to play the "webcam6" stream. But how do all the other movies know a slot named webcam6 has just been added to their copy of the remote shared object? The answer is that after the Flash Communication Server updates a movie's shared object the shared object's onSync() method is called. Since more than one property in the shared object may have changed, onSync() is always passed an array of information objects. In other words onSync() is another example of a callback function. It is defined in your ActionScript code but is called later (or called back) some time after you define it. It is called by Macromedia's SharedObject code running in Flash. Macromedia's code is "intrinsic" so you can't read it but it is there and when a local copy of a shared object is updated by the server it calls onSync() and passes in an array of information objects. Each information object has a code property, much like NetConnection information objects, that contain information on how the server updated the shared object.

Here is part of the onSync() method listing again:

streams_so.onSync = function(infoList){

   // See what infoList writes out as:
   writeln("streams_so.onSync> " + infoList);

   // Just show the info objects in the infoList array.
   for (var i = 0; i < infoList.length; i++){
      var info = infoList[i];
      for (var p in info){
         writeln(i + "> " + p + ": " + info[p]);
      }
   }

   // code removed here....
}

The first writeln() statement just writes out the infoList. Since it is an array that contains objects the output will not be that interesting. For example if infoList contains two objects the output would be: [object Object],[object Object]. To display the information inside each information object in the infoList array, the second part of the code uses a for loop to get each information object and then write out all its properties. If you cut and pasted the earlier complete shared object code sample into the Main frame of an FLA and run it you should see this:

Flash Player version: WIN 7,0,19,0
nc.onStatus> info.code: NetConnection.Connect.Success
nc.onStatus> info.description: Connection succeeded.
streams_so.onSync> [object Object]
0> code: clear

The last two lines were written out after the shared object was connected and updated by the server. The first line: streams_so.onSync> [object Object] shows that only one information object was in the infoList array. The second line: 0> code: clear shows that the information object had only one property: code, and that it contained the string: "clear". The string "clear" indicates that any properties of the local version of the shared object have been cleared; or in other words that they were deleted. The shared object, once cleared, is ready to be updated. If the code property contains a string such as "success", "change", "reject", or "delete" then the information object will also have a name property with the name of the slot that was changed. When many slots change there will be many information objects in the infoList array. There will be one information object for each slot of the shared object that has changed or been deleted.

Finally, lets look at the code that redisplays the shared object data in the DataGrid whenever onSync() is called.

streams_so.onSync = function(infoList){
   // code removed here

   // NOW, update the datagrid with the data in the shared object:
   slotGrid.removeAll();
   for (var p in streams_so.data){
      slotGrid.addItem({slotName: p, slotValue: streams_so.data[p] });
   }
}

This code is the easiest way to update a DataGrid whenever the shared object changes. (Later, we'll see an almost as easy but much more efficient way.) The code is very simple. The DataGrid's removeAll() method is called first. It just deletes all the items inside the DataGrid's dataProvider. You can think of it as simply clearing out all the rows in the DataGrid.Then a for in loop is used to add an item to the DataGrid for each slot in the shared object. Inside the for in loop the variable p will contain the slot name for one slot in the shared object and streams_so.data[p] will contain the slot value for that slot. In this example addItem() is being passed an anonymous object: {slotName: p, slotValue: streams_so.data[p]}. If, for example, p contains the slot name "webcam6" and streams_so.data[p] contains the slot value "available" then the anonymous object (or item) added to the DataGrid is {slotName: "webcam6", slotValue: "available"}.

Unless you instruct the DataGrid otherwise, when the first item is added it will create columns based on the property names of the item. In this case the DataGrid will create a slotName column and a slotValue column based on the item's property names. After creating the columns the DataGrid will display the item's slotName value "webcam6" in the slotName column and the item's slotValue value "available" in the slotValue column.

If that is a little opaque have a look at the DataGrid documentation in the Flash 2004 Pro help system or chapter 15 in Programming Flash Communication Server. Or, you might try this article for reference:

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

Unfortunately, if you were to run the example code at this point the DataGrid would always be empty because there is no data in the shared object. So the final step is to put data in the shared object.

Adding, Changing, and Deleting Shared Object Data

To complete the simple remote shared object editor I only need to add two lines of code. To add a new property to the shared object or to update an existing property this will do the trick:

streams_so.data[slotNameInput.text] = slotValueInput.text;

I'm using the text in the slotNameInput component as the slot name and putting the text in the slotValueInput component into it. Similarly to delete a slot in a shared object I can just do this:

delete streams_so.data[slotNameInput.text];

This time I use the delete keyword to delete a slot in the shared object. I'm assuming the slotNameInput component contains the name of a shared object slot. These two statements have to be added to the click() method so that they are executed when one of the buttons is pressed:

function click(ev){
   switch(ev.target){
      case addButton:
         writeln('Add Slot');
         streams_so.data[slotNameInput.text] = slotValueInput.text;
      break;
      case deleteButton:
         writeln('Delete Slot');
         delete streams_so.data[slotNameInput.text];
      break;
   }
}

Quick Summary

Before I go on to refine what I've done so far, it is probably a good time to summarize how everything works. The shared object I've created and let the user update in this example is very simple. Each slot only contains a string. The user can add a new shared object property by entering a slot name, a string to go in it, and then pressing the Add/Update button. Selecting an item in the DataGrid shows one shared object property name and value in the form. Pressing the Delete button will delete it. Whenever the shared object is updated the onSync() method writes out the information it receives about the changes. Then onSync() removes every item from the DataGrid and adds a new item for each slot in the shared object. Each item is an object with separate slotName and slotValue properties so we can see both the name of each slot and the string in it within the DataGrid. Selecting an item in the DataGrid produces a change event. The change() function updates the TextInput fields with values from the selected item in the grid. Pressing one of the buttons either updates the shared object or deletes a slot.

I could stop here. At this stage the editor works. If you haven't tried it already please experiment with adding, changing and deleting slots here. Then come back for the following two enhancements.

Are We Ready Yet?

When the remote shared object first connects to the server all its slots are cleared. Consequently if the user tries to add a slot to the shared object before it connects the data that was added will be cleared. To prevent the user from trying to edit the shared object before it is connected we can disable the two buttons until onSync() is called the first time. Then, when onSync() is called the buttons can be enabled. To disable the buttons when the Main frame loads only requires two statements:

addButton.enabled = false;
deleteButton.enabled = false;

However, enabling them is a little more complicated because there is no point in enabling them every time onSync() is called. Here is the slightly modified code that enables the buttons the first time onSync() is called:

streams_so.synchronized = false;
streams_so.onSync = function(infoList){
   if (! this.synchronized){
      this.synchronized = true;
      addButton.enabled = true;
      deleteButton.enabled = true;
   }

   // code removed....

}

The first line creates a new property named synchronized on the shared object itself and not on the data property of the shared object. The synchronized property could have been called anything but I've chosen to name it synchronized. When onSync() is called the first time the synchronized property is checked. If it is false it is set true and the buttons are enabled. From now on when onSync() is called synchronized will always be true so the buttons will not be needlessly re-enabled.

Note: the shared object used here is a temporary shared object. Persistent shared objects are usually not cleared when they first connect. See the Client-Side ActionScript Dictionary or Chapter 8 in Programming Flash Communication Server for more details on the differences between temporary and persistent remote shared objects.

Improving Performance

The onSync() method can still be further improved. The way I have updated the DataGrid is not very efficient. Here is a similar but much more efficient way to do it:

streams_so.onSync = function(infoList){
   //  code removed...

   // NOW, update the datagrid with the data in the shared object:
   var dp = slotGrid.dataProvider;
   dp.length = 0;
   for (var p in streams_so.data){
      dp.push({slotName: p, slotValue: streams_so.data[p] });
   }
   dp.dispatchEvent({target:dp, type:"modelChanged", eventName: "updateAll"});
}

This code gets and manipulates the DataGrid's dataProvider. After the for in loop adds one item to the dataProvider for each slot it asks the dataProvider to notify the grid that it has changed this way:

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

This approach is much faster than using the removeAll() and addItem() methods of the DataGrid.

Finally, here is the complete code from the Main frame of the movie. Despite the many pages I've taken to describe it the code itself is quite short.

addButton.addEventListener("click", this);
deleteButton.addEventListener("click", this);
slotGrid.addEventListener("change", this);
addButton.enabled = false;
deleteButton.enabled = false;

function change(ev){
   var item = ev.target.selectedItem;
   slotNameInput.text = item.slotName;
   slotValueInput.text = item.slotValue;
}

function click(ev){
   switch(ev.target){
      case addButton:
         writeln('Add Slot');
         streams_so.data[slotNameInput.text] = slotValueInput.text;
      break;
      case deleteButton:
         writeln('Delete Slot');
         delete streams_so.data[slotNameInput.text];
      break;
   }
}

streams_so = SharedObject.getRemote("streams", nc.uri);
streams_so.synchronized = false;
streams_so.onSync = function(infoList){
   if (! this.synchronized){
      this.synchronized = true;
      addButton.enabled = true;
      deleteButton.enabled = true;
   }
   // Just show the info objects in the infoList array.
   writeln("streams_so.onSync> " + infoList);
   for (var i = 0; i < infoList.length; i++){
      var info = infoList[i];
      for (var p in info){
         writeln(i + "> " + p + ": " + info[p]);
      }
   }
   // NOW, update the datagrid with the data in the shared object:
   var dp = slotGrid.dataProvider;
   dp.length = 0;
   for (var p in streams_so.data){
      dp.push({slotName: p, slotValue: streams_so.data[p] });
   }
   dp.dispatchEvent({target:dp, type:"modelChanged", eventName: "updateAll"});
}
streams_so.connect(nc);

Shared Object Review

I wrote the simple remote shared object editor to demonstrate the essential details of working with remote shared objects. Of course there is a lot more you can do with them. But, in this article you've seen how to:

  1. create and connect a remote shared object using a NetConnection (nc):
    streams_so = SharedObject.getRemote("streams", nc.uri);
    streams_so.onSync = function(infoList){
     // your code goes here...
    }
    streams_so.connect(nc);
  2. add or update a slot in a shared object:
    streams_so.data[slotNameInput.text] = slotValueInput.text;
  3. delete a shared object slot:
    delete streams_so.data[slotNameInput.text];

  4. wait until the shared object is synchronized the first time before allowing updates:
    streams_so.synchronized = false;
    streams_so.onSync = function(infoList){
       if (! this.synchronized){
          this.synchronized = true;
          addButton.enabled = true;
          deleteButton.enabled = true;
       }
       // your code goes here...
    }
  5. display the contents of a shared object within a DataGrid:
    streams_so.onSync = function(infoList){
       var dp = slotGrid.dataProvider;
       dp.length = 0;
       for (var p in streams_so.data){
          dp.push({slotName: p, slotValue: streams_so.data[p] });
       }
       dp.dispatchEvent({target:dp, type:"modelChanged", eventName: "updateAll"});
    }

Remote shared objects are an essential part of developing FlashCom-based applications. This article only covers the basics of using temporary remote shared objects. See chapters 8 of Programming Flash Communication Server for more details on using shared objects and chapter 15 for information on integrating remote shared objects into larger applications.


Post or read comments for this page.


Document posted February 14, 2005 by Brian Lesser