[ Team LiB ] |
27.5 Extending the FrameworkAs it is presented, the framework stores the user's settings in a local shared object on the user's computer. Ideally, however, the application should save the data in a way that the user's preferences can be retrieved from any computer. This can be accomplished in a variety of ways. The data could be stored to a database, a text file, an XML file, or using a FlashCom Server remote shared object. The following example stores the data to an XML document on the server. Without relying on a FlashCom server remote shared object or other proprietary system, the solution uses an ActionScript class, a simple server-side script for file I/O operations, and an XML document. Refer to Chapter 19 for more information on saving and retrieving XML. 27.5.1 Making a Server-Side Shared ObjectWithout much modification to the framework movie, you can have it use a server-side faux shared object instead of a local shared object (or instead of a true FlashCom server remote shared object). First, let's understand how the process works. Using an ActionScript class, FauxSharedObject, you can mimic the operations of a local shared object. You can perform the saving and retrieval operations by serializing and deserializing the data with XML, and then use a server-side script (ColdFusion in this case) to perform the file operations. Save the FauxSharedObject class definition, shown in the following example, in an ActionScript file named FauxSharedObject.as and place it in Flash's Include directory: // Include NetServices.as for Flash Remoting API. #include "NetServices.as" // FSOResponse is a response object used for the Flash Remoting // service function calls made by FauxSharedObject. function FSOResponse (parent) { this.parent = parent; } FSOResponse.prototype.onResult = function (result) { if (result != null) { this.parent.setData(FauxSharedObject.deserializeObject(result)); this.parent.onLoadPath[this.parent.onLoadCB]( ); } }; // The constructor accepts a filename and a gateway URL // used by the Flash Remoting connection. function FauxSharedObject (name, gwURL) { this.name = name; this.openRes = new FSOResponse(this); this.saveRes = new FSOResponse( ); this.initNetServices(gwURL); } // Define the properties of the class. FauxSharedObject.prototype.data = new Object( ); // Initialize the Flash Remoting connection and service. The service object // connects to FauxSharedObject.cfc, as shown in Section 27.5.2. FauxSharedObject.prototype.initNetServices = function (gwURL) { NetServices.setDefaultGatewayURL(gwURL); var conn = NetServices.createGatewayConnection( ); this.srv = conn.getService("FauxSharedObject"); }; // A static method that serializes an object to XML. FauxSharedObject.serializeObject = function (obj) { var sb = "<obj>"; if (obj instanceof Array) { sb = "<obj type=\"array\">"; } for (var i in obj) { sb += "<index name=\"" + i + "\""; if (obj[i] instanceof Object) { sb += ">"; sb += FauxSharedObject.serializeObject(obj[i]); sb += "</index>"; } else { sb += " val=\"" + obj[i] + "\" />"; } } sb += "</obj>"; return sb; }; // A static method that deserializes XML to an object. FauxSharedObject.deserializeObject = function (source) { XML.prototype.ignoreWhite = true; var sXML = new XML(source); var cn = sXML.firstChild.childNodes; var items = new Object( ); if (sXML.firstChild.attributes.type == "array") { items = new Array( ); } var attribs, childCn; for (var i = 0; i < cn.length; i++) { attribs = cn[i].attributes; childCn = cn[i].childNodes; if (childCn.length > 0) { items[attribs.name] = FauxSharedObject.deserializeObject(childCn[0]); } else { items[attribs.name] = attribs.val; } items[attribs.index] = new Object( ); for (var j in attribs) { items[attribs.index][j] = attribs[j]; } } return items; }; // Load an object from the server by calling Flash Remoting's open( ) method. FauxSharedObject.prototype.load = function ( ) { this.srv.open(this.openRes, this.name); }; // Save the object in serialized form to the server, calling Flash Remoting's // save( ) method. Mimics the shared object flush( ) method. FauxSharedObject.prototype.flush = function ( ) { this.srv.save(this.saveRes, this.name, FauxSharedObject.serializeObject(this.data)); }; // Called automatically when the object has loaded from the server FauxSharedObject.prototype.setData = function (val) { this.data = val; }; // Allow the user to define a callback function for when the object has loaded. FauxSharedObject.prototype.setLoadHandler = function(functionName, path) { if (path == undefined) { path = this._parent; } this.onLoadPath = path; this.onLoadCB = functionName; }; Now that you have had an opportunity to look over the code in the preceding example, let's look more closely at the parts that make up the FauxSharedObject class and its helper class, FSOResponse. The code first defines the FSOResponse class, which is a Flash Remoting response object class used by FauxSharedObject. I chose to separate the response object portion into its own class in keeping with object-oriented design. The constructor method takes a reference to the FauxSharedObject that instantiates it, and it sets its custom parent property to that reference. This reference is used within the onResult( ) method to invoke the loadHandler( ) method of the parent FauxSharedObject when results are returned. The parent property is another example in which using a reference can be useful in establishing relationships between objects. Because FauxSharedObject and FSOResponse are not MovieClip classes, you cannot rely on the _parent property or similar means to relate the objects. A reference such as parent provides this bridge between the objects. function FSOResponse (parent) { this.parent = parent; } FSOResponse.prototype.onResult = function (result) { if (result != null) { this.parent.setData(FauxSharedObject.deserializeObject(result)); this.parent.loadHandler.call( ); } }; The constructor method for the FauxSharedObject class takes parameters indicating the name of the object that is stored on the server (which is the same as the username in this example) as well as the Flash Remoting gateway URL to use for the Flash Remoting calls.
The constructor also creates the response objects as FSOReponse instances and invokes the initNetServices( ) method to initialize the NetServices objects: function FauxSharedObject (name, gwURL) { this.name = name; this.openRes = new FSOResponse(this); this.saveRes = new FSOResponse( ); this.initNetServices(gwURL); } The FauxSharedObject class defines a data property as an associative array to closely mimic the functionality of local shared objects. Creating a property of the same type, with the same name, and with the same intended functionality as with an LSO, you can ensure that minimum modifications are needed to integrate FauxSharedObject instances in place of SharedObject instances in existing applications. FauxSharedObject.prototype.data = new Object( ); The FauxSharedObject class serializes and deserializes data and sends it between the Flash movie and the server. Serialization converts a reference datatype (an object), which cannot be stored in a file or database on the server, into a format that can be saved. The code uses a custom XML-based structure to perform the serialization. Objects are converted to <obj> elements with nested <index> elements. The <index> elements always have a name attribute, which is the name of the variable/property for the object. If the <index> represents a primitive datatype, it also has a val attribute holding the value of the variable. On the other hand, if the <index> represents an object, it has a nested <obj> element. Here is an example of an object and the corresponding serialized form: obj = new Object( ); obj.prop1 = "a value"; obj.prop2 = "another value"; obj.prop3 = {a: "eh", b: "bee"}; /* <obj> <index name="prop1" val="a value" /> <index name="prop2" val="another value" /> <index name="prop3"> <obj> <index name="a" value="eh" /> <index name="b" value="bee" /> </obj> </index> </obj> */ To convert objects to their serialized forms, the FauxSharedObject class defines a static method, serializeObject( ). A static method is one that is invoked directly from the class rather than from instances of the class. Because serializeObject( ) and deserializeObject( ) do not need to be used from specific instances of the class, it makes sense to define them as static methods. To define a static method attach it to the FauxSharedObject top-level object and not to its prototype. The serializeObject( ) method accepts an object as a parameter and converts the object properties to <index> elements that are appended to a string. The serializeObject( ) method uses the instanceof operator to check whether an object contains a property that is itself an object. If so, that object is passed to serializeObject( ) in a recursive manner (i.e., the function calls itself). Recursion allows a function to handle an arbitrarily nested data structure, such as an object that contains other objects or a directory structure with nested subfolders. See Recipe 7.10 and Recipe 19.7 for more examples of recursive functions. FauxSharedObject.serializeObject = function (obj) { var sb = "<obj>"; if (obj instanceof Array) { sb = "<obj type=\"array\">"; } for (var i in obj) { sb += "<index name=\"" + i + "\""; if (obj[i] instanceof Object) { sb += ">"; sb += FauxSharedObject.serializeObject(obj[i]); sb += "</index>"; } else { sb += " val=\"" + obj[i] + "\" />"; } } sb += "</obj>"; return sb; }; The counterpart to serialization is deserialization—the conversion of XML data back to the original object. The static deserializeObject( ) method takes an XML string and parses it into an XML object. It then loops through all the child nodes of the root <obj> element and converts them back to properties of an object. If a child node contains nested elements, the node represents an object. As with the serializeObject( ) method, deserializeObject( ) calls itself recursively, passing in the next <obj> element, to handle child nodes that contain nested objects. FauxSharedObject.deserializeObject = function (source) { XML.prototype.ignoreWhite = true; var sXML = new XML(source); var cn = sXML.firstChild.childNodes; var items = new Object( ); if (sXML.firstChild.attributes.type == "array") { items = new Array( ); } var attribs, childCn; for (var i = 0; i < cn.length; i++) { attribs = cn[i].attributes; childCn = cn[i].childNodes; if (childCn.length > 0) { items[attribs.name] = FauxSharedObject.deserializeObject(childCn[0]); } else { items[attribs.name] = attribs.val; } items[attribs.index] = new Object( ); for (var j in attribs) { items[attribs.index][j] = attribs[j]; } } return items; }; The load( ) method calls the Flash Remoting service object's open( ) method to open the shared object data from the server: FauxSharedObject.prototype.load = function ( ) { this.srv.open(this.openRes, this.name); }; The FauxSharedObject.flush( ) method mimics a local shared object's flush( ) method by calling the service object's save( ) function to save the data to the server. The flush( ) method, as with the data property, uses the same name and performs the same function as its analog in the SharedObject class. This is a good practice when writing custom classes to perform similar tasks as existing classes because it makes the classes largely interchangeable. FauxSharedObject.prototype.flush = function ( ) { this.srv.save(this.saveRes, this.name, FauxSharedObject.serializeObject(this.data)); }; Finally, the setLoadHandler( ) method provides a means for a function to be set as a callback function for when the shared object data is loaded. The FSOResponse object's onResult( ) method automatically tries to call this function if it is defined. This is important because, unlike local shared objects, the FauxSharedObject is asynchronous. This means that, unlike a local shared object, in which the data is retrieved from the user's computer immediately, the FauxSharedObject might take milliseconds or even seconds to retrieve the data from the server. The rest of the ActionScript code continues to execute while the FauxSharedObject data is being retrieved, so any functionality that relies on the retrieved data must be handled only after that data has been returned. The RSOResponse instance is automatically called (because of Flash Remoting) when a response is returned from the server. By setting a loadHandler( ) method for automatic callback, you can define custom functions for each FauxSharedObject instance to know how to handle the particular results returned from the server. FauxSharedObject.prototype.setLoadHandler = function (functionRef) { this.loadHandler = functionRef; }; 27.5.2 Performing File OperationsOur example My Page application requires that the data sent between the Flash movie and the server be saved to a file on the server side. The following example shows how to save the data using a ColdFusion Component (CFC). You should save the CFC to the server's web root as FauxSharedObject.cfc, since this is the service name that Flash looks for. If you are comfortable with another language/platform that works with Flash Remoting (e.g., ASP.NET or J2EE), you can easily adapt this code. See Chapter 20 for examples in various server-side languages. <cfcomponent> <cffunction name="save" access="remote"> <cfargument name="name" type="string" required="true"> <cfargument name="data" type="string" required="true"> <cffile action="write" file="#GetDirectoryFromPath(GetBaseTemplatePath( ))##name#.so" output="#data#" nameconflict="overwrite"> <cfreturn #GetDirectoryFromPath(GetBaseTemplatePath( ))#> </cffunction> <cffunction name="open" access="remote"> <cfargument name="name" type="string" required="true"> <cffile action="read" file="#GetDirectoryFromPath(GetBaseTemplatePath( ))##name#.so" variable="data"> <cfreturn #data#> </cffunction> </cfcomponent> The ColdFusion Component in the preceding example consists of only two functions: save and open. The <cffunction> and <cfargument> tags define the functions and the parameters they accept. You should recognize these functions from the FauxSharedObject ActionScript class, since it makes calls to them by way of Flash Remoting. Each of these two functions performs basic file operations made available by the ColdFusion <cffile> tag. The save function writes the data to a file in the same directory with the name specified by the name parameter followed by the .so extension. The open function does the reverse by reading the contents of the specified file. The <cfreturn> tags merely return the results to the calling function. If you want to adapt this code to another language/platform, refer to the language's file I/O API. You should create a script or program that is accessible to Flash Remoting with the service name "FauxSharedObject." Also, within the script or program should be two methods: one named save that saves the specified data to a given filename, and one named open that reads the contents of a given file and returns the result. 27.5.3 Modifying the My Page FrameworkWe're almost done adapting the framework to store the data remotely. One more round of modification is needed to make the framework work with the server-side shared objects. The following example shows the changes to myPage.fla in abridged format. Notice that much of the code remains the same as the version from Section 27.2.4 earlier in the chapter, but some of it is placed into new functions to support the asynchronous callback functionality that is now required. // Include MovieClip.as from Chapter 7 and TextField.as from Chapter 8. #include "MovieClip.as" #include "TextField.as" // Include Table.as and Forms.as from Chapter 11. #include "Table.as" #include "Forms.as" // Include DrawingMethods.as from Chapter 4. #include "DrawingMethods.as" // Include the FauxSharedObject class from earlier in this chapter. #include "FauxSharedObject.as" // Open the movie with a login form to determine // for which user to retrieve the settings. function initLogin ( ) { // Create a text field to label the input field. this.createAutoTextField("usernameLabel", this.getNewDepth( )); usernameLabel.text = "username:"; // Create the input text field. this.createInputTextField("username", this.getNewDepth( )); // Create the Login button for logging in. this.attachMovie("FPushButtonSymbol", "loginBtn", this.getNewDepth( )); loginBtn.setLabel("login"); loginBtn.setClickHandler("initLSO"); // Place the form in the center of the movie. var x = Stage.width/2 - (usernameLabel._width + username._width)/2; loginTr0 = new TableRow(5, new TableColumn(5, usernameLabel), new TableColumn(5, username)); loginTr1 = new TableRow(5, new TableColumn(5, loginBtn)); loginTable = new Table(5, x, Stage.width/2, loginTr0, loginTr1); } // Opens the FauxSharedObject determined by the username entered. function initLSO ( ) { // Get username. var name = username.text; // Remove login form now that it is no longer needed. usernameLabel.removeTextField( ); username.removeTextField( ); loginBtn.removeMovieClip( ); // Open the FauxSharedObject. gw = "http://localhost:8500/flashservices/gateway/"; lso = new FauxSharedObject(name, gw); lso.setLoadHandler("soCallback", this); lso.load( ); // Load the XML for the services menu. loadXML( ); } // Some of this function is the same as what used to be in initLSO( ) but is moved // here because it has to be opened on callback now that Flash Remoting is used. function soCallback ( ) { trace("called"); if (lso.data.opened == undefined) { lso.data.opened = new Object( ); } var o = lso.data.opened; var newUnit; for (var item in o) { newUnit = openUnit(o[item].title, o[item].source, item); newUnit._x = o[item].x; newUnit._y = o[item].y; } lso.onMoved = function (unit) { this.data.opened[unit.index].x = unit._x; this.data.opened[unit.index].y = unit._y; this.flush( ); }; } function loadXML ( ) { // Remains the same as original } function initForm (services) { // Remains the same as original } function addUnit ( ) { // Remains the same as original } function openUnit (title, source, index) { // Remains the same as original } // This call to initLogin( ) replaces the calls to // initLSO( ) and loadXML( ) in original. initLogin( ); The myPage.fla code does not change a tremendous amount. The changes, as shown in the preceding example, involve the following:
The login form needs to be added so a user can identify himself to the My Page application so that his stored settings are loaded. This example does not provide any password security, but you might want to experiment by adding a password field and integrating the login form with a Flash Remoting back end to perform validation. You may want to refer to Chapter 28 for how such a system can be implemented. The primary technical modification to myPage.fla is that it now uses a FauxSharedObject. Because the FauxSharedObject class uses Flash Remoting, the operations are asynchronous. This means that it might take a moment for the data from the server to load.
These actions are now all placed within soCallback( ), which is set as the callback function for the FauxSharedObject in the initLSO( ) function: function initLSO ( ) { . . . lso.setLoadHandler("soCallback", this); . . . } The code within the soCallback( ) function is the same code that was contained within the initLSO( ) function. If you have any questions regarding that functionality, refer to the discussion of initLSO( ) in Section 27.2.4. 27.5.4 Reexamining the ServicesThe Google search service does not rely on local shared objects at all and thus will work perfectly regardless of which computer is used to access it. The notes and address book modules, on the other hand, rely on local shared objects for saving their information. Try to adapt each of these services to use either FauxSharedObject or your own Flash Remoting code. |
[ Team LiB ] |