Dynamic ObjectProxy

Flex Published on December 6, 2009 by Andrea Bresolin Add comments
Live demoSource code (LICENSE)

The problem: I’ve got a Flex class with all its properties well defined. It’s good for me, it has all the properties I need to represent correctly the model, but sometimes I wish I had a few more properties to use it in some contexts. These additional properties are not strictly related to the model I want to represent, so I don’t want to add them to the class, but I need them to easily integrate with Flex components functionalities for user interaction.

The solution: DynaObjectProxy!

Before looking through the source code of DynaObjectProxy, let’s talk briefly about what it does. Basically, it’s a sort of ObjectProxy, but it extends its functionalities.

With ObjectProxy you can wrap a class instance and access to its properties through the proxy

// MyClass contains the property "prop1"
var myClassInstance: MyClass = new MyClass();
myClassInstance.prop1 = "The value";

var myProxy: ObjectProxy = new ObjectProxy(myClassInstance);

trace(myProxy.prop1); // Outputs "The value"

but you cannot dynamically create new properties in the ObjectProxy instance

trace(myProxy.prop2); // ERROR! "prop2" doesn't exist in MyClass

Otherwise, you can create an ObjectProxy instance without wrapping a class instance and in this case you actually can create new properties in the ObjectProxy

var myProxy: ObjectProxy = new ObjectProxy();
myProxy.prop2 = "The property value";

trace(myProxy.prop2); // Outputs "The property value"

What if I would like to have both the functionalities? DynaObjectProxy allows exactly this: you can both wrap a class instance and create new properties keeping all the new properties bindable at the same time.

Now we can start looking through the DynaObjectProxy source code and later we’ll see how it can be used with an example.

package com.devahead.utils
{
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IEventDispatcher;
	import flash.utils.Proxy;
	import flash.utils.describeType;
	import flash.utils.flash_proxy;
	import flash.utils.getQualifiedClassName;

	import mx.events.PropertyChangeEvent;
	import mx.utils.ObjectProxy;

	[Bindable("propertyChange")]
	public dynamic class DynaObjectProxy extends Proxy implements IEventDispatcher
	{
		/**
		 * EventDispatcher used by the proxy to dispatch events.
		 */
		protected var _dispatcher: EventDispatcher;

		/**
		 * Object managed by the proxy.
		 */
		protected var _managedObj: Object = null;

		/**
		 * ObjectProxy used to managed the _managedObj.
		 */
		protected var _managedObjProxy: ObjectProxy = null;

		/**
		 * ObjectProxy used to allow the dynamic creation of
		 * new bindable properties.
		 */
		protected var _dynamicObjProxy: ObjectProxy = null;

		/**
		 * List of the properties belonging to the proxy. It contains, in
		 * alphabetical order, the list of all the properties beloging to
		 * the _managedObj plus those created dynamically on the _dynamicObjProxy.
		 */
		protected var _propertyList: Array = null;

		/**
		 * Constructor.
		 *
		 * @param managedObj
		 *         Object managed by the proxy.
		 */
		public function DynaObjectProxy(managedObj: Object = null)
		{
			super();

			_managedObj = managedObj;

			_propertyList = new Array();

			// Create the ObjectProxy to manage the managedObj and the one to manage
			// the dynamically created properties.
			_managedObjProxy = (managedObj != null ?
				new ObjectProxy(managedObj) :
				new ObjectProxy());

			_dynamicObjProxy = new ObjectProxy();

			// Create the events dispatcher and attach the appropriate listeners
			// to both the ObjectProxy.
			_dispatcher = new EventDispatcher(this);
			_managedObjProxy.addEventListener(PropertyChangeEvent.PROPERTY_CHANGE, onPropertyChangeEvent);
			_dynamicObjProxy.addEventListener(PropertyChangeEvent.PROPERTY_CHANGE, onPropertyChangeEvent);
		}

		/**
		 * Returns all the properties beloging to an object.
		 *
		 * @param obj
		 *         Object.
		 * @return All the object's properties.
		 */
		public static function getObjectProperties(obj: Object): Array
		{
			var result: Array = new Array();

			var objClassInfo: XML = describeType(obj);

			for each (var v: XML in objClassInfo..accessor)
			{
				result.push(String(v.@name));
			}

			return result;
		}

		/**
		 * Returns the original instance of the object managed internally
		 * by the proxy.
		 */
		public function get managedObj(): Object
		{
			return _managedObj;
		}

		/**
		 * Update the list of all the properties available in the proxy (those
		 * visible from outside the proxy by its users).
		 * The list contains, in alphabetical order, the set of the properties
		 * in the managedObj and those in the dynamicObj.
		 */
		protected function updatePropertyList(): void
		{
			var newPropertyList: Array = new Array();

			if (_managedObj != null)
			{
				if (getQualifiedClassName(_managedObj) == "Object")
				{
					// This is an Object so I can get all of its properties
					// through a cycle.
					for (var managedPropName: String in _managedObj)
					{
						newPropertyList.push(managedPropName);
					}
				}
				else
				{
					// This is likely to be a typed object so I get all its properties
					// through the getObjectProperties function.
					newPropertyList = getObjectProperties(_managedObj);
				}
			}

			// Add to the list also the properties created dynamically in the
			// dynamicObj.
			for (var dynamicPropName: String in _dynamicObjProxy)
			{
				newPropertyList.push(dynamicPropName);
			}

			// Update the _propertyList and sort it in alphabetical order.
			_propertyList = newPropertyList;
			_propertyList.sort();
		}

		/**
		 * Handler for the properties changes in the managedObj and the dynamicObj.
		 * This function dispatches again the PropertyChangeEvent as if it was
		 * generated by this proxy instead of the internal ObjectProxy used for
		 * managedObj and dynamicObj.
		 *
		 * @param event
		 *         Event.
		 */
		protected function onPropertyChangeEvent(event: PropertyChangeEvent): void
		{
			var newEvent: PropertyChangeEvent = new PropertyChangeEvent(event.type,
				event.bubbles, event.cancelable, event.kind, event.property,
				event.oldValue, event.newValue, this);

			dispatchEvent(newEvent);
		}

		// *** Implementation of Proxy methods ***

		override flash_proxy function getProperty(name: *): *
		{
			// If the property is in the managedObj then I return its value,
			// otherwise I use property's value from the dynamicObj.
			return (_managedObjProxy.hasOwnProperty(name) ?
				_managedObjProxy[name] :
				_dynamicObjProxy[name]);
		}

		override flash_proxy function callProperty(name: *, ... rest): *
		{
			// If the function is in the managedObj then I call it,
			// otherwise I call the function in the dynamicObj.
			return (_managedObjProxy.hasOwnProperty(name) ?
				_managedObjProxy[name].apply(_managedObjProxy, rest) :
				_dynamicObjProxy[name].apply(_dynamicObjProxy, rest));
		}

		override flash_proxy function deleteProperty(name: *): Boolean
		{
			// I cannot delete a property from the managedObj, so I delete it directly
			// from the dynamicObj.
			return (_dynamicObjProxy.hasOwnProperty(name) ?
				delete _dynamicObjProxy[name] : false);
		}

		override flash_proxy function hasProperty(name: *): Boolean
		{
			// A property exists if it's inside the managedObj or the dynamicObj
			return (_managedObjProxy.hasOwnProperty(name) ||
				_dynamicObjProxy.hasOwnProperty(name));
		}

		override flash_proxy function nextName(index: int): String
		{
			return _propertyList[index - 1];
		}

		override flash_proxy function nextNameIndex(index: int): int
		{
			if (index == 0)
			{
				// I update the list of alla the properties available in the proxy,
				// those from the managedObj and those dynamically created in the
				// dynamicObj.
				updatePropertyList();
			}

			if (index < _propertyList.length)
			{
				return index + 1;
			}
			else
			{
				return 0;
			}
		}

		override flash_proxy function nextValue(index: int): *
		{
			return flash_proxy::getProperty(_propertyList[index - 1]);
		}

		override flash_proxy function setProperty(name: *, value: *): void
		{
			// If the property is in the managedObj then I set its value, otherwise
			// I set the value on the dynamicObj (the property is created if it
			// doesn't already exist).
			if (_managedObjProxy.hasOwnProperty(name))
			{
				_managedObjProxy[name] = value;
			}
			else
			{
				_dynamicObjProxy[name] = value;
			}
		}

		// *** IEventDispatcher implementation ***

		public function addEventListener(type: String, listener: Function,
			useCapture: Boolean = false, priority: int = 0,
			useWeakReference: Boolean = false): void
		{
			_dispatcher.addEventListener(type, listener, useCapture, priority,
				useWeakReference);
		}

		public function removeEventListener(type: String, listener: Function,
			useCapture: Boolean = false): void
		{
			_dispatcher.removeEventListener(type, listener, useCapture);
		}

		public function dispatchEvent(event: Event): Boolean
		{
			return _dispatcher.dispatchEvent(event);
		}

		public function hasEventListener(type: String): Boolean
		{
			return _dispatcher.hasEventListener(type);
		}

		public function willTrigger(type: String): Boolean
		{
			return _dispatcher.willTrigger(type);
		}
	}
}

The constructor accepts a managedObj argument. That’s the class instance that can be wrapped by the proxy and we call it the managed object. Internally, two different proxies are created, the managedObjProxy and the dynamicObjProxy. The first one is useful to intercept the events related to the managed object, while the second one is used to create new properties and get the events for them as well. Whenever a property is read or written on a DynaObjectProxy instance, the implementation looks for it in the managed object instance through the managedObjProxy and if it’s not able to find it, then it looks inside the dynamicObjProxy. If the property is still not available, if it has to be read, then a null value is returned, otherwise it is created inside the dynamicObjProxy. The list of all the properties in a DynaObjectProxy instance is the union of both the properties available in the managedObjProxy and the dynamicObjProxy. All the events dispatched by the proxy are in fact dispatched through an internal EventDispatcher instance because we cannot extend that class as we are already extending the Proxy class and ActionScript 3 doesn’t support multiple inheritance.

So now we can go on with an example application. Let’s say we want the user to select some fruits that he likes from a list and we want him to be able to specify also their quantity. So a Fruit class could look like this:

package com.devahead.model
{
	[Bindable]
	public class Fruit
	{
		public var name: String;
		public var quantity: int;

		public function Fruit(name: String, quantity: int = 0)
		{
			this.name = name;
			this.quantity = quantity;
		}
	}
}

What is important, to identify a fruit in our model, are its name and its quantity. But in this way, we don’t have a property to specify if a fruit is selected or not in a grid, we don’t want to include it in our model because it’s only important for user interaction. This is especially true if you think about the Fruit class as a representation of a record in a database on the server side. In a FRUITS table the only things that have a meaning are the name and the quantity. It’s meaningless to know if a user selected a fruit on the client side interface because we store only the selected fruits in the database.

In the example below, we are able to dynamically add a isFruitSelected property to all the Fruit instances through the DynaObjectProxy while keeping at the same time the original typed instances. This allows us to put all our DynaObjectProxy in the grid’s dataProvider and the user is able to select the fruits through a checkbox and edit their quantity. When the user is happy with his choices, we are able to get all the selected fruits and we could send them to the sever to store them in a database.

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
	pageTitle="Dynamic ObjectProxy - devahead BLOG"
	layout="vertical" horizontalAlign="left"
	applicationComplete="{onApplicationComplete(event)}"
	viewSourceURL="srcview/index.html">

	<mx:Script>
		<![CDATA[
			import com.devahead.utils.DynaObjectProxy;
			import com.devahead.model.Fruit;
			import mx.collections.ArrayCollection;
			import mx.events.FlexEvent;

			[Bindable]
			protected var fruitsList: ArrayCollection = new ArrayCollection();

			protected function onApplicationComplete(event: FlexEvent): void
			{
				// Wrap the Fruit instances in DynaObjectProxy instances
				// so we can add the "isFruitSelected" property dynamically
				// in the grid.
				fruitsList.addItem(new DynaObjectProxy(new Fruit("Orange")));
				fruitsList.addItem(new DynaObjectProxy(new Fruit("Pear")));
				fruitsList.addItem(new DynaObjectProxy(new Fruit("Apricot")));
			}

			protected function getSelectedFruits(): void
			{
				selFruitsArea.text = "SELECTED FRUITS:\n\n";

				var originalFruitsInstances: ArrayCollection = new ArrayCollection();

				for each (var currFruit: DynaObjectProxy in fruitsList)
				{
					// If the fruit is selected, I add it to the selFruitsArea
					if (currFruit.isFruitSelected)
					{
						selFruitsArea.text += currFruit.name + " (" +
							currFruit.quantity + ")\n";

						// Get the instance of the Fruit class managed by the
						// DynaObjectProxy.
						originalFruitsInstances.addItem(currFruit.managedObj);
					}
				}

				selFruitsArea.text += "\nORIGINAL INSTANCES OF SELECTED FRUITS:\n\n";

				for each (var currOriFruit: Fruit in originalFruitsInstances)
				{
					selFruitsArea.text += currOriFruit.name + " (" +
						currOriFruit.quantity + ")\n";
				}
			}
		]]>
	</mx:Script>

	<mx:Label text="Select some fruits, edit their quantity, and press the 'Get selected fruits' button"
		fontWeight="bold"/>

	<mx:DataGrid width="300" height="100" dataProvider="{fruitsList}"
		editable="true">
		<mx:columns>
			<mx:DataGridColumn headerText="Selected" dataField="isFruitSelected"
				rendererIsEditor="true" itemRenderer="mx.controls.CheckBox"
				editorDataField="selected" textAlign="center"/>
			<mx:DataGridColumn headerText="Name" dataField="name" editable="false"/>
			<mx:DataGridColumn headerText="Quantity" dataField="quantity"/>
		</mx:columns>
	</mx:DataGrid>

	<mx:TextArea id="selFruitsArea" width="300" height="200"/>

	<mx:Button label="Get selected fruits" click="{getSelectedFruits()}"/>
</mx:Application>

If you use something like BlazeDS or web services, you know how much is important to keep the server side and the client side model classes synchronized to easily send them back and forth between the client and the server. This is were DynaObjectProxy turns out to be very useful. You can add temporary properties quickly while keeping the original instances intact. I’ve been using this technique for a very long time and it proved to be useful for me.

Tags: ,

2 Responses to “Dynamic ObjectProxy”

  1. toniloCoyote Says:

    Thank you, Andrea.
    I was struggling for understanding the differences between ObjectProxy and Proxy (and what the Proxy class was for), and now I understand all of these.

  2. Andrea Bresolin Says:

    You’re welcome. I’m glad you found the post useful.

Leave a Reply

WP Theme & Icons by N.Design Studio
©2009-2010 Andrea Bresolin. All rights reserved. - Privacy Policy
Entries RSS Comments RSS