Persistent Data

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.

Note

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.

Creating 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");

Reading and Writing to Shared Objects

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);

Note

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.

Controlling Scope

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.

Note

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.

Using Local Shared Objects

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.

Customizing Serialization

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);
            }
        }

    }
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset