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

5 Responses to Using a proxy to detect changes to models

  1. BadDude says:

    Hi Matt.I tried this example in Flash, like this:import mx.data.binding.*;var model:Object = new Object()var srcEndPoint = {component:src_txt, property:”text”, event:”focusOut”};var destEndPoint = {component:model, property:”name”};new mx.data.binding.Binding(srcEndPoint, destEndPoint);var srcEndPoint = {component:dest_txt, property:”text”, event:”focusOut”};var destEndPoint = {component:model, property:”street”};new mx.data.binding.Binding(srcEndPoint, destEndPoint);var cdp:ChangeDetectingProxy = new ChangeDetectingProxy()cdp.proxyFor = modelcdp.propertyList = [“name”, “street”]function save() {//somehow save the modelcdp.isDirty = false;}function exit() {if (cdp.isDirty) {trace(“You haven’t saved your data, please do so before leaving.”);} else {trace(‘Goodbye’);}}var listener:Object = new Object()listener.name = function(evtObj) {trace(“Something happened”)}model.addEventListener(“name”, listener),but with no success. If I set the property ‘name’ like cdp.name = ‘something’, it works, but if I write something in the text field, the isDirty remains false, but the cdp.name returns the ‘something’ value. What’s wrong, can you help me?thx, _bad

  2. Matt says:

    I think in your bindings the component needs to be cdp, not model.

  3. BadDude says:

    Why?The proxy is for the ‘model’ object. Anyway, tried that, and then the proxys ‘name’ and ‘street’ property will be undefined.If I set manually the cdp.name property, everything goes well. But through this binding, the cdp.name property got its value, but is seems that the class’ set method doesn’t fired.Do you understand what I mean? Sorry for my english ;)If I make a ‘debug’ button, which trace out the value of the cdp.name and the isDirty, it give me back the correct, changed value, but isDirty stay false. If I set the name manually to something before the trace, isDirty going to true.

  4. Matt says:

    I have to admit I really don’t know the Flash binding system too well. You say that the proxy works when you set things manually; that’s really all I can guarantee as Flash binding may do things differently than Flex. Sorry I can’t be of more help.

  5. BadDude says:

    Ok, ok, that’s ok :)))))I just made this experience, and I just want to know, if I misunderstood about your class. Thank you for your attention._bad