Using a proxy to detect changes to models

Someone recently asked how you might intercept changes on an object so that you could know when it is dirty and needs saving. The catch was
that he wants to use binding to populate the object and doesn’t want to put event handlers on his controls just for this monitoring. Finally, he
wants the solution to be generic (meaning that writing your object to have setters and monitor its own dirty state is not acceptable).

I’ve written up one solution to the problem here, using a proxy object to pass changes through to the final object, and using the proxy as the
indicator of when things are dirty.

import mx.events.EventDispatcher;/*** Dispatched whenever the proxy passes on a change to the destination.  Event properties are:* type: 'change'* target: this proxy* property: the name of the property changing* oldvalue: the value before the property is changed* newvalue: the new value of the property* wasPreviouslyDirty: true if the isDirty flag was set BEFORE this property was set*/[Event("change")]/*** This class can be used to proxy changes through to another object.  It is given a list of properties to proxy* and will pass on any changes that are different than the value already set.  In addition it will dispatch* a 'change' event indicating that the property has changed.  Finally it will mark itself dirty so that* it can be queried as to whether anything has changed.*/dynamic class ChangeDetectingProxy{//these functions will be filled in during event dispatcher initialization//they need to sit above the real fill-in so that it gets overriddenfunction dispatchEvent(obj : Object) : Boolean { return true; }function addEventListener(event : String, handler) : Void { }function removeEventListener(event : String, handler) : Void { }//this makes us an event dispatcherstatic function _SetupEventDispatcher_() : Object{var o:Object = ChangeDetectingProxy.prototype;EventDispatcher.initialize(o);return undefined;}private static var _sed_ = _SetupEventDispatcher_();private static var _dependsOnEventDispatcher_ : EventDispatcher = EventDispatcher;private var __propertyList : Array;/*** The object to which changes will be passed.*/public var proxyFor : Object = null;/*** Indicates whether this object has made any changes.  It is useful if you want to monitor the* saved state of the 'proxyFor' object.  You may reset isDirty any time (though it will be set* to true immediately after dispatching a change event).*/public var isDirty : Boolean = false;[ChangeEvent("propertyListChanged")]/*** An array of strings representing the properties that this proxy will pass through.*/public function get propertyList() : Array{return __propertyList;}public function set propertyList(pl : Array) : Void{__propertyList = pl;//for every property create a getter to return the proxied object's value and a setter//which will pass the change through (only if newvalue != oldvalue) and dispatch the//change eventfor (var i = 0; i < pl.length; ++i){var propname = pl[i];//arguments.callee refers to the function itself and is a way to make sure that each function has its unique//instance of the property namevar getter = function(){return this.proxyFor[arguments.callee.propname];}var setter = function(newvalue){var pn = arguments.callee.propname;var oldvalue = this.proxyFor[pn];if (oldvalue == newvalue) return;this.proxyFor[pn] = newvalue;this.dispatchEvent({type: "change", property: pn, oldvalue: oldvalue, newvalue: newvalue,wasPreviouslyDirty: this.isDirty});this.isDirty = true;}getter.propname = propname;setter.propname = propname;var tmp = undefined;if (this[propname] != undefined){tmp = this[propname];delete this[propname];}this.addProperty(propname, getter, setter);if (tmp != undefined){proxyFor[propname] = tmp;}}dispatchEvent({type: "propertyListChanged"});}}

Using this class you can now create MXML like this:

<?xml version="1.0" encoding="utf-8"?><mx:Application xmlns:mx="http://www.macromedia.com/2003/mxml" xmlns="*" backgroundColor="#ffffff" marginTop="0"><mx:Script>function save(){//somehow save the modelcdp.isDirty = false;}function exit(){if (cdp.isDirty){alert("You haven't saved your data, please do so before leaving.", 'Save Needed');}else{alert('Goodbye');}}</mx:Script><mx:Model id="mod"/><ChangeDetectingProxy id="cdp" proxyFor="{mod}"><propertyList><mx:Array><mx:String>name</mx:String><mx:String>street</mx:String></mx:Array></propertyList></ChangeDetectingProxy><mx:Binding source="nameInput.text" destination="cdp.name"/><mx:Binding source="streetInput.text" destination="cdp.street"/><mx:Panel title="ChangeDetectingProxy Demo" id="p"><mx:Form><mx:FormHeading label="Change an input and press exit to see how it works"/><mx:FormItem label="Name"><mx:TextInput id="nameInput"/></mx:FormItem><mx:FormItem label="Street"><mx:TextInput id="streetInput"/></mx:FormItem></mx:Form><mx:HBox><mx:Button label="Exit" click="exit()"/><mx:Button label="Save" click="save()"/></mx:HBox></mx:Panel></mx:Application>

Which in turn will produce a Flex app like this:

document.write(““);document.write(”“);document.write(”“);document.write(““);