14.5 Flash Remoting Code

The ActionScript code used to contact the remote services is contained in several files described in the following sections. One caveat when working with Flash Remoting is that the remote calls are asynchronous. This creates problems when you're trying to separate the logic in your applications, as discussed in Chapter 12. In our application, we decided to keep the logic simple by implementing the UI logic from within our responder objects. In other words, when a remote call returns a result, the responder object takes care of the details of updating the display.

14.5.1 RemotingInit.as

The RemotingInit.as file contains all of the Flash Remoting initialization code needed for the movie. The code is identical to the code you've seen throughout the book, but in this case we are creating three distinct service objects: one for the UserService, one for the ScriptService, and one for the SiteService. The code is shown in Example 14-7.

Example 14-7. Remoting initialization in the RemotingInit.as file
// Remoting initialization
if (initialized == null) {
  initialized = true;
  NetServices.setDefaultGatewayUrl("http://localhost/flashservices/gateway");
  my_conn = NetServices.createGatewayConnection( );
  // Create the service objects
  UserService = my_conn.getService(
                        "com.oreilly.frdg.ScriptRepository.UserService");
  ScriptService = my_conn.getService(
                        "com.oreilly.frdg.ScriptRepository.ScriptService");
  SiteService = my_conn.getService(
                        "com.oreilly.frdg.ScriptRepository.SiteService");
}

// Major error handler, usually a connection is bad, so the movie will fail
System.onStatus = function (error) {
  errorHandler("There was a connection failure");
};

// General error handler for entire movie
function errorHandler (message, callbackFunction) {
    alertBox("errorHandlerAlert", message, callbackFunction);
};

The code also includes the general System.onStatus( ) method to handle any catastrophic errors, such as a connection failure. Also, a general error handler is defined that will be used in all onStatus( ) methods for responder objects in the movie. By centralizing the error handling we can easily customize the message presented to the user. During development, we are simply displaying the return message. During debugging, we might want to trace some debugging information. At some later point, we can change this function to present a more meaningful error to the user.

14.5.2 ScriptObject.as

The ScriptObject.as file contains the definition for the ScriptObject class. The complete ScriptObject.as file is shown in Example 14-8. As you can see, the client-side ScriptObject contains the same properties as the ScriptObject that was created in the server-side code in Example 14-5. The object is passed back and forth when necessary to simplify operations on both ends. When passed to the server, the properties become arguments of the ColdFusion function. When passed back to the client, the properties are copied to the current instance of the ScriptObject using the private _copyProperties( ) method.

Example 14-8. The ScriptObject class
/*
ScriptObject  an object with all properties needed to add
the script to the database.
Properties:
  ScriptID
  ScriptName
  ScriptDescription
  ScriptCode
  LanguageID
  CategoryID
  UserID
  DateUploaded
  DateModified
  VersionMajor
  VersionMinor
  VersionMicro
  ScriptUniqueID
Methods:
  init          initialize the object.
  addScript     call the remote method to add a script to the DB
  updateScript  call the remote method to update a script in the DB
  test          debugging method that is used to make sure object is returning
                from the remote method as an object of type ScriptObject
  onResult      Responder method for the object's remote calls
  onStatus      Responder error method for object's remote calls
  _copyProperties  "Private" method to copy the properties from remote call
                to the current instance of a ScriptObject
  toString      debugging method to display the current script's properties
*/

function ScriptObject (id) {
  if (!this.inited)
    this.init(arguments);
}

ScriptObject.prototype.init = function (args) {
  this.inited = true;    // Instance is initialized
  this.ScriptID          = (args[0] != undefined) ? Number(args[0]) : 0;
  this.ScriptName        = (args[1] != undefined) ? args[1] : "";
  this.ScriptDescription = (args[2] != undefined) ? args[2] : "";
  this.ScriptCode        = (args[3] != undefined) ? args[3] : "";
  this.LanguageID        = (args[4] != undefined) ? Number(args[4]) : 0;
  this.CategoryID        = (args[5] != undefined) ? Number(args[5]) : 0;
  this.UserID            = (args[6] != undefined) ? Number(args[6]) : 0;
  this.DateUploaded      = (args[7] != undefined) ? args[7] : "";
  this.DateModified      = (args[8] != undefined) ? args[8] : "";
  this.VersionMajor =
       (args[9] != undefined && args[9] != "") ? Number(args[9]) : 0;
  this.VersionMinor =
       (args[10] != undefined && args[10] != "") ? Number(args[10]) : 0;
  this.VersionMicro =
       (args[11] != undefined && args[11] != "") ? Number(args[11]) : 0;
  this.ScriptUniqueID = (args[12] != undefined) ? args[12] : "";
};

Object.registerClass("ScriptObject", ScriptObject);

// Define a toString( ) function for reading the object
ScriptObject.prototype.toString = function ( ) {
  var temp = "inited:  "         +  this.inited + '\n';
  temp += "ScriptID:  "          +  this.ScriptID + '\n';
  temp += "ScriptName:  "        +  this.ScriptName + '\n';
  temp += "ScriptDescription:  " +  this.ScriptDescription + '\n';
  temp += "ScriptCode:  "        +  this.ScriptCode + '\n';
  temp += "LanguageID:  "        +  this.LanguageID + '\n';
  temp += "CategoryID:  "        +  this.CategoryID + '\n';
  temp += "UserID:  "            +  this.UserID + '\n';
  temp += "DateUploaded:  "      +  this.DateUploaded + '\n';
  temp += "DateModified:  "      +  this.DateModified + '\n';
  temp += "VersionMajor:  "      +  this.VersionMajor + '\n';
  temp += "VersionMinor:  "      +  this.VersionMinor + '\n';
  temp += "VersionMicro:  "      +  this.VersionMicro + '\n';
  temp += "ScriptUniqueID:  "    +  this.ScriptUniqueID;
  return temp;
};

// Methods are simple interfaces to the remote methods
ScriptObject.prototype.addScript = function (service) {
  service.addScript(this, this);
};

ScriptObject.prototype.updateScript = function (service) {
  service.updateScript(this, this);
};

// Debugging function to let us know that the object returned from
// the server was registered properly. In responder function, do:
//   result.test( )
ScriptObject.prototype.test = function ( ) {
  trace("ScriptObject successful")
};

//  Copy properties from an object to this (current instance of obj)
ScriptObject.prototype._copyProperties = function (from) {
  for (var prop in from) {
    if (this[prop] != from[prop]) this[prop] = from[prop];
  }
};

//  Responder function
ScriptObject.prototype.onResult = function (result) {
  var exists = false;
  if (this.ScriptID) exists = true;  // If this is an update, ScriptID exists
    // Put all properties from the result into the instance of the ScriptObject
    this._copyProperties(result);
  alertBox("alertModify", "Successful.", scriptRepositoryRefresh(this, exists));
};

// Responder error handler
ScriptObject.prototype.onStatus = function (error) {
  errorHandler(error.description);
};

ScriptObject.prototype.validate = function ( ) {
  var errorMsg = "";
  if (this.ScriptName == "")   errorMsg += "Script name must not be empty\n";
  if (this.ScriptDescription == "")
      errorMsg += "Script description must not be empty\n";
  if (this.ScriptCode == "")   errorMsg += "Script code must not be empty\n";
  if (this.LanguageID == 0)    errorMsg += "Must choose a script language\n";
  if (this.CategoryID == 0)    errorMsg += "Must choose a script category\n";
  if (this.DateUploaded == "") errorMsg += "Must choose date uploaded\n";
  if (this.DateModified == "") errorMsg += "Must include date modified\n";
  if (this.VersionMajor == "")
     errorMsg += "Script version must be in format x.x.x\n";
  if (this.VersionMinor == "")
     errorMsg += "Script version must be in format x.x.x\n";
  if (this.VersionMicro == "")
     errorMsg += "Script version must be in format x.x.x\n";
  return errorMsg;
}

The script is commented inline, but a few areas warrant further explanation. The calls to the remote service use a rather cryptic syntax, passing this as both the first and second parameters:

ScriptObject.prototype.addScript = function (service) {
  service.addScript(this, this);
};

This code passes the first argument of the current ScriptObject instance (this) as a responder object, because the object has onResult( ) and onStatus( ) methods declared on it. Flash will strip off the first argument to use as the responder, and the second argument (this) becomes the argument sent to the remote service. It is the current instance of the ScriptObject as well; all properties of the object are passed to the service.

The responder method, onResult( ), copies the properties of the result to the same instance of the ScriptObject that made the remote call. It then calls the scriptRepositoryRefresh( ) function as a callback function that will add the script to the Tree component (and to the scriptCache property shown in Example 14-9). The exists variable tells the callback function that the current script is either a new script (exists is false) or an updated script (exists is true).

The validate( ) method of the object is a general-purpose validation routine for the properties of the ScriptObject. Rather than validate the individual text fields, we wait until the call to the remote service to validate the text. By doing this, the complexities of validating various text fields and combo boxes throughout the application are eliminated; validation can be done all at once easily by invoking the validate( ) method of the object.

The init( ) method, as previously shown in Chapter 4, allows the object passed from the remote method to retain all of its properties upon instantiation, which occurs behind the scenes immediately upon return and before your ActionScript code can act on the object.

14.5.3 ScriptRepository.as

The ScriptRepository.as file is the only one included directly in our Flash movie. All other ActionScript documents are included from this file. The movie is initialized from here, and the user interface is populated from the remote methods.

The complete ScriptRepository.as file is shown in Example 14-9.

Example 14-9. The ScriptRepository.as file
// Flash Remoting include
#include "NetServices.as"
// NetDebug for debugging purposes
#include "NetDebug.as"
// Data provider for UI components
#include "DataGlue.as"
// Initialize Flash Remoting
#include "RemotingInit.as"
// Site utility functions
#include "SiteUtilityFunctions.as"
// UserObject class
#include "UserObject.as"
// ScriptObject class
#include "ScriptObject.as"
// User interface stuff
#include "UI.as"

// Set up a cache for scripts to avoid hitting the remote service again
var scriptCache = new Object( );

// Initialize the user and some other globals
_global.currentUser = new UserObject( );
_global.currentScript = 0;   // If there is a current script shown, it will be here
_global.downloadLink = "http://www.flash-remoting.com";

// General responder object for methods that return nothing
//   onResult( ) method displays a message in an alert box
//   onStatus( ) method simply calls the error handler
function GeneralResponder (theName, theMessage, callbackFunction) {
  this.onResult = function (result) {
    alertBox(theName, theMessage, callbackFunction);
  };
  this.onStatus = function (error) {
    errorHandler(error.description);
  };
}

// Set up a default "script" that contains generic labels for the interface
scriptCache["0"] = new ScriptObject(
            0,                    // scriptid
            "Script name...",     // ScriptName
            "Description...",     // ScriptDescription
            "Code...",            // ScriptCode
            0,                    // LanguageID
            0,                    // CategoryID
            0,                    // UserID
            "Date uploaded...",   // DateUploaded
            "Date modified...",   // DateModified
            "",                   // Version major
            "",                   // Version minor
            "",                   // Version micro
            "");                  // ScriptUniqueID
// Display the dummy script
displayIt(0, cnt_main_mc.cnt_view_mc);

// Search the scripts database
function searchScripts (searchWord) {
  ScriptService.displayList(new ScriptListingResponder(
                            cnt_main_mc.cnt_view_mc.scripttree_tree,
                            'containing ' + searchWord),searchWord);
  workingAlert( );   // Display a "...working" box
}

// Get all of the scripts
function getAllScripts ( ) {
  ScriptService.displayList(new ScriptListingResponder(
                            cnt_main_mc.cnt_view_mc.scripttree_tree));
  workingAlert( );   // Display a "...working" box
}

// Upon initialization, get all scripts for tree
// Tree control named scripttree_tree

// Call the remote service to display scripts
getAllScripts( );

// Responder object to populate tree with script names and IDs
function ScriptListingResponder (theTree, rootNode) {
  // Serves double duty for searches and all scripts.
  // Pass in the tree reference and a string containing the rootNode text.
  if (rootNode == undefined || rootNode == "") rootNode = "All Scripts";
  // Set a root node and open it
  var myRootNode = new FTreeNode(rootNode).setIsOpen(true);
  theTree.setRootNode(myRootNode);
  // Responder onResult() method lists the scripts and removes the "working" box.
  // The listScripts( ) function is defined within this object.
  this.onResult = function (result_rs) {
    listScripts(result_rs, theTree);
    theTree.refresh( );
    workingBox_mc.removeMovieClip( );
  };
  this.onStatus = function (error) {
    errorHandler(error.description)
  };

  function listScripts (my_rs, theTree) {     // Populate the tree
    // Set up a nested repeat using CategoryID.
    // CategoryIDs are in order, so when it changes, start a new node.
    var CatID = "";
    var catNode;
    var rootNode = theTree.getRootNode( );
    for (var i=0; i < my_rs.getLength( ); i++) {
      if (my_rs.getItemAt(i).CategoryID != CatID) {
        catNode = new FTreeNode(my_rs.getItemAt(i).CategoryDesc,
        my_rs.getItemAt(i).CategoryID);
        rootNode.addNode(catNode);
      }
      catNode.addNode(new FTreeNode(my_rs.getItemAt(i).ScriptName,
        my_rs.getItemAt(i).ScriptID));
      CatID = my_rs.getItemAt(i).CategoryID;
    }
  }
}

// Set up change handler for the Tree component
cnt_main_mc.cnt_view_mc.scripttree_tree.setChangeHandler("displayScript", _root);

// Display the script in the interface
function displayScript (tree) {
  var theNode = tree.getSelectedNode( );
  var theScriptId = theNode.data;
  _global.currentScript = theScriptId;
  if (!theNode.isBranch( )) {
    if (findItem(scriptCache, theScriptId)) {
      displayIt(theScriptId, cnt_main_mc.cnt_view_mc);
    } else {
      putScriptInCacheAndDisplayIt(theScriptId, cnt_main_mc.cnt_view_mc);
    }
  } else {
    _global.currentScript = 0; // no current script
    // Display the dummy script
    displayIt(0, cnt_main_mc.cnt_view_mc);
  }
}

// Set up change handler for the scriptname_cb in
// cnt_main_mc.cnt_modify_mc (Modify screen)
cnt_main_mc.cnt_modify_mc.scriptname_cb.setChangeHandler(
                                          "displayScriptUpdate", _root);

// Display the script in the interface
function displayScriptUpdate (cb) {
  var theScript = cb.getSelectedItem( );
  var theScriptId = theScript.data;
  _global.currentScript = theScriptId;
  if (findItem(scriptCache, theScriptId)) {
    displayIt(theScriptId, cnt_main_mc.cnt_modify_mc);
  } else {
    putScriptInCacheAndDisplayIt(theScriptId, cnt_main_mc.cnt_modify_mc);
  }
}

// Display routines for scripts
// Scripts are cached the first time they are accessed from the remote DB

// Cache the script first, then display it
function putScriptInCacheAndDisplayIt (theScriptId, theMovieClip, refreshTree) {
  var temp = new ScriptObject(theScriptId);
  // Script is not cached, so get it from the remote service
  ScriptService.getScript(
       new ScriptDisplayResponder(theMovieClip,refreshTree), temp);
}

// Default responder for the remote method getScript( ).
// The movie clip is passed to the object so that
// the display will work in View and Modify screens
function ScriptDisplayResponder (theMovieClip, refreshTree) {
  this.onResult = function (result) {
    // Get the script from the remote method
    scriptCache[result.ScriptID] = result;
    // Have to display from the responder function
    displayIt(result.ScriptId, theMovieClip);
    if (refreshTree)
      setTheScriptNode(cnt_main_mc.cnt_view_mc.scripttree_tree, result, true);
  };
}

function displayIt (theScriptId, theMovieClip) {
  // Set text fields
  theMovieClip.scriptname_txt.text = scriptCache[theScriptId].ScriptName;
  theMovieClip.scriptdesc_txt.text = scriptCache[theScriptId].ScriptDescription;
  theMovieClip.scriptcode_txt.text = scriptCache[theScriptId].ScriptCode;
  theMovieClip.scriptdateuploaded_txt.text =
                                     scriptCache[theScriptId].DateUploaded;
  theMovieClip.scriptdatemodified_txt.text =
                                     scriptCache[theScriptId].DateModified;
  theMovieClip.scriptversion_txt.text  =
                scriptCache[theScriptId].VersionMajor + '.' +
                scriptCache[theScriptId].VersionMinor + '.' +
                scriptCache[theScriptId].VersionMicro;
  // Pick values in combo boxes
  theMovieClip.scriptlanguage_cb.pickValue(scriptCache[theScriptId].LanguageID);
  theMovieClip.scriptcategory_cb.pickValue(scriptCache[theScriptId].CategoryID);
  theMovieClip.scriptuser_cb.pickValue(scriptCache[theScriptId].UserID);
  return;
}

// Refresh the script tree
function scriptRepositoryRefresh (ScriptObj, exists) {
  scriptCache[ScriptObj.ScriptID] = ScriptObj;
  setTheScriptNode(cnt_main_mc.cnt_view_mc.scripttree_tree, ScriptObj, exists);
  displayScript(cnt_main_mc.cnt_view_mc.scripttree_tree);
}

// Open a specific node of the tree.
// If the node does not exist in the tree, add it.
function setTheScriptNode (theTree, theScript, exists) {
  var theParent = theTree.getRootNode( );
  var theCategories = theParent.getChildNodes( );
  var theParentNodeId = theScript.CategoryID;
  var theChildNodeId = theScript.ScriptID;
  for (var i=0; i < theCategories.length; i++) {
    if (theCategories[i].getData( ) == theParentNodeId) break;
  }
  if (!exists) {   // New script -- add the node to the main display tree
    theCategories[i].addNode(new FTreeNode(theScript.ScriptName,
                                            theScript.ScriptID));
  }
  theCategories[i].setIsOpen(true);
  theTree.refresh( );
  var theNodes = theCategories[i].getChildNodes( );
  for (var j=0; j < theNodes.length; j++) {
    if (theNodes[j].getData( ) == theChildNodeId) break;
  }
  theTree.setSelectedNode(theNodes[j]);
}

// findItem:  method for the cache array to find an
//            item with a ScriptID that matches
function findItem (theArray, theItem) {
  for (var i in theArray) {
    if (theArray[i].ScriptID == theItem) {
      return true;
    }
  }
  return false;
}

The script is commented inline, but a few points are worth mentioning. A scriptCache property, which contains a generic Object instance, is set up as a cache for all displayed scripts. When a user clicks an item in the tree, the remote method is called and returns a ScriptObject. If the user clicks off the tree item and then back on again, further calls to the remote method are unnecessary, because the ScriptObject is stored in the cache. The zeroth element of the object become the descriptive label whenever a user has clicked on a folder in the tree; no script is shown. The code is written to be self-documenting (if it finds the items in the cache, it displays it; otherwise, it both adds it to the cache and displays it):

if (findItem(scriptCache, theScriptId)) {
  displayIt(theScriptId, cnt_main_mc.cnt_modify_mc);
} else {
  putScriptInCacheAndDisplayIt(theScriptId, cnt_main_mc.cnt_modify_mc);
}

The findItem( ) method is also declared in the ScriptRepository.as file. This function finds a script in the scriptCache object by iterating through the objects that are part of the scriptCache and comparing the theItem parameter (the second argument to the function) to the ScriptID property of each scriptCache element.

The searchScripts( ) and getAllScripts( ) methods utilize the same responder object. If searching for a phrase, the phrase is shown in the root of the tree ("scripts containing..."). If the user clicks the Show All Scripts button, "All Scripts" will be shown as the root node of the tree.

The displayIt( ) function also serves double duty: the displayScript( ) and displayScriptUpdate( ) methods use it to refresh the main script and update script movie clips, respectively. User interface elements share the same names on the different movie clips, so we are able to reference them by passing the movie clip name to the function and using it as a prefix.

14.5.4 UserObject.as

The UserObject, like the ScriptObject, is a class definition that defines a class of an object that we will pass back and forth between client and server.

The complete UserObject class is shown in Example 14-10.

Example 14-10. The UserObject class definition
/*
UserObject
   Properties:
     UserID        numeric
     Username      string
     userpassword  string
     FirstName     string
     LastName      string
     Emailaddress  string
     HintQuestion  string
     HintAnswer    string
   Methods:
     init       initialize the object
     toString
     loginUser
     addUser
     emailPassword
     _copyProperties
     onResult
     onStatus
*/
function UserObject ( ) {
  if (!this.inited)
    this.init(arguments);
}

UserObject.prototype.init = function (args) {
  this.inited = true; // Instance is initialized
  this.UserID          = (args[0] != undefined) ? args[0] : "";
  this.Username        = (args[1] != undefined) ? args[1] : "";
  this.Userpassword    = (args[2] != undefined) ? args[2] : "";
  this.FirstName       = (args[3] != undefined) ? args[3] : "";
  this.LastName        = (args[4] != undefined) ? args[4] : "";
  this.Emailaddress    = (args[5] != undefined) ? args[5] : "";
  this.HintQuestion    = (args[6] != undefined) ? args[6] : "";
  this.HintAnswer      = (args[7] != undefined) ? args[7] : "";
  this.PasswordConfirm = (args[8] != undefined) ? args[8] : "";
  this.isUserLogged    = (args[9] != undefined) ? args[9] : false;
};

// Register the object for use by Flash Remoting remote methods
Object.registerClass("UserObject", UserObject);

// Define a toString( ) function for reading the object
UserObject.prototype.toString = function ( ) {
  var temp = "inited:  "       +  this.inited + '\n';
  temp += "UserID:  "          +  this.UserID + '\n';
  temp += "Username:  "        +  this.Username + '\n';
  temp += "Userpassword:  "    +  this.Userpassword + '\n';
  temp += "FirstName:  "       +  this.FirstName + '\n';
  temp += "LastName:  "        +  this.LastName + '\n';
  temp += "Emailaddress:  "    +  this.Emailaddress + '\n';
  temp += "HintQuestion:  "    +  this.HintQuestion + '\n';
  temp += "HintAnswer:  "      +  this.HintAnswer + '\n';
  temp += "PasswordConfirm:  " +  this.PasswordConfirm;
  temp += "isUserLogged:  "    +  this.isUserLogged;
  return temp;
};

// Call the remote loginUser( ) service
UserObject.prototype.loginUser = function (service) {
  service.loginUser(this, this);
};

// Call the remote addUser( ) service 
UserObject.prototype.addUser = function (service) {
  if (this.Userpassword != this.PasswordConfirm) {
    errorHandler("Passwords don't match");
  this.isUserLogged = false;
  }
  service.addUser(this, this);
};

// Get scripts for user
UserObject.prototype.getScriptsForUser = function (service, box) {
  service.getScriptsForUser(new ComboBoxResponder(box), this);
};

// Debugging function to let us know that the object returned from 
// the server was registered properly. In responder function, do:
//   result.test( )
UserObject.prototype.test = function ( ) {
  trace("UserObject successful")
};

// Copy properties from an object to this
UserObject.prototype._copyProperties = function (from) {
  for (var prop in from) {
    if (this[prop] != from[prop]) this[prop] = from[prop];
  }
};

// Responder method
UserObject.prototype.onResult = function (result) {
  if (result.isUserLogged == true) {
    // Put all properties from the result into the instance of the UserObject
    this._copyProperties(result);
    alertBox("userAlert", "Welcome " + result.FirstName);
  } else {
    trace("fail");
  }
};

// Call a global error handler for the movie
UserObject.prototype.onStatus = function (error) {
  errorHandler(error.description);
};

// Validation function for properties of the UserObject
UserObject.prototype.validate = function ( ) {
  var errorMsg = "";
  if (this.Username == "")     errorMsg += "Username must not be empty\n";
  if (this.Userpassword == "") errorMsg += "Password must not be empty\n";
  if (this.FirstName == "")    errorMsg += "First name must not be empty\n";
  if (this.LastName == "")     errorMsg += "Last name must not be empty\n";
  if (!isValidEmail(this.Emailaddress))
      errorMsg += "Email address must be valid\n";
  if (this.HintQuestion == "") errorMsg += "Hint question must not be empty\n";
  if (this.HintAnswer == "")   errorMsg += "Hint answer must not be empty\n";
  if (this.PasswordConfirm == "")
      errorMsg += "Password confirmation must not be empty\n";
  return errorMsg;
};

You can see that the UserObject is constructed in a fashion similar to the ScriptObject. The object contains an init( ) method to allow objects returned from the server to retain their properties, a _copyProperties( ) method to copy the properties from the object returned from the server to the current instance that called the remote service, toString( ) and test( ) methods for debugging, and onResult( ) and onStatus( ) methods that give it the ability to act as a responder object. The validate( ) method of the UserObject works like its counterpart in the ScriptObject, with the exception that it calls a named function, isValidEmail( ), to validate an email address.

14.5.5 SiteUtilityFunctions.as

The calls to the site services are implemented as a set of named functions in the SiteUtilityFunctions.as file. The complete ActionScript code is shown in Example 14-11.

Example 14-11. The site utility functions
// General responder object for methods that return nothing:
//   onResult( ) method displays a message in an alert box
//   onStatus( ) method simply calls the error handler
function GeneralResponder (theName, theMessage, callbackFunction) {
  this.onResult = function (result) {
    alertBox(theName, theMessage, callbackFunction);
  };
  this.onStatus = function (error) {
    errorHandler(error.description);
  };
}

// Contact form event -- contactForm( ) calls remote method contactForm( )

function contactForm (from, userid, message) {
  if (message.length == 0) {
    errorHandler("Must enter a message");
    return;
  }
  // Call the remote service
  SiteService.contactForm(
    new GeneralResponder("contactAlert",
    "Email was sent: Thank you for contacting us"),
    from,     // Email from field
    userid,   // User ID
    message   // Message to send
  );
  workingAlert( );   // Display a "...working" box
  return;
}

// Send Page event -- sendPage( ) calls remote method sendPage( )
function sendPage (scriptid, to, from) {
  // Call the remote service
  SiteService.sendPage(
    new GeneralResponder("sendpageAlert", "Email was sent", setSendPageText),
       // Responder function fires alert
      scriptid,    // Script ID
      to,          // Email to field
      from         // Email from field
    );
  workingAlert( );  // Display a "...working" box
  return;
}

// Callback function to reset the text for the "send page to a friend" text field
function setSendPageText ( ) {
  cnt_main_mc.cnt_view_mc.sendto_txt.doDefault( );
}

// Set up About box on load
SiteService.about(new AboutResponder( ));

function AboutResponder ( ) {
  this.onResult = function (result_rs) {
    cnt_main_mc.cnt_about_mc.aboutname_txt.text =
       result_rs.getItemAt(0).CompanyName;
    cnt_main_mc.cnt_about_mc.aboutdesc_txt.text =
       result_rs.getItemAt(0).Description;
    // Set up the default download link for scripts stored in the remote database
    _global.downloadLink = result_rs.getItemAt(0).DownloadLink;
  };
  this.onStatus = function (error) {
    errorHandler(error.description);
  };
}

// Specialized responder object for retrieving the hint question
var GetEmailResponder = new Object( );
GetEmailResponder.onResult = function (result) {
  if (_global.currentUser.HintQuestion == "")
    _global.currentUser.HintQuestion = result;
  retrieveBox("getHintAnswerBox", "Your hint Question", result,
              "Your answer", getAnswer);
};
GetEmailResponder.onStatus = function (error) {
  errorHandler(error.description);
};

// Callback function for getEmail( )
function getQuestion (theField) {
  _global.currentUser.Emailaddress = theField;
  UserService.getEmail(GetEmailResponder, theField);
  workingAlert( );    // Display a "...working" box
}


// Callback function for emailPassword( )
function getAnswer (theField) {
  var temp = SharedObject.getLocal("tries");
  if (temp.data.tries < temp.data.triesLimit &&
      temp.date.datetime < new Date( ).getMilliseconds( ) + temp.data.timeLimit) {
    UserService.emailPassword(EmailPasswordResponder,
                  _global.currentUser.Emailaddress,
                  theField);
    workingBox( );    // show the "...working" message
  } else {
    alertBox("badRetrieve",
      "You've tried more than " + temp.data.triesLimit + " times within " +
      temp.data.hours + " hours.\n" +
      "Try again later or contact the site administrator.");
  }
}

// Specialized responder object for emailPassword( )
var EmailPasswordResponder = new Object( );
EmailPasswordResponder.onResult = function (result) {
  if (result == true) {
    alertBox("goodRetrieveBox", "Your password has been sent");
  } else {
    retrieveBox("tryAgainBox", "Wrong answer. Try Again: ",
      _global.currentUser.HintQuestion, "", getAnswer);
    var temp = SharedObject.getLocal("tries");
    temp.data.tries ++;
  }
};

EmailPasswordResponder.onStatus = function (error) {
  errorHandler(error.description);
};

// Call the remote service emailPassword( ).
// This function interacts with alert boxes and message boxes in the main movie.
function emailPassword ( ) {
  // Set up limits for how many tries we will allow and store in SharedObject
  var hours = 24;
  var timeLimit = hours * 60 * 60 * 1000; // 1 day in milliseconds
  var triesLimit = 5;                // triesLimit within the timeLimit specified

  var temp = SharedObject.getLocal("tries");
  // Set up the SharedObject if it hasn't been set up yet
  if (!temp.data.tries) {
    temp.data.tries      = 0;
    temp.data.triesLimit = triesLimit;
    temp.data.datetime   = new Date( );
    temp.data.timeLimit  = timeLimit;
    temp.data.hours      = hours;
  }
  // Step 1: Get email address
  retrieveBox("getEmail", "Enter your email address", "", "",getQuestion);
}

// Validate an email address (simple client-side validation): returns true or false
function isValidEmail (theString) {
  var isValid = (
   (theString.lastIndexOf('.') < theString.length - 2) &&  // must have dot
   (theString.indexOf('@') != -1) &&                       // must have @
   (theString.indexOf('@') == theString.lastIndexOf('@'))  // must not have two @@
   )
  return isValid;
}

// Put a Date object into human-readable date format (US format)
function doDateFormat (dateObj_date) {
  var d = dateObj_date.getDay( );
  var m = dateObj_date.getMonth( );
  var y = dateObj_date.getFullYear( );
  var h = dateObj_date.getHours( );
  var mn = dateObj_date.getMinutes( );
  mn = (mn < 10) ? '0' + mn : mn;
  var s = dateObj_date.getSeconds( );
  s = (s < 10) ? '0' + s : s;
  return m + '/' + d + '/' + y + ' ' + h + ':' + mn + ':' + s;
}

The SiteUtilityFunctions.as file takes care of calls to contactForm( ), sendPage( ), and about( ) in the SiteService remote service. It also takes care of the email password functionality, which is one of the more complicated aspects of the application. In a typical HTML-based application, the steps can be followed like this:

  1. User clicks the "email me my password" link and a page loads in with an email address box. User fills in email address and clicks Submit.

  2. The remote service finds the user's email address in the database and returns a question. A new page loads in with a hint question and an answer box. User fills in answer and clicks Submit again.

  3. The hint answer is checked in the database and, if correct, the username and password are mailed to the user. A new page loads, telling the user that the password has been mailed.

In the Flash Remoting application, we can't implement functionality that follows steps like this, but that is not a bad thing?it gives the application more of an immediate feel. Each call to the remote service is going to be handled by a responder object, but how do you call three remote services in a row that depend on the response from the previous call? You can't call them like this:

getQuestion(emailAddress);
getAnswer(hintAnswer);
emailPassword( );

If you were to execute this code, you would have an error because the three functions would fire immediately, even before the response was returned from the first function call.

Instead, we've created specialized responder objects that take care of calling the next remote method. It works like this:

  1. The user clicks the email password link and the emailPassword( ) function fires, displaying the dialog box that prompts the user for an email address.

  2. The prompt box uses a callback function, getQuestion( ), which calls the remote method getEmail( ) using the GetEmailResponder object.

  3. The "...working" dialog box pops up while the question is being retrieved. Within the GetEmailResponder.onResult( ) method, the hint question is shown in a second prompt box.

  4. The prompt box (retrieveBox( )) function takes a callback function (getAnswer( )) as an argument. This way, when the user clicks OK, we can call another remote method: emailPassword( ).

  5. If the hint answer matches the answer in the database, the username and password are mailed to the user. If not, getAnswer( ) is called again, but a counter limits the number of attempts (for security reasons). After five unsuccessful attempts, the user is locked out for 24 hours. The number of tries and the length of lockout are variables that you can change.

Finally, the SiteUtilityFunctions.as file contains a few utility functions for email validation and date formatting.

14.5.6 UI.as

The UI.as file contains several additions to the built-in objects and components of Flash MX, and a responder object to simplify populating a combo box with a recordset result. The complete code for UI.as is shown in Example 14-12.

Example 14-12. The UI.as file contains code for GUI elements
// Set up a responder object to handle recordsets for ComboBoxes.
// This responder assumes that data is coming in with ID column 
// in [0] position and description column in the [1] position.
// cbName is the fully-qualified name of the ComboBox.
// zeroElement is an optional argument that contains a zeroeth element
// of a descriptive label, like "--Categories--"

function ComboBoxResponder (cbName, zeroElement) {
  this.onResult = function (result_rs) {
    var fields = result_rs.getColumnNames( );
    // If there is a descriptive text to put in the Combo box
    // put it in the 0 position of the recordset.
    if (zeroElement != null) {
      var temp = {};
      result_rs.addItemAt(0, temp);
      result_rs.setField(0,fields[0], 0);
      result_rs.setField(0,fields[1],zeroElement);
    }
    var idField = '#' + fields[0] + '#';
    var descField = '#' + fields[1] + '#';
    DataGlue.bindFormatStrings(cbName, result_rs, descField, idField);
  };
  this.onStatus = errorHandler;
}

// Call the remote service to get all script IDs and names for scripts
// created by the current user.
function getUserScripts ( ) {
  ScriptService.getScriptsForUser(
    new ComboBoxResponder(
    cnt_main_mc.cnt_modify_mc.scriptname_cb, "-Scripts-"),
    _global.currentUser.username,
    _global.currentUser.password
  )
}

// pickValue( ): New method for ComboBoxes to be able to pick a value.
FComboBoxClass.prototype.pickValue = function (value) {
  for (var i=0; i<this.getLength( ); i++) {
    if (this.getItemAt(i).data == value) {
      this.setSelectedIndex(i);
      break;
    }
  }
};

// setAutoBlank( ): New method for the TextField object.
// Set the field to blank when cursor is placed in field.
// NOTE: if passing false to the function to turn feature off,
// need to redefine any onSetFocus( ) functionality.
TextField.prototype.setAutoBlank = function (value) {
  if (value) {
    this.onSetFocus = function ( ) {this.text = "";}
  } else {
    this.onSetFocus = null;
  }
};

// defaultText: Allow for default text to be placed in a text field.
TextField.prototype.defaultText = null;

TextField.prototype.setDefaultText = function (value) {
  this.defaultText = value;
};

TextField.prototype.getDefaultText = function ( ) {
  return this.defaultText;
};

TextField.prototype.addProperty("defaultText",
                                this.getDefaultText,
                                this.setDefaultText);

// doDefault( ): Set the field text to defaultText.
TextField.prototype.doDefault = function ( ) {
  this.text = this.defaultText;
};

The ComboBoxResponder object is used by all combo boxes in the movie that are fed by remote recordsets. The recordsets are assumed to contain a number field and a description field. There are four combo boxes that use this responder object.

The pickValue( ) method is added to the FComboBox class to add the functionality to all combo boxes in the movie. With this method, you can now pass a number to the combo box to have that particular record shown. For example, if you have a list of six categories in the Categories_cb combo box and you want the fourth item, you bring it into focus like this:

Categories_cb.pickValue(3);

There are two additions to the TextField class as well. We've added an autoblank feature, which allows you to create a TextField that automatically becomes blank when you place your cursor in it. Turn on this functionality like this:

myTextfield.setAutoBlank(true);

We've also added a defaultText property to the TextField class. This property stores the default text for that particular field. Restore the default text for the text field using the custom doDefault( ) method:

myTextField.doDefault( );

14.5.7 Flash User Interface Code

Many of the remote methods are called from the Flash interface. The ActionScript code for the interface is fairly elaborate and too long to reprint here in full, but a few of the key ActionScript snippets should be explained. (The full version can be downloaded from the online Code Depot.)

There are two custom message boxes that are built from movie clips rather than components, because one of the Macromedia components that would have been necessary is a commercial component (the Advanced Message Box). The message boxes are both set up to accept a callback function, which would be fired upon the user clicking the OK button. The alertBox( ) function is shown in Example 14-13.

Example 14-13. Custom alert box movie clip is used extensively in the movie
// Display Alert Box
// Arguments:
//   theName:    name for the box
//   theMessage: text message to display
//   callback:   callback function when OK is clicked

function alertBox (theName, theMessage, callbackFunction, hideOK) {
  if (workingBox_mc)             // If there is a "...working" box, remove it
    workingBox_mc.removeMovieClip( );
  _root.attachMovie("alertbox_mc", theName, 1);
  var thisBox = _root[theName];
  thisBox._x = (Stage.width - thisBox._width)/2;
  thisBox._y = (Stage.height - thisBox._height)/2;
  thisBox.message_txt.text = theMessage;
  if (!hideOK) {
    // ok button
    thisBox.ok_btn.onRollOver = overState;
    thisBox.ok_btn.onRollOut = outState;
    thisBox.ok_btn.onPress = function ( ) {
      thisBox.onUnload = callbackFunction;
      thisBox.removeMovieClip( );
    };
  } else {
    thisBox.ok_btn._visible = false;
  }
};

The workingAlert( ) function also shares this alertBox( ) function and displays a "...working" message to the user. This is used by many remote methods in the application. The retrieveBox( ) function displays a similar box, but it allows for user input, as shown in Figure 14-4.

Figure 14-4. The retrieveBox( ) function calls a custom movie clip to retrieve information
figs/frdg_1404.gif

Remote methods are called from the onRelease events of the buttons in the interface. Example 14-14 shows the code for the Upload Script button (scriptupload_btn).

Example 14-14. The Upload Script button calls
// scriptupload_btn
cnt_main_mc.cnt_upload_mc.scriptupload_btn.onRollOver = overState;
cnt_main_mc.cnt_upload_mc.scriptupload_btn.onRollOut = outState;
cnt_main_mc.cnt_upload_mc.scriptupload_btn.onRelease = function (mc) {
  var tempScript = new ScriptObject(null,
              cnt_main_mc.cnt_upload_mc.scriptname_txt.text,
              cnt_main_mc.cnt_upload_mc.scriptdesc_txt.text,
              cnt_main_mc.cnt_upload_mc.scriptcode_txt.text,
              cnt_main_mc.cnt_upload_mc.scriptlanguage_cb.getSelectedItem( ).data,
              cnt_main_mc.cnt_upload_mc.scriptcategory_cb.getSelectedItem( ).data,
              _global.currentUser.UserID,
              cnt_main_mc.cnt_upload_mc.scriptdateuploaded_txt.text,
              cnt_main_mc.cnt_upload_mc.scriptdatemodified_txt.text,
              1,
              0,
              0);
  // Make sure the script is filled in
  var errorMessage = tempScript.validate( );
  if (errorMessage == "") {
    tempScript.addScript(ScriptService);
    workingAlert( );
    mainScreen( );
  } else {
    alertBox("validationError",errorMessage);
  }
};

In the scriptupload_btn button's onRelease( ) event handler, a temporary ScriptObject is created using the text from the interface elements as the arguments to create the object. The tempScript variable contains the new ScriptObject, and the remote addScript( ) method is called through this object. The "working" alert box is shown until it is removed by the appropriate responder function.

The "send this page to a friend" functionality is made possible by the use of the FlashVars attribute in the <object> and <embed> tags on the ColdFusion page that houses the movie. If an id variable is passed to the page, Flash will pick up the variable and execute the following code:

if (scriptid != null && scriptid != "" && scriptid != "undefined") {
    putScriptInCacheAndDisplayIt(scriptid, cnt_main_mc.cnt_view_mc, true);
}

We simply pass to the putScriptInCacheAndDisplayIt( ) function the scriptid variable, the main display movie clip, and the value true to signal a refresh of the Tree component.

The HTML and ColdFusion code required for this functionality is shown in Example 14-15. The ColdFusion logic is highlighted in bold. Similar functionality can be created in PHP, ASP.NET, or Java pages as well.

Example 14-15. HTML and ColdFusion code to pass URL variables
<OBJECT classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/
swflash.cab#version=6,0,65,0"
 WIDTH="100%" HEIGHT="100%" ALIGN="">
  <cfif isdefined("url.scriptid")><param name="flashvars"
   value="scriptid=<cfoutput>#url.scriptid#</cfoutput>"></cfif>
<PARAM NAME=movie VALUE="ScriptRepository.swf">
<PARAM NAME=quality VALUE=high>
<PARAM NAME=bgcolor VALUE=#D9EFB4>
<EMBED src="ScriptRepository.swf" WIDTH="100%" HEIGHT="100%" ALIGN=""
quality=high bgcolor=#D9EFB4 TYPE="application/x-shockwave-flash" 
PLUGINSPAGE="http://www.macromedia.com/go/getflashplayer"
<cfif isdefined("url.scriptid")>
flashvars="scriptid=<cfoutput>#url.scriptid#</cfoutput>"</cfif> ></EMBED>
</OBJECT>

You can also simply append variables to the end of the URL, but this technique is known to be buggy in several versions of the Player. Using FlashVars is a better approach when you can control the output of the HTML tags with server-side logic.



    Part III: Advanced Flash Remoting
     
    ASPTreeView.com
     
    Evaluation has ёЩФexpired.
    Info...