//////////////////////////////////////////////////////////////////////////////// // // Licensed to the Apache Software Foundation (ASF) under one or more // contributor license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright ownership. // The ASF licenses this file to You under the Apache License, Version 2.0 // (the "License"); you may not use this file except in compliance with // the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // //////////////////////////////////////////////////////////////////////////////// package mx.binding.utils { import flash.events.IEventDispatcher; import flash.events.Event; import mx.core.EventPriority; import mx.binding.BindabilityInfo; import mx.events.PropertyChangeEvent; import mx.utils.DescribeTypeCache; /** * The ChangeWatcher class defines utility methods * that you can use with bindable Flex properties. * These methods let you define an event handler that is executed * whenever a bindable property is updated. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public class ChangeWatcher { include "../../core/Version.as"; //-------------------------------------------------------------------------- // // Class methods // //-------------------------------------------------------------------------- /** * Creates and starts a ChangeWatcher instance. * A single ChangeWatcher instance can watch one property, * or a property chain. * A property chain is a sequence of properties accessible from * a host object. * For example, the expression * obj.a.b.c contains the property chain (a, b, c). * * @param host The object that hosts the property or property chain * to be watched. * You can use the use the reset() method to change * the value of the host argument after creating * the ChangeWatcher instance. * The host maintains a list of handlers to invoke * when prop changes. * * @param chain A value specifying the property or chain to be watched. * Legal values are: * * *

Note: The property or properties named in the chain argument * must be public, because the describeType() method suppresses all information * about non-public properties, including the bindability metadata * that ChangeWatcher scans to find the change events that are exposed * for a given property. * However, the getter function supplied when using the { name, getter } * argument form described above can be used to associate an arbitrary * computed value with the named (public) property.

* * @param handler An event handler function called when the value of the * watched property (or any property in a watched chain) is modified. * The modification is signaled when any host object in the watcher * chain dispatches the event that has been specified in that host object's * [Bindable] metadata tag for the corresponding watched property. * The default event is named propertyChange. * *

The event object dispatched by the bindable property is passed * to this handler function without modification. * By default, Flex dispatches an event object of type PropertyChangeEvent. * However, you can define your own event type when you use the * [Bindable] metadata tag to define a bindable property.

* * @param commitOnly Set to true if the handler should be * called only on committing change events; * set to false if the handler should be called on both * committing and non-committing change events. * Note: the presence of non-committing change events for a property is * indicated by the [NonCommittingChangeEvent(<event-name>)] metadata tag. * Typically these tags are used to indicate fine-grained value changes, * such as modifications in a text field prior to confirmation. * * @param useWeakReference (default = false) Determines whether * the reference to handler is strong or weak. A strong * reference (the default) prevents handler from being * garbage-collected. A weak reference does not. * * @return The ChangeWatcher instance, if at least one property name has * been specified to the chain argument; null otherwise. * Note that the returned watcher is not guaranteed to have successfully * discovered and attached itself to change events, since none may have * been exposed on the given property or chain by the host. * You can use the isWatching() method to determine the * watcher's state. * * @see mx.events.PropertyChangeEvent * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public static function watch(host:Object, chain:Object, handler:Function, commitOnly:Boolean = false, useWeakReference:Boolean = false):ChangeWatcher { if (!(chain is Array)) chain = [ chain ]; if (chain.length > 0) { var w:ChangeWatcher = new ChangeWatcher(chain[0], handler, commitOnly, watch(null, chain.slice(1), handler, commitOnly)); w.useWeakReference = useWeakReference; w.reset(host); return w; } else { return null; } } /** * Lets you determine if the host exposes a data-binding event * on the property. * *

NOTE: Property chains are not supported by the canWatch() method. * They are supported by the watch() method.

* * @param host The host of the property. * See the watch() method for more information. * * @param name The name of the property. * See the watch() method for more information. * * @param commitOnly Set to true if the handler * should be called only on committing change events. * See the watch() method for more information. * * @return true if host exposes * any change events on name. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public static function canWatch(host:Object, name:String, commitOnly:Boolean = false):Boolean { return !isEmpty(getEvents(host, name, commitOnly)); } /** * Returns all binding events for a bindable property in the host object. * * @param host The host of the property. * See the watch() method for more information. * * @param name The name of the property, or property chain. * See the watch() method for more information. * * @param commitOnly Controls inclusion of non-committing * change events in the returned value. * * @return Object of the form { eventName: isCommitting, ... } * containing all change events for the property. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public static function getEvents(host:Object, name:String, commitOnly:Boolean = false):Object { if (host is IEventDispatcher) { // Get { eventName: isCommitting, ... } for all change events // defined by host's class on prop var allEvents:Object = DescribeTypeCache.describeType(host). bindabilityInfo.getChangeEvents(name); if (commitOnly) { // Filter out non-committing events. var commitOnlyEvents:Object = {}; for (var ename:String in allEvents) if (allEvents[ename]) commitOnlyEvents[ename] = true; return commitOnlyEvents; } else { return allEvents; } } else { // If host is not IEventDispatcher, no events will be dispatched, // regardless of metadata. // User-supplied [Bindable("eventname")] metadata within a // non-IEventDispatcher class is unlikely, // but not specifically prohibited by the compiler currently. return {}; } } /** * Lets you determine if an Object has any properties. * * @param Object to inspect. * * @return true if Object has no properties. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ private static function isEmpty(obj:Object):Boolean { for (var p:String in obj) { return false; } return true; } //-------------------------------------------------------------------------- // // Constructor // //-------------------------------------------------------------------------- /** * Constructor. * Not for public use. This method is called only from the watch() method. * See the watch() method for parameter usage. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public function ChangeWatcher(access:Object, handler:Function, commitOnly:Boolean = false, next:ChangeWatcher = null) { super(); host = null; name = access is String ? access as String : access.name; getter = access is String ? null : access.getter; this.handler = handler; this.commitOnly = commitOnly; this.next = next; events = {}; useWeakReference = false; isExecuting = false; } //-------------------------------------------------------------------------- // // Variables // //-------------------------------------------------------------------------- /** * Host object. * Volatile; may be reset, and will fluctuate in non-root watchers * due to property value changes. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ private var host:Object; /** * Name of watched property. * Nonvolatile. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ private var name:String; /** * Optional user-supplied getter function. * Nonvolatile. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ private var getter:Function; /** * User-supplied event handler function. * Nonvolatile. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ private var handler:Function; /** * True iff watching only committing events. * Nonvolatile. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ private var commitOnly:Boolean; /** * If watching a chain, this is a watcher on the next property * in the chain: next.host == host[name]. * Null otherwise. Nonvolatile. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ private var next:ChangeWatcher; /** * Object { : , ... } for current host[name]. * Volatile; varies with host. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ private var events:Object; /** * True while handling a change event. Used to prevent two way * bindings from getting into an infinite loop. */ private var isExecuting:Boolean; //-------------------------------------------------------------------------- // // Properties // //-------------------------------------------------------------------------- //---------------------------------- // useWeakReference //---------------------------------- /** * Determines whether the reference to handler * is strong or weak. * A strong reference (the default) prevents * handler from being garbage-collected. * A weak reference does not. * * @default false * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public var useWeakReference:Boolean; //-------------------------------------------------------------------------- // // Methods // //-------------------------------------------------------------------------- /** * Detaches this ChangeWatcher instance, and its handler function, * from the current host. * You can use the reset() method to reattach * the ChangeWatcher instance, or watch the same property * or chain on a different host object. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public function unwatch():void { reset(null); } /** * Retrieves the current value of the watched property or property chain, * or null if the host object is null. * For example: *
     *  watch(obj, ["a","b","c"], ...).getValue() === obj.a.b.c
     *  
* * @return The current value of the watched property or property chain. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public function getValue():Object { return host == null ? null : next == null ? getHostPropertyValue() : next.getValue(); } /** * Sets the handler function. * * @param handler The handler function. This argument must not be null. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public function setHandler(handler:Function):void { this.handler = handler; if (next) next.setHandler(handler); } /** * Returns true if each watcher in the chain is attached * to at least one change event. * Note that the isWatching() method * varies with host, since different hosts may expose different change * events for the watcher's chosen property. * * @return true if each watcher in the chain is attached * to at least one change event. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public function isWatching():Boolean { return !isEmpty(events) && (next == null || next.isWatching()); } /** * Resets this ChangeWatcher instance to use a new host object. * You can call this method to reuse a watcher instance * on a different host. * * @param newHost The new host of the property. * See the watch() method for more information. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public function reset(newHost:Object):void { var p:String; if (host != null) { for (p in events) { host.removeEventListener(p, wrapHandler); } events = {}; } host = newHost; if (host != null) { events = getEvents(host, name, commitOnly); for (p in events) { host.addEventListener(p, wrapHandler, false, EventPriority.BINDING, useWeakReference); } } if (next) next.reset(getHostPropertyValue()); } /** * @private * Retrieves the current value of the root watched property, * or null if host is null. * I.e. watch(obj, ["a","b","c"], ...).getHostPropertyValue() === obj.a */ private function getHostPropertyValue():Object { return host == null ? null : getter != null ? getter(host) : host[name]; } /** * @private * Listener for change events. * Resets chained watchers and calls user-supplied handler. */ private function wrapHandler(event:Event):void { if (!isExecuting) { try { isExecuting = true; if (next) next.reset(getHostPropertyValue()); if (event is PropertyChangeEvent) { if ((event as PropertyChangeEvent).property == name) handler(event as PropertyChangeEvent); } else { handler(event); } } finally { isExecuting = false; } } } } }