Traditionally, Macromedia Flash adds motion and audio to web
sites. With the release of Flash MX Macromedia is clearly trying to
position Flash as a general-purpose client-side development
environment suitable for the enterprise. For example, they now
market a server extension for Java and .NET based servers that allow
Flash clients to invoke web services via SOAP (See Flash MX, Remoting, ASP.NET and Web Services by Pallav Nadhani). Wide acceptance of Flash as an
enterprise client seems a way off. In the near term developers are
hearing a more mundane and frequent Flash-related request: to
make Flash work like ASP and ASP.NET web pages and persist values to
and from the server.
This article provides a generic
persistence framework for Flash to let developers transparently save
and load Flash movie values to and from an SQL Server database. It
has advantages such as the ability to selectively store variables on
a "per user" scope or "application" scope. Also, the framework
provides both ASP and ASP.NET implementations of the server-side
code so you can experiment whether you’ve made the leap to .NET or
not. Finally, the solution points to techniques you could use to
achieve a more primitive form of the remote function calling
provided by Macromedia’s new Flash MX Remoting product – without
incurring the additional cost for a server license and without
requiring you to make the leap to .NET.
The generic Flash persistence framework provided in this article
serves three audiences:
- Server-side developers who want to provide their client-side
Macromedia Flash-based colleagues with a consistent technique for
saving and loading variables for their Flash movies.
- Development organizations that often deal with Flash
development and want a standard method for persistence
- ASP/ASP.NET developers who are new to Flash and wish to
explore the ways Flash can interact with server-side code without
requiring Flash MX Remoting or .NET
This project assumes you have at least some experience with Flash
and that you are using FlashMX. It has been tested with FlashMX,
though the loadVariables routine it
uses is available in Flash 4 and beyond so it may work in earlier
versions of Flash. (If you have these versions of Flash please let
me know if it worked!) The project also requires access to an SQL
Server environment as well as an IIS server with ASP 3.0 or ASP.NET.
(The server-side code could easily be rewritten to run against a
different database environment.)
This project involves three sets of code:
- An .AS ActionScript file to include with movies that
need to persist their state
- An ASP, and functionally identical ASP.NET, page that
facilitates the state-related calls from Flash movies using the
provided ActionScript file
- An SQL Server database and stored procedures for loading and
saving Flash movie variables
Flash Scope and the Basic Persistence
Strategy
Flash uses ActionScript, an interpreted JavaScript-like language
that tries to conform to the ECMA-262 language definition. So, a
C-style syntax is followed, including curly braces, semicolons to
close statements, case-sensitivity, etc. Like VBScript, variables
type is inferred based on context by the runtime. ActionScript
supports objects and uses dot notation to refer to object methods
and properties. (The entire ActionScript Dictionary is freely available online
for your examination.)
A brief review of Flash variable scope will help us understand
the solution provided. The "uber container" object in Flash is the
movie object. The movie object is serialized within the .swf that
your browser downloads so the Flash player can execute it. Flash is
animation and time-line oriented and movies include a main time line
and other playback associated structures and methods. Movies can
contain "mini" movie object instances called movie clips in
Flash. So when Flash developers say "the movie" they mean the root
movie object and timeline at _level0.
(More on _level0 in a moment!)
Otherwise they will refer to "a clip", and mean a movie clip
instance, which is a property of a movie. Movie clips, like movies,
have timelines, but can only exist within a parent movie object.
Additionally, a Flash movie can load one or more external movies
( .swf files) into its context and these movie instances can
interact. The first loaded movie lives in a scope context named
_level0. Each movie file loaded after
that is typically enumerated, that is, _level1, _level2 , etc….
Macromedia Flash MX. Note the timeline structure at the top.
Another scope related keyword, _root, resolves the main timeline scope of
the movie that referenced the code. So a reference to _root in _level1 gets the main movie scope of _level1 and so on. If your movie uses levels
and movie loading you must understand the full implications of
Flash’s scope and naming strategies. NOTE: If you are using multiple
levels you must use the proper level notation for the included code
framework to run properly, specifically, make sure your resolving
level scope as you intended.
Obviously, any generic mechanism for persisting Flash values to a
database needs to address the variety of scope boundaries in Flash
discussed above, including clips, object instances and levels. As
we’ll see, the provided code meets this basic need. (For more
information on scope and levels in Flash see "Differences between _root and _level0" and "ActionScript Coding Standards".)
The important thing to understand is that movies and their
embedded object instances, like movie clips, have properties. We can refer to these properties
by name and, using the enclosed framework, we can save the values of
these properties to the database and load them from the database.
So, as we’ll see, the provided framework operates by writing the
following to the server-side database:
- A string representing the scope path to the variable, stored
in the targetMovie column in the
database
- A string representing the variable name itself, stored in the
varName column in the database
- A string holding the value for the variable, stored in the
varVal column in the database
Since the whole scope path is persisted with the other variable
information we can later use these values, when retrieved from the
database, to set the appropriate value of the appropriate runtime
object instance! So, looking all the way through the various
solution layers to the data storage model, we arrive at a very
simple SQL Server data model that looks like the following:
The flashApplets table has a single
table with a single column called flashAppletID. The flashAppletID column is a string that
uniquely identifies a particular Flash movie and matches a variable
of the same name in the Flash movie. A single database can store
all the variables for any number of Flash movies created by
your developers for any number of clients. You could even add a
client name column to the flashApplets
table if this would help.
The flashState table contains the
variable names and their state. Note the column targetMovie, which is used to store the
scope path information for the variable, such as _level1.someClip. The varName is the discrete variable name itself
and the varVal is self-explanatory.
The userID column allows the framework
to store different properties for the same variable for different
users. The userID column is a varchar column. I would expect developers to
use an email address or user name to uniquely identify users. The
userID column is associated with the
varScope column. The varScope column defines whether the variable
is "application scoped" or user scoped.
We’ll return to the database and its attendant stored procedures
after turning to the meat of the Flash code.
Building on loadVariables
Flash communicates with server-side code using HTTP GET or POST
requests. The basic function, to communicate with server-side
script or files is named loadVariables, and looks like this: loadVariables(url, target, postingType);
The url parameter points to the
sever script that will be called by loadVariables using the postingType, GET or POST.
The functions which rely on loadVariables in the provided code set the
posting type to POST, which can,
theoretically, handle larger text strings than GET requests. The target parameter refers to the movie clip
where the variables returned by the server will be loaded.
The loadVariables function works by
sending all the variables in the scope of the movie that
invoked the function as name/value pairs in standard MIME format
(application/x-www-form-urlencoded).
So, if SomeMovieClip calls loadVariables, all the variable name/value
pairs in scope for SomeMovieClip are sent to the server in a
GET or POST request as one long MIME encoded
string. If the server-side script returns any MIME-encoded name value pairs in
response, those values are persisted into the movie clip that was
specified as the target in the loadVariables function call. This send and
receive capability is the glue that can bind any Flash code to
server-side routines and it sits at the heart of the provided Flash
code.
(Note: As a nod to security, a Flash movie will only call URLs
local to, or subordinate to, the URL in which it resides.
This is to prevent Flash movies from crossing server or security
boundaries. It means your ASP or ASP.NET pages will need to be in or
below the web directory from which your Flash file is served. The
SQL Server that actually stores the variables can, naturally, be
practically anywhere.)
The Demo Movie and FlashState.AS File
The flashState.fla demo movie. The black selected text
represents the starting code, defined below, that #includes the flashState.as ActionScript
routines, sets name of the Flash movie as defined in the database
and sets the URL that points to the server-side scripts. Note the
very first frame of the main movie is selected, meaning the selected
script will be the first to execute in the movie.
The FlashState.AS file is an ActionScript script that
needs to be "#included" on the very
first line of the very first frame of your primary Flash movie
(_level0). This is done for you in the
provided flashState.fla demo, but you still need to set the
variables referenced in these opening lines of script as explained
below. Remember, this first bit of code needs to be in the first
frame of the first movie in order for the routines to work.
Your first few lines of Flash code should look like the
following: #include "flashState.as"
persistenceScriptURL="http://127.0.0.1/flashState/flashState.aspx";
flashAppletID="flashStateTest";
The first line includes the ActionScript file with the
persistence functions detailed below.
The persistenceScriptURL value must
point to your server-side page, either the ASP or the ASP.NET
equivalent.
The flashAppletID is a string that
must previously exist as a row in the flashApplets table. The flashAppletID value uniquely identifies the
Flash movie making the call to the server and all the state
variables are stored in association with this value. In the database
flashAppletID is a primary key, and is
specified for two reasons. First, it allows the solution to provide
state storage for any number of named Flash movies. Secondly, it
provides a minimal level of security to prevent the arbitrary
posting and retrieving of values via the server-side pages; if the
code doesn’t know the name of an existing applet, it can’t post or
retrieve. Again, this means you must manually enter this string in
the FlashApplets table in order to "activate" persistence for your
applet (see the SQL section below for more details).
The Example Movie
The example movie, flashState.fla, is laid out to
demonstrate the use and functionality of the code across the three
scope boundaries we previously discussed, including "local" (example
1), from within a clip instance (example2) and in relation to an
external movie (example3). The FlashState.swf example movie
provides some basic feedback about its operation in the very top of
the movie’s interface. Before you run the examples by clicking the
buttons, type a username in the userID
field. This will set the userID value
in the movie so that values specified as user-scoped will be stored
appropriately.
flashState.fla movie
Example 1
In the Example 1 section of the movie the text field control
labeled rootVar is bound to a root
scoped variable of the same name. The three buttons let you Save All Variables, Load All Variables and Unmark the rootVar.
We first "mark" the variable using the provided MarkVar function. MarkVar stores the scope path and variable
name to an array so that the saving and loading functions provided
can act in a "batch" mode when called later. The following Flash
code sets up example 1 and can be found in the first frame of the
actions layer on the main movie timeline of flashState.fla: // try a root level variable here for Example 1
var rootVar;
rootVar="starting value";
_root.MarkVar("_root","rootVar",true);
There are three event handles in the flash movie associated with
example 1, one for each button in the example. The event handler
actions are associated with the button instance objects on the
stage. To see the event handler code for a button, select the button
(not the text) and view the actions associated with that object. The
event handler code to, for example, load all
variables, looks like this: on (release)
{
trace("Load All Vars Pressed");
_root.LoadAllVars();
}
When the user clicks the button, LoadAllVars loops through an array
containing the names and scope paths of all marked variables in the
entire movie. It retrieves the stored values for each variable from
the database and sets the targeted variable to the value returned by
the database. Again, LoadAllVars
affects all variables previously marked in the current runtime
context of movie. So Example 1 demonstrates the least granular
approach to saving and loading.
If for some reason you need to exclude a variable, which you had
previously marked and saved from the persistence routines, there is
a corollary UnMarkVark function. UnMarkVar not only removes the references to
the variable in the local flash runtime context, it removes any and
all references to the variable and associated values that may exist
on the server-side database.
Example 2
The Example 2 section demonstrates the ability to cross-scope
boundaries, including movie clip instances. It also demonstrates a
more granular provided method of saving and loading, the LoadMovieVars and SaveMovieVars functions, which target a
specific movie clip instance rather than the entire Flash movie.
Since these functions target a particular movie clip instance the
clip must have an instance name in order to work. In the cases where
your clips are dynamically generated you can use the technique below
to mark the variable of the targeted clip instance, even if that
clip instance name isn’t known in advance: // we will mark
_root.markVar(this._name,"testVar",false);
_root.markVar(this._name,"testVarUser",true);
As you can see, by using this and
the _name property, this code snippet
from frame 1 of the movie clip timeline proves you don’t have to
know the name of the movie clip instance in advance. The use of
_root in the code fragment above is
appropriate only because this movie clip is known to be the
child of the main movie, the one living at _level0. If we we’re uncertain about where
this clip instance might wind up in terms of its parent movie, we
could use _level0.MarkVar instead to
ensure the function resolved correctly.
As explained, the buttons in Example 2 demonstrate the ability to
load and save using only a movie clip or scope path (like _level1) as a reference. For example: _root.LoadMovieVars("testClip1");
Even though you don’t need to know the clip instance name when
you save out, you obviously need it when you load in. In a complex
scenario involving lots of dynamically generated clip instances you
might store the clip instance names in an array and use common
string concatenation techniques save or load variables as needed.
You could also, and more simply, have each movie instance call LoadMovieVars itself and pass in this_name as the parameter. The choice will
depend on the complexity of your movie and your coding style.
Example 3
Example 3 demonstrates the ability of the code to cross Flash’s
quirky "level boundaries" (See "Flash Scope
and the Basic Persistence Strategy" above). The main movie,
residing by default at _level0, loads
a second movie in at _level1. This
second movie can fully participate in the saving and loading, as
demonstrated.
Example 3 also shows the most granular level of the save and load
functionality provided, saving and loading single variable values.
As in the previous example, the button handlers here can save and
load a single variable. In this example the buttons are in the main
movie and event handlers work by referring to the known load level
of the external movie. For example, the event handler for Save a Var references the _level1.externalVar directly and is an
action associated with the Save a Var
button instance in the main (_level0)
movie: on (release)
{
trace("Save a Variable Pressed");
SaveVar("_level1","externalVar", _level1.externalVar, false)
}
The buttons could have just as easily have been in the external
movie and referenced the SaveVar and
LoadVar functions by calling them with
a scope path that resolves the main movie, like this: _level0.SaveVar("_level1","externalVar", _level1.externalVar, false)
The Clever Bit
The mildly clever bit of ActionScript coding that makes this all
work is the use of ActionScript’s eval
function. While the eval function in
ActionScript is a little brain-dead compared to its JavaScript
equivalent it does just enough by returning a reference to any
variable in any Flash scope when fed a string that resolves to an
existing variable. If the eval
function were more like JavaScript’s and could return not only
references, but also execute the code in the strings... well, even
more elegant solutions might be possible.
The SaveMovieVars function offers a
good model of the typical strategy of the various saving and loading
functions provided.
Function SaveMovieVars takes a
string that represents a scope path to a target movie as input… function SaveMovieVars(targetMovie)
{
lastFunctionCalled="SaveMovieVars";
trace("Entering SaveMovieVars with " + targetMovie);
It then loops through the arrays where the persisted variable
names and associated scope paths and scope details are stored… [code block continues]
for (varIndex=0;varIndex<persistedVarNames.length;varIndex++)
{
If the current array element matches the scope path, the discrete
SaveVar function is called using the
properties associated with the current element. Note the use of
eval to resolve and return a real
run-time reference to the targeted variable value… [code block continues]
if (persistedMovieNames[varIndex]==targetMovie)
{
var evalString;
evalString = persistedMovieNames[varIndex] + "." +
persistedVarNames[varIndex];
saveVar(persistedMovieNames[varIndex],
persistedVarNames[varIndex],
eval(evalString),
persistedVarScope[varIndex]
);
}
}
}
flashState.as Functions
The flashState.as ActionScript file includes the following
functions:
Function |
Description |
MarkVar(targetMovie,varName,userScoped) |
"Marks" a variable for persistence
and sets its persistence scope as user or application scoped.
userScoped is Boolean, true for user scoped and false for
application scoped. |
UnMarkVar(targetMovie,varName) |
"Unmarks" a variable previously
marked for persistence. It simultaneously removes any and all
instances of the variable in the database. |
SaveVar(targetMovie,varName, varVal,
varScope) |
Saves a single variable to the
database. It is up to you to mark the variable using MarkVar
before calling it if you later want this var to participate in
the "batch" load and save functions described below like
"SaveAllVars". |
SaveMovieVars(targetMovie) |
Saves all variables associated with
a movie clip or any other named Flash scope, like "_level2" or
"_global". |
SaveAllVars( ) |
Saves all currently marked variables
for all clips and scopes to the database. |
LoadVar(targetMovie,varName) |
Loads a single variable that was
previously stored. |
LoadMovieVars(targetMovie) |
Loads all variables previously
persisted to the database into their respective movie
clips/levels. |
LoadAllVars( ) |
Loads all variables previously
persisted to the database into their respective movie
clips/levels. |
NOTE : It’s up to your Flash programmers to ensure they
have a userID variable when you want
to use user variables. The code will normalize all userID validation to lower case in the SQL
stored procedure, so even though the userID s are stored with case, the code that
checks whether the userID provided
matches the userID on file will
compare lower case versions of each.
The SQL Database and Stored Procedures
The provided code relies on an SQL Server data store and a
database that can be created using the SQL script file included in
the supporting source code. Certainly with a little elbow grease and
because of the modular approach of the code, you could rewrite the
server-side scripts to use Access, XML or whatever other persistence
medium floats your boat. You wouldn’t have to touch the ActionScript
file because it’s isolated from the SQL routines by intermediary
functions.
As described earlier, the database provided is absolutely
minimal, and is modeled as follows:
There are three stored procedures, loadVariable, saveVariable and deleteVariable. Stored procedures are used
because we can then provide a variety of server-side pages to answer
our Flash requests. This article includes ASP and ASP.NET examples,
but a PHP equivalent would be very straightforward to build if the
target web server didn’t support Microsoft server pages. The other
advantage of stored procedures is we add a little bit of SQL logic
at the data layer to make sure the appropriate value is returned
based on user or application scope, etc.
The only stored procedure that may not be self-evident in terms
of its relation to the Flash functions discussed thus far, is deleteVariable. Stored procedure deleteVariable is called by the UnMarkVar function and removes instances of
variables from the table.
The ASP and ASP.NET Server-Side Code
The ASP code (and its functionally identical ASP.NET brother) is
a paragon of the mundane. The page resolves the function to call
based on the form parameter persistMode. Consider the following fragment
from flashState.aspx.cs. It determines persistMode then executes the relevant
function. It also does some maintenance work associated with logging
the request information for debugging purposes: if(Request.Form.Count>0)
{
persistMode=Request.Form["persistMode"];
flashAppletID=Request.Form["flashAppletID"];
currentTargetMovie=Request.Form["currentTargetMovie"];
currentVarName=Request.Form["currentVarName"];
currentVarVal=Request.Form["currentVarVal"];
currentVarScope=Request.Form["currentVarScope"];
userID=Request.Form["userID"];
}
else
{
persistMode=Request.QueryString["persistMode"];
flashAppletID=Request.QueryString["flashAppletID"];
currentTargetMovie=Request.QueryString["currentTargetMovie"];
currentVarName=Request.QueryString["currentVarName"];
currentVarVal=Request.QueryString["currentVarVal"];
currentVarScope=Request.QueryString["currentVarScope"];
userID=Request.QueryString["userID"];
}
// RESOLVE REQUEST MODE
if (persistMode=="debug")
{
strResponseToFlash+=("<h1>DEBUGGING LOG</h1>");
strResponseToFlash+=(Application["test"]);
}
else
{
LogPageHit();
switch (persistMode)
{
case "save":
SaveVariable(flashAppletID,currentTargetMovie,currentVarName,currentVarVal,
currentVarScope,userID);
break;
case "load":
LoadVariable(flashAppletID,currentTargetMovie,currentVarName,userID);
break;
case "debugErase":
Application.Lock();
Application["test"]="";
Application.UnLock();
break;
case "unmark":
UnmarkVariable(flashAppletID,currentTargetMovie,currentVarName);
break;
default:
strResponseToFlash=" ";
return;
break;
}
}
If the persistMode was "save" the following function would be
invoked: private void SaveVariable(string flashAppletID, string targetMovie,string
varName, string varVal, string varScope, string userID)
{
Application.Lock();
Application["test"]+="Entered SaveVariable(" + flashAppletID + "," +
targetMovie + "," + varName + "," + varScope + "," + userID + ")<br>";
Application.UnLock();
// set up our sql related strings
string strConnection = "user id=user;password=pwd;";
strConnection += "initial catalog=flashState;data source=(local);";
strConnection += "Connect Timeout=30";
string strSql = "saveVariable";
// load the variable from sql
try
{
// get a connection
SqlConnection con = new SqlConnection(strConnection);
//@flashAppletID nvarchar(255),
//@targetMovie nvarchar(255),
//@varName nvarchar(255),
// @userID nvarchar(255)
SqlCommand cmd = new SqlCommand(strSql,con);
cmd.CommandType=CommandType.StoredProcedure;
SqlParameter sqlparam;
sqlparam = cmd.Parameters.Add("@flashAppletID",SqlDbType.VarChar,255);
sqlparam.Direction = ParameterDirection.Input;
sqlparam.Value=flashAppletID;
sqlparam = cmd.Parameters.Add("@targetMovie",SqlDbType.VarChar,255);
sqlparam.Direction = ParameterDirection.Input;
sqlparam.Value=targetMovie;
sqlparam = cmd.Parameters.Add("@varName",SqlDbType.VarChar,255);
sqlparam.Direction = ParameterDirection.Input;
sqlparam.Value=varName;
sqlparam = cmd.Parameters.Add("@varVal",SqlDbType.VarChar,255);
sqlparam.Direction = ParameterDirection.Input;
sqlparam.Value=varVal;
bool blnVarScope;
if((varScope=="true") || (varScope=="1"))
{
blnVarScope=true;
}
else
{
blnVarScope=false;
}
sqlparam = cmd.Parameters.Add("@varScope",SqlDbType.Bit,1);
sqlparam.Direction = ParameterDirection.Input;
sqlparam.Value=blnVarScope;
sqlparam = cmd.Parameters.Add("@userID",SqlDbType.VarChar,255);
sqlparam.Direction = ParameterDirection.Input;
sqlparam.Value=userID;
con.Open();
cmd.ExecuteNonQuery();
con.Close();
}
catch(Exception e)
{
Application["test"]+="<br><b>ERROR:" + e.Message + " in " +
e.Source + "<br>" + e.StackTrace + "</b></br>";
strResponseToFlash=" ";
return;
}
}
In the case of loadVariable, the
page returns a single name value pair in MIME format, which is bound
by Flash to the appropriate clip or scope. In all other cases except
where persistMode is "debug" or "debugErase", the page returns no strings so
as not to inadvertently set a Flash variable.
The only interesting bits here are extra debugging bits. Details
about every call to the page are aggregated in an application scoped
string variable. You can view this information by using a web
browser, typing in the URL of the page, and setting the persistMode attribute of the querystring to
"debug". For example: http://localhost/flashState/flashState.asp?persistMode=debug&WROXEMPTOKEN=1103385Z3YqbAwJHRpbmbQSmfM
This will give you loads of information about the page requests.
This is all necessary because Flash will tell you almost nothing
about its call to the server, even when there are errors. You can
use Flash’s trace function to write
Flash values to the screen, but you can’t use Flash’s trace to
easily render the debugging or error information returned by the
server side pages. Moreover, in the interest of not inadvertently
corrupting the Flash movie by having ASP/ASP.NET debugging info
construed as MIME values, the server-side script only returns values
when the script operated normally. All debugging and error codes are
trapped and only available via the persistMode=debug parameter viewable through
your web browser.
Trust me, you’ll be using this feature. There is even a Boolean
value you can set in the page that will add a full enumeration of
the querystring and the form values submitted. If you want to wipe
the slate clean set persistMode=debugErase in the querystring
then try again. And for those who are paranoid about server
resources, the code automatically deletes the error "report" when
the string hits 10000 characters in size.
Extending the Framework
Currently the framework only supports ASP and ASP.NET server-side
scripts working against an SQL data store. I can imagine someone
will want to redo the data layer and persist to, for example, one or
more XML files, perhaps one file per "flashApplet". You could also
repurpose the ASP and ASP.NET code to equivalent server-side
routines in PHP or ColdFusion and have a single Flash persistence
framework that can operate well beyond the Microsoft server
landscape and in practically any server context a client might throw
at you. If done properly, the client side flash code should remain
unchanged regardless of how many server-side variants you
develop.
For those heavily invested in Flash, one might consider a full
Flash object serialization strategy. It seems more reasonable to me,
to persist simple object properties, but there may be those that
want to take it even farther and be able to "save" a movie clip
instance, for example, against the data store. I’m not sure that
ActionScript has the sufficient reflection capability to really pull
this off but some might find it worth exploring.
I look forward to hearing where you take the framework and how
you put it to work!
Application Setup
- Run the flashState.sql SQL script to install tables and
stored procedures on your server
- Make sure to set permissions on both tables and stored
procedures
- Create a web directory to hold the flash movie and the
ASP/ASP.NET pages
- Make sure you #include the
flashState.as file and initialize your persistenceScriptURL and flashAppletID values as discussed herein
- Run the Flash demo ( flashState.swf ) and step through
the movie in Flash to see how it works.
- Enjoy!
Summary
Flash is a growing presence on the web. The included code shows a
nice generic framework for binding your Flash movie properties to a
server-side database. In our Interactive Media program at Columbia
College Chicago, we are using this technique to quickly bootstrap
new Flash developers (college students) into working with
"persistent applications" of Flash. I hope it proves as useful to
you and look forward to your feedback. |