8.2. Getting a Shared Object in Flash
One reason shared
objects cannot be extended, or subclassed, to create custom classes
is that they are created by static methods of the
SharedObject class rather than constructed using
the new operator. In client-side Flash
ActionScript, the two static methods to create shared objects are
SharedObject.getLocal( ) and
SharedObject.getRemote( ).
8.2.1. Getting and Using a Local Shared Object
SharedObject.getLocal( ) returns a
local shared object (LSO):
local_so = SharedObject.getLocal("ConfigData");
Once returned, a local shared object can be used immediately.
Properties and values can be created, stored in, and retrieved from
the shared object's data property
(data is a generic Object
instance):
// Set up an onStatus( ) handler for this LSO.
local_so.onStatus = function (info) {
if (info.code == "SharedObject.Flush.Failed") {
trace("Unable to save user information - space request not granted.");
}
};
// Set two properties of the shared object.
local_so.data.userName = "blesser";
local_so.data.password = "bigSecret";
// Try to write the data to disk.
var result = local_so.flush( );
if (result == false) {
trace("Can't save user information because local storage is set to Never.");
}
The Flash editor provides code hints for shared objects when variable
names end in "_so".
Local shared objects are not covered in detail in this book. See
ActionScript for Flash MX: The Definitive Guide
(O'Reilly) for a complete description of how to work
with LSOs. A comparison of local path options for local and remote
shared objects is included in "Locally and Remotely
Persistent Shared Objects" later in this chapter.
However, you may also want to consult the previously referenced book
for a description of how to share local objects between movies from
the same domain and how to use the local path parameter in
getLocal( ).
8.2.2. Getting and Using a Remote Shared Object
Remote shared objects are created using
SharedObject.getRemote( ) but do not dynamically
share information until they are connected to the server using a
NetConnection object and have been synchronized
with a copy of the shared object on the server. Setting up an RSO is
a multistep process that begins with creating an unconnected RSO. The
RSO must be associated with a relative URI (the shared
object's name) and an RTMP address (the URI of the
application instance), both of which must be passed into the
getRemote( ) method:
nc = new NetConnection( );
nc.connect("rtmp:/courseChat");
remote_so = SharedObject.getRemote("UsersData", nc.uri);
Normally, a NetConnection
object's uri property is passed
into the getRemote( ) method as the RTMP
address, but a string can be used as well:
remote_so = SharedObject.getRemote("UsersData", "rtmp:/courseChat");
RSOs work across the network, so the
next step is to connect the RSO instance in the Flash movie with the
server by calling the shared object's
connect( ) method and passing it a
NetConnection object:
if (remote_so.connect(nc)) {
trace("Connection object and URIs are OK. Wait for first onSync.");
}
else {
trace("Can't connect - check URIs and the NetConnection object.");
}
If the connection succeeds, the SharedObject.connect(
) method returns true, but the shared
object is still not ready for use. The server may have to create its
own copy of the shared object or copy the contents of an already
existing shared object to the local copy that has just connected.
When the server has completely synchronized its own copy of the
shared object with the movie's copy, the
onSync( ) method of the movie's
shared object is called. The onSync( ) method
should be defined before connect( ) is called to
ensure that onSync( ) calls are not missed. The
onSync( ) method is passed an array of
information objects when it is called. After the initial call to
onSync( ) indicates that synchronization has
taken place, each information object will normally contain a message
about a slot in the shared object. Example 8-2 shows
a simple default onSync( ) handler that outputs
all the information it receives, which is often useful during
testing. This code belongs in the client-side
Flash
movie. The writeIn( ) function is shown in Example 5-1.
Example 8-2. A default onSync( ) handler
SharedObject.prototype.onSync = function (list) {
writeln("=====SharedObject.prototype.onSync=====");
if (!this.synchronized) {
writeln("This shared object has been synchronized.");
this.synchronized = true;
}
for (var i in list) { // Loop through all the objects in the array.
writeln("------------------");
var info = list[i]; // Get the object at position i in the array.
for (var p in info) { // Loop through all the properties of the object.
writeln("i: " + i + ", " + p + ": " + info[p]);
if (p == "oldValue") { // If a value changed, show the new one too.
writeln("newValue: " + this.data[info.name]);
}
}
}
};
Once onSync( ) has been
called at least once, the shared object is ready for use.
 |
In client-side ActionScript, all data exchange is performed through
the data property of a shared object. (Properties
assigned directly to the shared object are ignored.) To write data to
a slot, assign a value to a property of the shared
object's data object. To read
data from a slot, read a property of the shared
object's data object.
|
|
This example shows how to write properties to the
data property of the shared object:
// Assign numbers or other primitive types using the dot operator.
remote_so.data.ball_1_x = 0; // Assign a number.
remote_so.data.ball_1_y = 0; // Assign a number.
// Create an array.
var shapeList = new Array( );
shapeList[0] = "lineStyle";
shapeList[1] = 2;
shapelist[2] = "moveTo";
shapeList[3] = 0;
shapeList[4] = 0;
shapeList[5] = "lineTo";
shapeList[6] = 125;
shapeList[7] = 0;
shapeList[8] = "Z"; // Sentinel value to force drawing of the last command
// Assign an array to a slot, in this example using the [] operator.
var shapeName = "line_20";
remote_so.data[shapeName] = shapeList;
// Assign an object, in this example using an object literal and the [] operator.
var userName = "blesser";
remote_so.data[userName] = {fullName: "Brian Lesser", status: "Offline"};
Similarly, reading data from a
shared object requires accessing the properties of the
data object:
// Move a movie clip to the coordinates stored in a shared object.
ball_1_mc._x = remote_so.data.ball_1_x;
ball_1_mc._y = remote_so.data.ball_1_y;
// Create a new movie clip and draw within it using an array in a shared object.
var sList = remote_so.data["line_20"];
var mc = this.createEmptyMovieClip("line_20_mc", 100);
startData = -1;
endData = -1;
for (var i = 0; i < sList.length; i++) {
var token = sList[i];
if (typeof token == "string") {
if (lastCommand != undefined) {
mc[lastCommand].apply(mc, sList.slice(startData, endData));
}
lastCommand = token;
startData = i + 1;
} else {
endData = i + 1;
}
}
// Fill in the fields of a form using an object in a shared object.
fullName_txt.text = remote_so.data[userName].fullName;
status_txt.text = remote_so.data[userName].status;
Movie clips should not be stored in a shared object slot; their
properties, such as _x and
_alpha, will not be sent to the server and other
clients:
remote_so.data.ball_1 = ball_1_mc; // Error: mc properties cannot be shared!
A shared object cannot send movie clip
properties to the server because the properties cannot be accessed by
a for-in loop. Similarly, an
object's properties that have been hidden using
ASSetPropFlags( ) will not be sent to the server
and other clients. Individual properties, such as
MovieClip._x, can be assigned to a shared object
slot.
Deleting a slot in a shared object is done using the
delete operator:
// Delete properties of the shared object.
delete remote_so.data.ball_1_x;
delete remote_so.data.ball_1_y;
var shapeName = "line_20";
delete remote_so.data[shapeName];
var userName = "blesser";
delete remote_so.data[userName];
To delete all the properties of a shared object, you can loop through
all the properties of the data object using a
for-in loop and delete each one:
for (var p in so.data) {
delete so.data[p];
}
However, depending on how your application is designed, another
client can add a new property at the same time you are trying to
clear the shared object of all its properties. A shared object can be
reliably cleared using Server-Side ActionScript (SSAS).
You cannot replace the data object with your own
object by assigning it to the data property of a
shared object. If you try to assign something to the
data object itself, nothing happens:
remote_so.data = new Object( ); // Illegal (does nothing)
Instead, assign the value to a property of the
data object, as follows:
remote_so.data.someObj = new Object( ); // This works
8.2.2.1 Private versus shared data
Whenever a slot in the data
object is created, updated, or deleted, the corresponding slot on
every other instance of the RSO is updated accordingly. However,
properties that are added directly to the shared object itself remain
private to the instance and do not cause a remote update:
// Add a new property to the shared object that will not be copied
// to remote instances.
remote_so.synchronized = false;
If you intended to create a shared property, use this instead:
remote_so.data.synchronized = false;
 |
When you call SharedObject.getRemote( ) to get a
reference to a shared object, it always returns the same object (if
it is successful), regardless of how many times you call it. Consider
the following short script:
r1_so = SharedObject.getRemote("UsersData", nc.uri);
r2_so = SharedObject.getRemote("UsersData", nc.uri);
r1_so.synchronized = false;
trace(r2_so.synchronized); // false
Both r1_so and r2_so refer to
the same remote shared object instance and have identical
properties.
|
|
8.2.3. Updating Objects and Arrays in Shared Object Slots
When a shared object's
slot contains an object or array, modifying an element in the object
or array causes the entire contents of the shared
object's slot to be sent to FlashCom and copied to
all the remote instances of the shared object. For example, if a
property of a shared object named line_20 contains
an array, the following statement causes the entire array to be
copied to every remote copy of the shared object:
remote_so.data.line_20[4] = 3;
This may sound very inefficient, and at times it is.
 |
It is often necessary to design shared objects differently than you
might design local objects in order to avoid large updates caused by
small changes in data.
|
|
However, shared objects are not updated as soon as a change occurs.
They are updated once within a set time interval and only if a change
has occurred. Often this reduces the amount of redundant data being
sent when many changes are made on an object or array in a slot.
Update intervals are described in "Updates and Frame
Rates" later in this chapter.
Reassigning the same value to an existing shared object slot will not
cause an update. However, assigning new arrays or objects containing
identical values will cause an update:
// Changing the value in a shared object slot causes an update.
remote_so.data.ball_1_x = 2;
// Assigning the same value again will not cause an update.
remote_so.data.ball_1_x = 2;
// Changing the array in a slot causes an update.
remote_so.data.line_20[4] = 4;
// Assigning the same value that is already there does not.
remote_so.data.line_20[4] = 4;
var userName = "peldi";
var user1 = {fullName: "Giacomo Guilizzoni", status: "Online"};
var user2 = {fullName: "Giacomo Guilizzoni", status: "Online"};
// When adding a new property, the slot will be created on all copies.
remote_so.data[userName] = user1;
// Assigning a different object with identical contents will force an update.
remote_so.data[userName] = user2;
When an object or array in a slot is updated, a new copy of that
object or array is created in all the remote shared objects (except
in the movie where the slot was updated). The old object or array is
replaced with a new one. This can produce surprising results if you
are using a reference to an object or array in a shared object slot.
For example, if one movie creates a reference to an object in a
shared object slot this way:
// Movie #1 gets a reference to an object in a slot.
var peldi = this.data.peldi;
then another movies updates the same slot in its copy this way:
// Movie #2 changes the status property of the object in the same slot.
this.data.peldi.status = "Offline";
When the update is complete, the peldi object will
not refer to the same object as the one stored in
remote_so.data["peldi"]. After the update, the
following test will produce the results listed after each statement:
// Movie #1 now has two separate objects with a different peldi property.
trace(peldi == this.data.peldi); // false
trace(peldi.status); // Online
trace(this.data.peldi.status); // Offline
To fix this problem, the old reference should be overwritten with a
reference to the new object once the change has occurred (see the
next section for how to detect a slot change):
// Movie #1 retrieves the most recent object in the slot.
peldi = this.data.peldi;
The ActionScript garbage collection process will dispose of the old
object.
8.2.4. onSync( ) and Managing Change
Whenever the content of a shared object slot is created, updated,
or deleted in one movie, the changes are sent to the server. If the
update is successful, every movie connected to the same shared object
will eventually have its copy of the slot updated. Updates occur at
set intervals (and only if changes were made), so several slots may
be updated in one batch. When the updates for one interval are
complete, the onSync( ) method of the shared
object is called and passed an array of information objects. Each
information object contains a code property that
indicates what happened (usually either a slot has changed or all
slots have been deleted). Information objects may also have a
name property, which contains the slot name, and
an oldValue property, which contains the
slot's value just before the slot was updated. When
a movie attempts to add a new slot to a shared object or update an
existing slot, the change will be either accepted or rejected by the
server. The supported code values
are described in Table 8-1.
Table 8-1. Properties of information objects passed to onSync( )|
Code
|
Description
|
Name
|
oldValue
|
|---|
|
"success"
|
Indicates a slot change by the current movie was accepted. Other
movies receive a "change" event for
this slot.
|
Name of the new or updated slot
|
None
| |
"change"
|
A slot was changed or added by another movie or by a server-side
script. The first time onSync( ) is called after
connecting to the shared object, existing values on the server are
copied to the connecting movie's shared object.
|
Name of the new or updated slot
|
Indicates previous value (or none if a new slot)
| |
"delete"
|
The local movie or a remote movie deleted a slot (sent to all movies
connected to the shared object).
|
Name of the deleted slot
|
None
| |
"reject"
|
Server rejected a movie's attempt to change a shared
object slot. This can happen when there is contention between movies
to update a shared object or when a locally and remotely persistent
shared object connects.
|
Name of the rejected slot
|
Indicates previous value
| |
"clear"
|
Indicates the shared object has been clearedthat is, it had no
slots before the current set of updates. Occurs when
onSync( ) is called for the first time after
synchronizing with a temporary shared object or when a persistent
shared object has gotten so out of sync with the
server's copy of the shared object that it has to be
clearedall its slots were deleted.
|
None
|
None
|
Even though the array passed into onSync( )
contains information objects that may contain any of the codes in
Table 8-1, some or even all of them can often be
ignored. Example 8-3 contains the definition of a
short onSync( ) method that simply deletes the
contents of a ListBox component and then repopulates it using shared object data.
Example 8-3. Tying together a shared object and a listbox
Chat.prototype.init = function (nc) {
this.userList_so = SharedObject.getRemote("UserList", nc.uri);
this.userList_so.owner = this;
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]);
}
};
this.userList_so.connect(nc)
};
One reason for this simple approach is that the ListBox component
does not provide a way to efficiently search for, update, and delete
elements in the data provider array in which it stores list items.
So, we call its removeAll( ) method to delete
all the items and then use a for-in loop to
repopulate the listbox using the addItem( )
method. In Example 8-3, the name and value of each
shared object property are passed into addItem(
) as the label string and
data parameters. See Example 13-1
and Chapter 15 for more efficient ways to update
ListBox and DataGrid components with shared object data. Creating and
managing shared objects within another object, such as the
Chat object, is a common practice. In Example 8-3, this statement adds a reference in the shared
object to the Chat object for later use:
this.userList_so.owner = this;
Since the owner property is not assigned to the
data property of the shared object, it is simply a
private property of the shared object and will not cause a shared
object update. Within the shared object's
onSync( ) method, it is used to get the
Chat object and its properties:
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]);
}
};
Within onSync( ), the keyword
this refers to the shared object and not the
Chat object. So this.owner
refers to the Chat object and
this.data refers to the shared
object's data object. Example 8-3 shows only how to repopulate a listbox after a
shared object's data has been updated. You may be
wondering how the user-list data gets populated in the first place.
The code that adds and deletes the properties of the user list is
shown in Example 8-5. In that example, the user list
shared object's properties are set using Server-Side
ActionScript as clients connect and disconnect from an application
instance. Server-side scripting of shared objects is described under
Section 8.4
a little later in this chapter.
8.2.5. Yet Another Shared Ball Example
A shared ball sample application in
which users drag around a movie clip so that other users see it move,
has become the FlashCom equivalent of "hello
world" for introducing shared objects.
Macromedia's version of this application can be
found in the server's
samples/tutorial_sharedball directory. It
displays real-time shared object updates as the ball is dragged
around on the Stage, including responding to those updates when
someone else drags the ball around.
Example 8-4 is a slightly more involved version of
the same basic idea. A shared object, named
BallPosition, contains the x and y coordinates of
a shared ball movie clip. The slot names are named after the movie
clip. If the shared ball movie clip is named
ball_1_mc, the x and y coordinate slots are named
ball_1_mc_x and ball_1_mc_y.
When a copy of the shared ball movie first starts, the ball is
colored red to indicate that its shared object is not synchronized.
It can still be moved around on stage without any effect on other
movies. After the movie connects and the shared object is
synchronized for the first time, the ball turns green. If one or more
movies are already connected and their movie clips of the shared ball
are already synchronized, the most recently connected ball jumps to
the latest position defined in the shared object. If no other movies
are connected, the ball's current position is saved
in the shared object. From then on, when the movie clip is dragged
around the Stage, the following events occur:
The global x and y coordinates of the mouse are retrieved and clipped
to the rectangular area the ball is allowed to move within. The shared ball movie clip is moved to the x and y coordinates. The BallPosition shared object's
slots are updated with the new x and y coordinates of the movie clip,
and the changes are sent to other connected movies. The data objects of all movies are updated, after
which the onSync( ) method of every copy of each
movie's shared object is called. The
onSync( ) method in the movie that changed the
ball position is passed a list of slots with information objects
(normally one for the x position and one for the y position) with a
code value of
"success." The other movies receive
a code value of
"change". The movies that receive a "change"
notification move the ball movie clip to the coordinates in the
shared object.
In summary, if one movie changes the x and y coordinates in the
shared object, all the movies are notified of the change. The movie
that made the change is notified that the change was successful, and
the movies that did not make the change are notified that the data in
their copy of the BallPosition shared object has
changed. In response to the notification, the movie that made the
change does not have to do anything. It has already placed the movie
clip of the ball in the correct place. Every movie that receives a
"change" notification must move the
ball movie clip to its new location on stage.
What happens if two or more movies try to move the ball at the same
time? One movie will be successful, and all the
other's changes to the shared object will be
rejected. Instead of a "success"
code, a value of
"reject" is received in the
information objects in the list; the movie receiving the rejection
must update the clip position to match the location stored in the
shared object from the last movie that moved the ball.
Like Example 8-3, Example 8-4
places the code that gets and manipulates the shared object within
another object. In this case, the SharedBall
class defines the behavior of a shared ball movie clip and contains
all the shared object code. The code in Example 8-4
is the complete listing for the SharedBall
class, but the listing does not show the complete working example,
which is available on the book's web site. When a
ball is created in the movie, it is passed a reference to the
NetConnection object that has just connected to
the server. The SharedBall class takes it from
there. Note that Example 8-4 uses some AS 2.0
syntax. The book's web site also
contains an AS 1.0
version of this example.
Example 8-4. A SharedBall class containing a shared object
class SharedBall extends MovieClip {
var xSlot:String; // Slot holding _x location of the shared ball
var ySlot:String; // Slot holding _y location of the shared ball
var bounding_mc; // Initially, the name of the parent clip's bounding area
var left:Number, right:Number, top:Number, bottom:Number; // Travel limits
var so; // SharedObject, but not typed, so we can add the synchronized property
var color:Color;
var ballColor_mc:MovieClip;
/* SharedBall constructor function. The bounding_mc property may be set
* in the property dialog box or within the init object passed to attachMovie( )
* when this clip is created.
*/
function SharedBall ( ) {
// Save the name of the slots this shared ball will use in the shared object,
// such as ball_1_mc_x and ball_1_mc_y
xSlot = _name + "_x";
ySlot = _name + "_y";
// Set the left, top, right, and bottom properties that act as movement limits.
setTravelLimits( );
// Get a color object for the movie clip that gives the overall clip its color.
color = new Color(ballColor_mc);
color.setRGB(0xff0000);
}
/* The registration point for this clip is the center of the ball.
* The radius is, therefore, half the width or half the height.
* Keeping the center one radius length from the bounding box's edge
* keeps the ball entirely within the bounding box.
* If the bounding_mc property is the name of a clip in the parent,
* use it as the bounding box; otherwise use the Stage as the bounding box.
*/
function setTravelLimits ( ) {
bounding_mc = _parent[bounding_mc];
if (!bounding_mc) {
bounding_mc = {_width: Stage.width, _height: Stage.height};
}
var radius = _width / 2;
left = bounding_mc._x + radius;
right = bounding_mc._x + bounding_mc._width - radius;
top = bounding_mc._y + radius;
bottom = bounding_mc._y + bounding_mc._height - radius;
}
/* onConnect( ) is called after the movie has connected to the application
* instance. It is passed a reference to the connected NetConnection object.
* It sets up the BallPosition shared object.
*/
function onConnect (nc) {
// Get the RSO named BallPosition
so = SharedObject.getRemote("BallPosition", nc.uri);
// Create a private flag property named synchronized
so.synchronized = false;
// Create a private reference, named owner, to the Ball object.
so.owner = this;
// Define the onSync( ) handler.
so.onSync = function(list) {
// If synchronized is not set, this is the first call.
if (!this.synchronized) {
this.synchronized = true;
// Tell the ball clip we have just synchronized.
this.owner.onSynchronized( );
}
/* If any movies have changed the ball's position, tell the
* ball clip that a change has occured. We do not need to watch
* for "success" codes here, as the ball has already been moved
* by this movie when a "success" code is received. If an update
* is rejected, move back.
*/
for (var i in list) {
if (list[i].code == "change" || list[i].code == "reject") {
this.owner.onChange( );
break;
}
}
}
// Don't forget to connect to the shared object!
so.connect(nc);
}
// Color the ball green when the ball is synchronized.
function onSynchronized( ) {
// Turn green to show the ball is synchronized to the shared object.
color.setRGB(0x00ff00);
// If there are no _x and _y values in the shared object
// for this ball, set them.
if (typeof so.data[xSlot] == "undefined") {
so.data[xSlot] = _x;
so.data[ySlot] = _y;
}
}
// Change the ball position--see so.onSync( ).
function onChange( ) {
_x = so.data[xSlot];
_y = so.data[ySlot];
}
// Color the ball red when the network connection is closed.
function onDisconnect ( ) {
color.setRGB(0xff0000);
}
/* When the user clicks on the ball movie clip, define an onMouseMove() method
* to move the ball within the travel limits defined by the left, top, right,
* and bottom properties. Then update the shared object with the new position.
*/
function onPress ( ) {
onMouseMove = function( ) {
// Get the mouse position.
var x = _root._xmouse;
var y = _root._ymouse;
// Make sure x and y are within their travel limits.
if (x < left) {
x = left;
} else if (x > right) {
x = right;
}
if (y < top) {
y = top;
} else if (y > bottom) {
y = bottom;
}
// Move the clip to (x, y), and store the coordinates in the shared object.
so.data[xSlot] = _x = x;
so.data[ySlot] = _y = y;
}
}
// Stop dragging when the mouse is released over the clip.
function onRelease ( ) {
delete onMouseMove;
}
// Stop dragging when the mouse is released outside the clip.
function onReleaseOutside ( ) {
delete onMouseMove;
}
}
Inside the SharedBall.onMouseMove( ) method are
two statements that attempt to both set both the position of the
shared ball movie clip and update the shared object:
so.data[xSlot] = _x = x;
so.data[ySlot] = _y = y;
In the code snippet, the x and
y variables contain the coordinates to which to
move the ball. Sometime after these statements are executed, the
onSync( ) method of the
SharedBall clip is called and this short
for-in loop checks each information object in
the list:
for (var i in list) {
if (list[i].code == "change" || list[i].code == "reject") {
this.owner.onChange( );
break;
}
}
Inside the onSync( ) method,
this refers to the shared object. The
code property of each information object in the
list is checked for the value
"change" or
"reject". If either value occurs,
the SharedBall clip is told to change its
position to the coordinates currently in the shared object. The
onSync( ) method notifies the
SharedBall clip that a change has occurred by
calling a method of its owner object, which is in
fact the SharedBall clipsee the
SharedBall.onConnect( ) method, which sets the
shared object's owner property to
refer to the SharedBall clip. Here is the
SharedBall.onChange( ) method that sets the
position of the ball:
function onChange ( ) {
_x = so.data[xSlot];
_y = so.data[ySlot];
}
In this example, the SharedBall class contains
the shared object and is completely responsible for creating,
updating, and responding to it.
Example 8-4 also fixes a small bug in the shared
ball sample that ships with FlashCom versions 1.0 and 1.5. The
onMouseMove( ) method in the Macromedia example
sets the location of the movie clip in the shared object before
checking to see if the mouse cursor has moved off stage. You can
therefore drag and release the ball off stage and it will sometimes
stay completely out of view where you can no longer click on it to
drag it around. If you quickly drag the ball off stage and release
it, the shared object position is set to the offstage position, but
the movie clip's position is set back to being on
stage. However, when onSync( ) is called, it
moves the clip to whatever coordinates are in the shared object. To
correct the bug, make the shared object update the last statements in
the onMouseMove( ) method as shown in
Example 8-4.
 |