Many applications need to store persistent data on the client computer, and Flex applications are no exception. For example, a Flex application may display a “start page” for new users, yet the application may give the user the option to hide the start page on subsequent visits. Though you could store that preference remotely, a more common way to accomplish that is to store the preference on the client side.
Flash Player security is a top priority at Adobe, and for this reason Flash Player does not have the capability to write to arbitrary files on the client computer. However, Flash Player does have a designated area on the client computer where it can write to very specific files that are controlled and managed entirely by Flash Player. These files are called local shared objects, and you can use ActionScript to write to and read from these files.
Flash Player uses the flash.net.SharedObject
class to manage access to local shared object data. Although the
data is stored in files on the client machine, the access to those files
is controlled exclusively through the SharedObject
interface. This both simplifies
working with shared objects and improves security to protect Flex
application users from malicious programmers.
The SharedObject
class also
allows you to work with remote shared objects.
For this reason, you may notice that the SharedObject
class API includes many
properties and methods not discussed in this chapter. Remote shared
objects allow real-time data synchronization across many clients, but
they also require server software such as Flash Media Server. In this
book, we discuss local shared objects, not remote shared objects.
Unlike many ActionScript classes, the SharedObject
constructor is never used
directly, and you cannot meaningfully create a new instance using the
constructor. Rather, the SharedObject
class defines a static, lazy
instantiation factory method called getLocal()
. The getLocal()
method
returns a SharedObject
instance that
acts as a proxy to a local shared object file on the client computer.
There can obviously be many local shared objects on a client computer,
so you must specify the specific shared object you want to reference by
passing a string parameter to getLocal()
. If the file does not yet exist,
Flash Player first creates the file and then opens it for reading and
writing. If the file already exists, Flash Player simply opens it for
reading and writing. The following code retrieves a reference to a
shared object called example
:
var sharedObject:SharedObject = SharedObject.getLocal("example");
Once you’ve retrieved the reference to the shared object,
you can read and write to it using the data property of
the object, which is essentially an associative array. You must write
all data that you want to persist to disk to the data property. You can
use dot syntax or array-access notation to read and write arbitrary keys
and values. In general, dot syntax is marginally optimal because it
yields slightly faster performance. The following writes a value of
true
to the shared object for a key
called hideStartScreen
:
sharedObject.data.hideStartScreen = true;
You should use array-access notation when you want to read or write using a key that uses characters that are not valid for use in variable/property names. For example, if you want to use a key that contains spaces, you can use array-access notation:
sharedObject.data["First Name"] = "Bob";
Data is not written to disk as you write it to the SharedObject
instance. By default, Flash
Player will attempt to write the data to disk when the .swf closes. However, this can fail silently
for several reasons. For example, the user might not have allocated
enough space, or the user might have disallowed writing to shared
objects entirely. In these cases, the shared object data will not write
to disk, and the Flex application will have no notification. For this
reason, it is far better to explicitly write the data to disk.
You can explicitly write data to disk using the flush()
method. The flush()
method
serializes all the data and writes it to disk. If the user has
disallowed local data storage for Flash Player for the domain, flush()
will throw an error:
try { sharedObject.flush(); } catch { Alert.show("An error occurred. This could be because you have disallowed local data storage."); }
The flush()
method also returns
a string value corresponding to either the PENDING
or the FLUSHED
constants of flash.net.SharedObjectFlushStatus
. If the return value is FLUSHED
, the data was successfully saved to
disk. If the return value is PENDING
,
it means that the user has not allocated enough disk space for the
amount of data the shared object is trying to write to disk, and Flash
Player is displaying a settings dialog to the user, prompting her to
allow the necessary allocation. When the user selects either to allow or
disallow the allocation, the shared object will dispatch a netStatus
event. You
can listen for the event using the flash.events.NetStatusEvent.NET_STATUS
constant:
sharedObject.addEventListener(NetStatusEvent.NET_STATUS, flushStatusHandler);
The NetStatusEvent
type
defines a property called info
that contains a property called code
. The code
property will have a string value of
either SharedObject.Flush.Success
or
SharedObject.Flush.Failed
. Example 16-3 tries to write to disk. If the user
has disallowed local data storage or does not allocate the space when
prompted, the application displays an alert.
Example 16-3. Shared object example
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" initialize="initializeHandler(event)"> <mx:Script> <![CDATA[ import flash.net.SharedObject; import mx.controls.Alert; private var _sharedObject:SharedObject; private function initializeHandler(event:Event):void { _sharedObject = SharedObject.getLocal("example"); if(_sharedObject.data.count == null) { _sharedObject.data.count = 20; try { var status:String = _sharedObject.flush(); if(status == SharedObjectFlushStatus.PENDING) { _sharedObject.addEventListener( NetStatusEvent.NET_STATUS, flushStatusHandler); } } catch (error:Error) { Alert.show("An error has occurred. This may be because you have disallowed local data storage."); } } else { Alert.show("Shared object data: " + _sharedObject.data.count); } } private function flushStatusHandler(event:NetStatusEvent):void { event.target.removeEventListener(NetStatusEvent.NET_STATUS, flushStatusHandler); if(event.info.code == "SharedObject.Flush.Failed") { Alert.show("You must allow local data storage."); } } ]]> </mx:Script> </mx:Application>
By default, Flash Player attempts to allocate enough space for the
shared object data. If the shared object is likely to grow over time,
Flash Player might prompt the user to allocate more space with each
incremental increase. If you know that a shared object will require more
disk space in the future, you can preallocate space by calling flush()
with the number of bytes you want to
allocate. For example, the following attempts to allocate 512,000
bytes:
sharedObject.flush(512000);
The default allocation is 100 KB. Unless the user has changed his Flash Player settings you can generally assume that you can store up to 100 KB of data in a local shared object without prompting the user.
By default, every shared object is specific to the .swf from which it originates. However, you
can allow several .swf files to
access the same shared object(s) by specifying a path when calling
getLocal()
. The default path is the
path to the .swf. For example, if
the .swf is at http://www.example.com/flex/client/a.swf, the path is
/flex/client/a.swf, which means
only a.swf can access the shared
object. For this example, we’ll assume that a.swf retrieves a reference to a shared
object called example
as
follows:
var sharedObject:SharedObject = SharedObject.getLocal("example");
If b.swf is in the same
directory as a.swf and b.swf also tries to retrieve a reference to a
shared object called example
using
the exact same code as appears in a.swf, b.swf will retrieve a reference to a
different shared object—one that is scoped specifically to the path
/flex/client/b.swf. If you want
a.swf and b.swf to be able to access the same shared
object, you must specify a path parameter using a common path that they
both share, such as /flex/client:
var sharedObject:SharedObject = SharedObject.getLocal("example", "/flex/client");
For .swf files to have access to the same shared objects they must specify a path that they have in common. For example, both a.swf and b.swf have /flex/client in common. They also share the paths /flex and /. If http://www.example.com/main.swf wants to use the same shared object as a.swf and b.swf, all three .swf files must specify a path of / for the shared object because that is the only path they have in common.
Shared objects can be shared by all .swf files within a domain. However, .swf files in two different domains cannot access the same local shared object.
Thus far, we’ve talked about local shared objects in theory. In this section, we’ll build a simple application that utilizes a shared object in a practical way. This example displays a log-in form in a pop up. However, the user has the option to set a preference so that the application will remember her.
This example application uses an MXML component that displays the
log-in window. It also uses a User Singleton class that allows the user
to authenticate. Note that in this example, the application uses
hardcoded values against which it authenticates. In a real application
the authentication would be against data from a database, LDAP, or some
similar data store. The UserAuthenticator
class looks like Example 16-4.
Example 16-4. UserAuthenticator class for shared object example
package com.oreilly.programmingflex.lso { import flash.events.EventDispatcher; import flash.events.Event; public class UserAuthenticator extends EventDispatcher { // The managed instance. private static var _instance:UserAuthenticator; // Declare two constants to use for event names. public static const AUTHENTICATE_SUCCESS:String = "success"; public static const AUTHENTICATE_FAIL:String = "fail"; public function UserAuthenticator () {} // The Singleton accessor method. public static function getInstance():UserAuthenticator { if(_instance == null) { _instance = new UserAuthenticator(); } return _instance; } // The authenticate() method tests if the username and password are valid. // If so it dispatches an AUTHENTICATE_SUCCESS event. If not it dispatches // an AUTHENTICATE_FAIL event. public function authenticate(username:String, password:String):void { if(username == "user" && password == "pass") { dispatchEvent(new Event(AUTHENTICATE_SUCCESS)); } else { dispatchEvent(new Event(AUTHENTICATE_FAIL)); } } } }
The log-in form component looks like Example 16-5 (name the file LogInForm.mxml and save it in the com/oreilly/programmingflex/lso/ui directory for the project).
Example 16-5. LogInForm.mxml
<?xml version="1.0" encoding="utf-8"?> <mx:TitleWindow xmlns:mx="http://www.adobe.com/2006/mxml"> <mx:Script> <![CDATA[ import mx.managers.PopUpManager; import com.oreilly.programmingflex.lso.UserAuthenticator; // This method handles click events from the button. private function onClick(event:MouseEvent):void { // If the user selected the remember me checkbox then save the // username and password to the local shared object. if(rememberMe.selected) { var sharedObject:SharedObject = SharedObject.getLocal("userData"); sharedObject.data.user = {username: username.text, password: password.text}; sharedObject.flush(); } // Authenticate the user. UserAuthenticator.getInstance().authenticate(username.text, password.text); } ]]> </mx:Script> <mx:Form> <mx:FormHeading label="Log In" /> <mx:FormItem label="Username"> <mx:TextInput id="username" /> </mx:FormItem> <mx:FormItem label="Password"> <mx:TextInput id="password" displayAsPassword="true" /> </mx:FormItem> <mx:FormItem> <mx:Button id="submit" label="Log In" click="onClick(event)" /> </mx:FormItem> <mx:FormItem> <mx:CheckBox id="rememberMe" label="Remember Me" /> </mx:FormItem> </mx:Form> </mx:TitleWindow>
The application MXML file itself is shown in Example 16-6.
Example 16-6. Main MXML file for shared object example
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" initialize="initializeHandler(event)"> <mx:Script> <![CDATA[ import mx.containers.Form; import mx.managers.PopUpManager; import com.oreilly.programmingflex.lso.UserAuthenticator; import com.oreilly.programmingflex.lso.ui.LogInForm; private var _logInForm:LogInForm; private function initializeHandler(event:Event):void { // Retrieve the same shared object used to store the data from the // log-in form component. var sharedObject:SharedObject = SharedObject.getLocal("userData"); // Listen for events from the UserAuthenticator instance. UserAuthenticator.getInstance().addEventListener (UserAuthenticator.AUTHENTICATE_SUCCESS, removeLogInForm); UserAuthenticator.getInstance().addEventListener (UserAuthenicator.AUTHENTICATE_FAIL, displayLogInForm); // If the shared object doesn't contain any user data then // display the log-in form. Otherwise, authenticate the // user with the data retrieved from the local shared object. if(sharedObject.data.user == null) { displayLogInForm(); } else { UserAuthenticator.getInstance().authenticate (sharedObject.data.user.username, sharedObject.data.user.password); } } private function displayLogInForm(event:Event = null):void { if(_logInForm == null) { _logInForm = new LogInForm(); PopUpManager.addPopUp(_logInForm, this, true); } } private function removeLogInForm(event:Event = null):void { if(_logInForm != null) { PopUpManager.removePopUp(_logInForm); _logInForm = null; } } ]]> </mx:Script> <mx:TextArea x="10" y="10" text="Application"/> </mx:Application>
This simple application illustrates a practical use of local
shared objects. When you test this example, use the username user
and the password pass
.
Many built-in types are automatically serialized and deserialized.
For example, strings, numbers, Boolean values, Date
objects, and arrays are all automatically
serialized and deserialized. That means that even though shared object
data is ultimately saved to a flat file, when you read a Date
object or an array from a shared object,
it's automatically recognized as the correct type. Flash Player
automatically serializes all public properties (including public
getters/setters) for custom types as well. However, Flash Player does
not automatically store the class type. That means that when you
retrieve data of a custom type from a shared object, it doesn't
deserialize to the custom type by default. For instance, consider the
class shown in Example 16-7.
Example 16-7. The Account class
package com.oreilly.programmingflex.serialization { public class Account { private var _firstName:String; private var _lastName:String; public function get firstName():String { return _firstName; } public function set firstName(value:String):void { _firstName = value; } public function get lastName():String { return _lastName; } public function set lastName(value:String):void { _lastName = value; } public function Account() {} public function getFullName():String { return _firstName + " " + _lastName; } } }
If you try to write an object of this type to a shared object, it
correctly serializes the firstName
and lastName
properties
(getters/setters). That means that when you read the data back from the
shared object, it displays those values properly. However, it throws an
error if you attempt to call getFullName()
because the deserialized object
won't be of type Account
. To test
this we’ll use two MXML applications called A and B. A is defined as
shown in Example 16-8, and it sets the shared object
data.
Example 16-8. Application A
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" initialize="initializeHandler(event)"> <mx:Script> <![CDATA[ import flash.net.SharedObject; import mx.controls.Alert; import com.oreilly.programmingflex.serialization.Account; private var _sharedObject:SharedObject; private function initializeHandler(event:Event):void { _sharedObject = SharedObject.getLocal("test", "/"); var account:Account = new Account(); account.firstName = "Joey"; account.lastName = "Lott"; _sharedObject.data.account= account; try { var status:String = _sharedObject.flush(); if(status == SharedObjectFlushStatus.PENDING) { _sharedObject.addEventListener(NetStatusEvent.NET_STATUS, flushStatusHandler); } } catch (error:Error) { Alert.show("You must allow local data storage."); } } private function flushStatusHandler(event:NetStatusEvent):void { event.target.removeEventListener(NetStatusEvent.NET_STATUS, flushStatusHandler); if(event.info.code == "SharedObject.Flush.Failed") { Alert.show("You must allow local data storage."); } } ]]> </mx:Script> </mx:Application>
Application B, shown in Example 16-9, reads
the shared object data and attempts to display the data in alert pop
ups. Note that it will correctly display firstName
and lastName
, but it will throw an error on
getFullName()
.
Example 16-9. Application B
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" initialize="initializeHandler(event)"> <mx:Script> <![CDATA[ import flash.net.SharedObject; import flash.utils.describeType; import com.oreilly.programmingflex.serialization.Account; import mx.controls.Alert; private var _sharedObject:SharedObject; private function initializeHandler(event:Event):void { _sharedObject = SharedObject.getLocal("test", "/"); try { var account:Account = _sharedObject.data.account as Account; Alert.show(account.firstName + " " + account.lastName); Alert.show(account.getFullName()); } catch (error:Error) { Alert.show(error.toString()); } } ]]> </mx:Script> </mx:Application>
If you want to store the type in the serialized data, you can use
either of two approaches: the flash.net.registerClassAlias()
function or the
RemoteClass
metadata tag. The registerClassAlias()
function and RemoteClass
tag do the same thing by allowing
you to map the class to an alias. The alias is written to the serialized
data. When the data is deserialized, Flash Player automatically
instantiates the object as the specified type. The following revisions
to applications A and B will cause the Account
data to deserialize as an Account
object. Although you can use either
the registerClassAlias()
function or
the RemoteClass
metadata tag, the
latter is simpler and is specific to Flex. Therefore, we’ll look at how
to use the RemoteClass
metadata tag
to achieve the goal in this section.
Example 16-10 shows
the new Account
class. Because the
code now registers the class to the alias, Account
, it will store the alias in the
serialized data as well.
Example 16-10. Application A registering a class alias
package com.oreilly.programmingflex.serialization { [RemoteClass(alias="Account")] public class Account { private var _firstName:String; private var _lastName:String; public function get firstName():String { return _firstName; } public function set firstName(value:String):void { _firstName = value; } public function get lastName():String { return _lastName; } public function set lastName(value:String):void { _lastName = value; } public function Account() {} public function getFullName():String { return _firstName + " " + _lastName; } } }
You’ll notice that the RemoteClass
metadata tag appears just prior to
the class declaration. It requires one attribute called alias
to which you must assign the alias to use for the
class.
When you register a class, it must not have any required parameters in the constructor. If it does, Flash Player throws an error when trying to deserialize the data.
The default serialization and deserialization for custom classes
work well for standard value object-style data model types. However, if
you want to serialize and deserialize any nonpublic state settings, you
must implement flash.utils.IExternalizable
. When a class implements IExternalizable
, Flash Player automatically
uses the custom serialization and deserialization you define rather than
the standard. That allows you much more control over what the objects
will store and how they will store it.
The IExternalizable
interface
requires two methods, called writeExternal()
and readExternal()
. The writeExternal()
method requires a flash.utils.IDataOutput
parameter, and the
readExternal()
method requires a
flash.utils.IDataInput
parameter. Both IDataInput
and IDataOutput
provide interfaces for working with binary data. IDataInput
lets you read data using methods
such as readByte()
, readUTF()
, and readObject()
. IDataOutput
lets you write data using methods
like writeByte()
, writeUTF()
, and writeObject()
. The writeExternal()
method is called when the
object needs to be serialized. You must write all data to the IDataOutput
parameter that you want to store.
The readExternal()
method is called
when the object is deserialized. You must read all the data from the
IDataInput
parameter. The data you
read from the IDataInput
parameter is
in the same order as the data you write to the IDataOutput
parameter. Example 16-11 rewrites Account
using IExternalizable
. There’s no setter method for
firstName
or lastName
, which proves the data is set via the
customized deserialization.
Example 16-11. Account rewritten to implement IExternalizable
package com.oreilly.programmingflex.serialization { import flash.utils.IExternalizable; import flash.utils.IDataInput; import flash.utils.IDataOutput; public class Account implements IExternalizable { private var _firstName:String; private var _lastName:String; public function get firstName():String { return _firstName; } public function get lastName():String { return _lastName; } public function Account(first:String = "", last:String = "") { _firstName = first; _lastName = last; } public function getFullName():String { return _firstName + " " + _lastName; } public function readExternal(input:IDataInput):void { _firstName = input.readUTF(); _lastName = input.readUTF(); } public function writeExternal(output:IDataOutput):void { // Verify that _firstName is not null because this method may get called // when the data is null. Only serialize when the object is non-null. if(_firstName != null) { output.writeUTF(_firstName); output.writeUTF(_lastName); } } } }