On [Transient], ObjectUtil.copy(), and Casting

| 1 Comment | No TrackBacks

I ran into an interesting situation not too long ago where ObjectUtil.copy wasn't working quite as I had expected. The solution that I came up with was to rely on [Transient] metadata. Confused? Let me explain...

ObjectUtil.copy is a wonderful utility method to perform a deep copy of an object. Its implementation is also amazingly simple:

var buffer:ByteArray = new ByteArray();
buffer.writeObject(value);
buffer.position = 0;
var result:Object = buffer.readObject();
return result;

Rather than using introspection and recursively copying properties from one object to another, the entire object is serialized into a array of bytes. When the bytes are deserialized, a brand new object is created copying all of the original contents.

Simple, but effective. Instead of writing a custom deep copy algorithm , the copy() method uses the built in Flash Player AMF capabilities. This is the same serialization that Flash Remoting uses. When you send an object to the server via Flash Remoting (<mx:RemoteObject/>), you send an AMF array of bytes describing the object that the server deserializes to create an identical object on the server.

Because of using AMF behind the scenes to perform object copies, a few interesting things happen:

  • If you try to cast the results of a copy to a class, the cast might fail.
  • Using the [Transient] metadata tag affects the output of copy().

When an object is deserialized from AMF, it does not automatically get created as a class instance. The object might have all of the properties of a class instance, but it will not be a true class instance unless the AMF packet includes type information about the object. The type information gets added to AMF in one of two ways:

If you're using [RemoteClass] metadata on a class instance being copied, then it is safe to cast the result as that particular class instance:

// The works if Book has [RemoteClass] metadata
var bookCopy:Book = Book( ObjectUtil.copy( book ) );

If you're not using [RemoteClass], before performing the copy you need to register the class against a string alias. The alias is written to the AMF packet so that when the object is deserialized, it can be created as the proper type. For example:

// Book doesn't have [RemoteClass] metadata, so associate the my.package.Book string
// with a reference to the Book class.
registerClassAlias( "my.package.Book", Book );

// Now we can cast the result of the copy without errors.
var bookCopy:Book = Book( ObjectUtil.copy( book ) );

So, what about [Transient], and how is this related to the issue I ran into where the copy wasn't working as I had expected? Now that you know how copy works behind the scenes, let's talk about [Transient].

The [Transient] metadata tag is not very well documented in Flex 2 or 2.0.1. If you look in some of the LiveCycle Data Services documentation you can see mentions of it, but there isn't a dedicated page on the subject. In the beta Flex 3 documentation, [Transient] is finally explained. There is also some documentation gathered on The Flex Non-Docs weblog.

Essentially, what it comes to is that using [Transient] on a property removes that property from the AMF packet during serialization. This is important. The reason [Transient] properties are not sent over the wire via Flash Remoting is because, again, they're not included in the AMF packet.

With the background information explained, onto the problem I ran into. Here's a small class that doesn't copy correctly (after the jump):

package
{

public class Example
{
	
	/** Constant for when the flag value indicates option 1. */
	public static const OPTION_1:String = "1";
	
	/** Constant for when the flag value indicates option 2. */
	public static const OPTION_2:String = "2";
	
	/** Constant for when th flag vlaue indicates no option. */
	public static const OPTION_NONE:String = "0";

	/** One of the option constants, determines what value is used for. */
	public var flag:String;
	
	/** The numeric value for whatever option the flag indicates. */
	public var value:int;
	
	// ================================
	//  option1 property
	// ================================
	
	/** 
	 * Helper method to get/set the value of option 1.  Returns -1 if option 1
	 * is not the flag value.
	 */
	public function get option1():int
	{
		return flag == OPTION_1 ? value : -1;
	}
	
	public function set option1( value:int ):void
	{
		// Anytime the option1 setter is called, set the flag and save the value
		flag = OPTION_1;
		this.value = value;
	}
	
	// ================================
	//  option2 property
	// ================================
	
	/** 
	 * Helper method to get/set the value of option 2.  Returns -1 if option 2
	 * is not the flag value.
	 */
	public function get option2():int
	{
		return flag == OPTION_2 ? value : -1;
	}
	
	public function set option2( value:int ):void
	{
		// Anytime the option2 setter is called, set the flag and save the value
		flag = OPTION_2;
		this.value = value;
	}

} // end class
} // end package

What's important about this class is that there are really only two values that we care about - flag, and value. I'm using option1 and option2 as helper getter/setters to read and manipulate those properties.

Trying to copy this class yields some interesting behavior:

// Create an object and assign a value
var example:Example = new Example();
example.option2 = 123;
				
// Register the class so deserialization preserves the type information
registerClassAlias( "Example", Example );
				
// Make a copy of the original object
var copy:Example = Example( ObjectUtil.copy( example ) );
				
trace( "example: " + example.option2 ); // example: 123
trace( "copy: " + copy.option2 ); 		// copy: -1

trace( example.flag );  // 2
trace( copy.flag );		// 1

Since example and copy have different option2 values, obviously the copy didn't complete as expected. Where did we go wrong?

When the copy() method serialized the data as AMF, Flash Player not only wrote the flag and value properties and their values, but it also wrote the option1 and option2 values. When the deserialization happened, all 4 properties were read back in. The option1 property was read in after option2. This means the option1 setter was called and changed the values of both flag and value incorrectly. Oops!

The solution, now that we know how [Transient] relates to AMF, is to tag the option1 and option2 helper properties as transient. This excludes them from the AMF serialization and prevents errors when the setters would be called during deserialization:

// Flag option1 as transient so it is not included as part
// of the object when copied or sent to the server
[Transient]
public function get option1():int
// ...
// ...likewise with option2

After adding [Transient], when the example code is run again only flag and value are serialized (and therefore deserialized and copied). This corrects the original behavior and the copy's option2 value is the same as example's value.

I hope this helps in your understanding of AMF, how it relates to class instances and casting, and what kind of effect the [Transient] metadata actually has behind the scenes. Even if a class is never meant to be sent to the server, [Transient] can still be useful.

No TrackBacks

TrackBack URL: http://www.darronschall.com/mt/mt-tb.cgi/152

1 Comment

Great info Darron, I've been working on understanding AS3's serialization and the AMF format for AIR type apps, and have been looking for just this feature (after using the similar one in Java)

Been covering some of the tips I've found along the way on my blog, like here:
http://troyworks.com/blog/?cat=30

Leave a comment



About this Entry

This page contains a single entry by darron published on August 13, 2007 10:20 AM.

Flex 2.0.1 Hotfix 3 - The WebService Fix was the previous entry in this blog.

Do you use the public Flex bug base? is the next entry in this blog.

Find recent content on the main index or look in the archives to find all content.

Archives

OpenID accepted here Learn more about OpenID
Powered by Movable Type 5.02