Monthly Archives: July 2011

The Dark World of IExternizable

I never worked with Remote Objects much; I stuck with SOAP, REST., and AMF. What I did learn was how custom objects can represent themselves to an AMF stream, and to local-storage SharedObjects and fileStreams too.

ActionScript has built-in serialization (compatible with java.io.Externalizable interface). While this is very obvious streaming data over AMF pipelines (to LCDS, AMFPHP, Zend, etc.), it quietly makes saving and restoring data pretty painless. These operations use the streaming protocol:

  • RemoteObjects
  • Streaming objects to ByteArray.writeObject()
  • SharedObject (local storage)
  • EncryptedSharedObject

Simple Streaming

Most values and object will stream to and from AMF just fine. To effect this simple protocol, put the [RemoteClass("com.company.application.section.ClassName")] metadata tag (Flex projects only) above the class declaration so the parser knows what class to create. The class constructor must have defaults for all its arguments (or no arguments), and all public properties must writable (i.e. they must be vars or have mutators — aka “setters”).

The streamed properties that are the ones that are public var or have both a public accessor and a mutator. If a class has public properties one does not wish to stream, mark those properies with the [Transient] metadata tag.

[RemoteClass("com.company.application.section.Foo")]
public class Foo 

    public var name : String = "";
    public var id : String = "";

    [Transient]
    public var contact : Contact = null;
}

The streaming system streams and object in by creating a new instance and then setting each property in turn (it will not use the constructor’s parameters).

Streaming Read-Only and Private Properties

If some properties don’t have public mutators (i.e. they are publicly read-only), or one wants to stream non-public attributes, one can write custom serializing and de-serializing routines by implementing the interface IExternizable. It requires two methods:

[RemoteClass("com.company.application.section.Foo")]
public class Foo implements IExternizable
{
    public var name : String  = "";

    public function get id() : String
    {
        return _id;
    }
    private var _id : String = "";

    public function readExternal(input : IDataInput) : void
    {
        name = input.readObject() as String;
        _id = input.readUnsignedInt();

                // this object might need to be IExternizable too
        _contact = input.readObject() as Contact;
    }
    public function writeExternal(output : IDataOutput) : void
    {
        output.writeObject(name);   //  same sequence as readExternal()
        output.writeUnsignedInt(_id);
        output.writeObject(_contact);
    }
}

The IDataInput has no information about its data, so one cannot, for example, test to see if an integer value is less than 127 and safely call writeByte() instead of writeInt() (to save space) because readExternal() has no way of knowing if it should call readByte() or readInt(). One can stream format and version information out first, and use that information to determine the format of the rest of the data when streaming it in (although this smacks of variable record types from Days Long Past). It’s much easier to always stream the same object types in the same sequence, even writing out some empty strings and nulls as placeholders when necessary.

Any class implementing IExternizable must explicitly stream all its data because the built-in streaming is disabled . Note that the example above will stream the entire nested _contact object out as part of the Foo object stream. If  Contact implemented IExternizable, it’s readExternal() and writeExternal() would handle its streaming too.

Streaming References

If a class contains a references to another object, one can stream out an id and resolve the id after it streams in. One can resolve that id into an object reference either explicitly when both the referer and the cross-reference (e.g. the list of all Contacts) are ready, or do it as a lazy-loading accessor:

[RemoteClass("com.company.application.section.Foo")]
public class Foo implements IExternizable
{
    public var name : String  = "";

    public function get id() : String
    {
        return _id;
    }
    private var _id : String = "";

    public function get contact() : Contact
    {
        if (_contact == null)
        {
            if (_contactId != "")
            {
//
// some mechanism to find the contact by Id; it can return null
//
                _contact = Contact.lookupId(_contactId);
                _contactId = "";
            }
        }

        return _contact;
    }
    private var _contact : Contact = null;
    private var _contactId : String = "";

    public function readExternal(input : IDataInput) : void
    {
        name = input.readObject() as String;
        _id = input.readInt();
        _contactId = input.readObject() as String;
    }

    public function writeExternal(output : IDataOutput) : void
    {
         output.writeObject(name); // same sequence as readExternal()
         output.writeInt(_id);

         if (_contact == null)
            output.writeObject(""); // placeholder
        else
            output.writeObject(_contact.Id);   // just the Id
    }
}

Adding IExternizable to Existing Objects

If you add IExternizable to an object that has been saved and will be retrieved, you need to read the stream in the same sequence (and don’t fail if some new property is not in the stream). You can determine the sequence of properties before adding the writeExternal() by creating mutators for all the public properties, and then debugging while an instance comes in.

See Adobe Documentation on IExternizable

All AIR Applications Are Single-Instance

This is not an option: if one tries to launch an AIR application twice, the first instance remains and no other instances start. The first application, however, does get notification and the command-line arguments of the subsequent application executions. It’s a subtle way of communicating with a running AIR application.

If an application has to handle multiple instanciation (like a registered file reader), it has to be able to present multiple instances of some part of its user interface (or be willing to replace the current data at any time). This hearkens back to the days of Multi-Document Interface (MDI) applications. One can encapsulate the main UI as a component, and the application can create one for each “instance” the application needs to present.

Multiple Invocations of an AIR ApplicationThe NativeWindow.invoke Event Fires On Every Application Launch

The application will get an event every time the OS launches an instance of the application. It gets an event on startup, and it gets one each time the OS executes the AIR application file; subsequent executions do not start additional  instances. These events contains the command-line parameters specific to that invocation.

The Invoke event fits into the startup cycle here:

  1. FlexEvent.ADD for the application object
  2. FlexEvent.PREINITIALIZE
  3. Event.ADDED for the descendents of the application object
    (These events happen sporadically intermixed with the following events)
  4. FlexEvent.INITIALIZE
  5. FlexEvent.CREATION_COMPLETE
  6. Event.ADDED_TO_STAGE
  7. FlexEvent.APPLICATION_COMPLETE
  8. InvokeEvent.INVOKE
  9. Event.ACTIVATE

Command-line Parameters in AIR Applications

First, the simple behavior:

<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
    xmlns:s="library://ns.adobe.com/flex/spark" 
    xmlns:mx="library://ns.adobe.com/flex/mx"
    invoke="onInvoke(event)">
    <fx:Script>
        <![CDATA[
            private function onInvoke(event : InvokeEvent) : void
            {
                logText.text += "invoke: event.arguments = " + 
                    event.arguments.toString();

                if (event.currentDirectory != null) {
                    logText.text += "; event.currentDirectory = " + 
                        event.currentDirectory.nativePath;
                }
                else
                    logText.text += "; event.currentDirectory = null";


                if (event.reason != null)
                    logText.text += "; event.reason = " + event.reason;
                else
                    logText.text += "; event.reason =  null";
            }
        ]]>
    </fx:Script>

    <s:TextArea id="logText" left="10" right="10" top="105" bottom="10" />
</s:WindowedApplication>

 

Things My Event Told Me

  • InvokeEvent.arguments is an array (never null) of strings. See your operating system for the rules about special characters and quoting.
  • InvokeEvent.currentDirectory is a File instance set to the directory of the executable. Note that running from the IDE will point to the FlashBuilder.exe directory; running from a shortcut/alias will indicate the location of the shortcut, not the .air file.
  • InvokeEvent.reason is either “standard” or “login” if the  OS starts it automatically (see InvokeEventReason for constants)

Things My Event Never Told Me

  • The event does not indicate if this event is part of the application startup (i.e. the first event) or a subsequent invocation. Use a global counter.
  • The event does not indicate if this event is because the OS registered this application for a file type and the user “opened” a file of that type. The sole argument is the complete (native) path including file name, but shortcuts and the command-line can start the application can have a single argument that is a file path as well.