//////////////////////////////////////////////////////////////////////////////// // // 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 { import mx.collections.errors.ItemPendingError; import mx.core.mx_internal; import flash.utils.Dictionary; use namespace mx_internal; [ExcludeClass] /** * @private */ public class Binding { include "../core/Version.as"; // Certain errors are normal during binding execution, so we swallow them. // 1507 - invalid null argument // 2005 - argument error (null gets converted to 0) mx_internal static var allowedErrors:Object = generateAllowedErrors(); mx_internal static function generateAllowedErrors():Object { var o:Object = {}; o[1507] = 1; o[2005] = 1; return o; } //-------------------------------------------------------------------------- // // Constructor // //-------------------------------------------------------------------------- /** * Create a Binding object * * @param document The document that is the target of all of this work. * * @param srcFunc The function that returns us the value * to use in this Binding. * * @param destFunc The function that will take a value * and assign it to the destination. * * @param destString The destination represented as a String. * We can then tell the ValidationManager to validate this field. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public function Binding(document:Object, srcFunc:Function, destFunc:Function, destString:String, srcString:String = null) { super(); this.document = document; this.srcFunc = srcFunc; this.destFunc = destFunc; this.destString = destString; this.srcString = srcString; if (this.srcFunc == null) { this.srcFunc = defaultSrcFunc; } if (this.destFunc == null) { this.destFunc = defaultDestFunc; } _isEnabled = true; isExecuting = false; isHandlingEvent = false; hasHadValue = false; uiComponentWatcher = -1; BindingManager.addBinding(document, destString, this); } //-------------------------------------------------------------------------- // // Variables // //-------------------------------------------------------------------------- /** * @private * Internal storage for isEnabled property. */ mx_internal var _isEnabled:Boolean; /** * @private * Indicates that a Binding is enabled. * Used to disable bindings. */ mx_internal function get isEnabled():Boolean { return _isEnabled; } /** * @private */ mx_internal function set isEnabled(value:Boolean):void { _isEnabled = value; if (value) { processDisabledRequests(); } } /** * @private * Indicates that a Binding is executing. * Used to prevent circular bindings from causing infinite loops. */ mx_internal var isExecuting:Boolean; /** * @private * Indicates that the binding is currently handling an event. * Used to prevent us from infinitely causing an event * that re-executes the the binding. */ mx_internal var isHandlingEvent:Boolean; /** * @private * Queue of watchers that fired while we were disabled. * We will resynch with our binding if isEnabled is set to true * and one or more of our watchers fired while we were disabled. */ mx_internal var disabledRequests:Dictionary; /** * @private * True as soon as a non-null or non-empty-string value has been used. * We don't auto-validate until this is true */ private var hasHadValue:Boolean; /** * @private * This is no longer used in Flex 3.0, but it is required to load * Flex 2.0.0 and Flex 2.0.1 modules. */ public var uiComponentWatcher:int; /** * @private * It's possible that there is a two-way binding set up, in which case * we'll do a rudimentary optimization by not executing ourselves * if our counterpart is already executing. */ public var twoWayCounterpart:Binding; /** * @private * If there is a twoWayCounterpart, hasHadValue is false, and * isTwoWayPrimary is true, then the twoWayCounterpart will be * executed first. */ public var isTwoWayPrimary:Boolean; /** * @private * True if a wrapped function call does not throw an error. This is used by * innerExecute() to tell if the srcFunc completed successfully. */ private var wrappedFunctionSuccessful:Boolean; //-------------------------------------------------------------------------- // // Properties // //-------------------------------------------------------------------------- /** * All Bindings hang off of a document for now, * but really it's just the root of where these functions live. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ mx_internal var document:Object; /** * The function that will return us the value. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ mx_internal var srcFunc:Function; /** * The function that takes the value and assigns it. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ mx_internal var destFunc:Function; /** * The destination represented as a String. * This will be used so we can signal validation on a field. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ mx_internal var destString:String; /** * The source represented as a String. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 4 */ mx_internal var srcString:String; /** * @private * Used to suppress calls to destFunc when incoming value is either * a) an XML node identical to the previously assigned XML node, or * b) an XMLList containing the identical node sequence as the previously assigned XMLList */ private var lastValue:Object; //-------------------------------------------------------------------------- // // Methods // //-------------------------------------------------------------------------- private function defaultDestFunc(value:Object):void { var chain:Array = destString.split("."); var element:Object = document; var i:uint = 0; if (chain[0] == "this") { i++; } while (i < (chain.length - 1)) { element = element[chain[i++]]; } element[chain[i]] = value; } private function defaultSrcFunc():Object { return document[srcString]; } /** * Execute the binding. * Call the source function and get the value we'll use. * Then call the destination function passing the value as an argument. * Finally try to validate the destination. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public function execute(o:Object = null):void { if (!isEnabled) { if (o != null) { registerDisabledExecute(o); } return; } if (twoWayCounterpart && !twoWayCounterpart.hasHadValue && twoWayCounterpart.isTwoWayPrimary) { twoWayCounterpart.execute(); hasHadValue = true; return; } if (isExecuting || (twoWayCounterpart && twoWayCounterpart.isExecuting)) { // If there is a twoWayCounterpart already executing, that means that it is // assigning something of value so even though we won't execute we should be // sure to mark ourselves as having had a value so that future executions will // be correct. If isExecuting is true but we re-entered, that means we // clearly had a value so setting hasHadValue is safe. hasHadValue = true; return; } try { isExecuting = true; wrapFunctionCall(this, innerExecute, o); } catch(error:Error) { if (allowedErrors[error.errorID] == null) throw error; } finally { isExecuting = false; } } /** * @private * Take note of any execute request that occur when we are disabled. */ private function registerDisabledExecute(o:Object):void { if (o != null) { disabledRequests = (disabledRequests != null) ? disabledRequests : new Dictionary(true); disabledRequests[o] = true; } } /** * @private * Resynch with any watchers that may have updated while we were disabled. */ private function processDisabledRequests():void { if (disabledRequests != null) { for (var key:Object in disabledRequests) { execute(key); } disabledRequests = null; } } /** * @private * Note: use of this wrapper needs to be reexamined. Currently there's at least one situation where a * wrapped function invokes another wrapped function, which is unnecessary (i.e., only the inner function * will throw), and also risks future errors due to the 'wrappedFunctionSuccessful' member variable * being stepped on. Leaving alone for now to minimize pre-GMC volatility, but should be revisited for * an early dot release. * Also note that the set of suppressed error codes below is repeated verbatim in Watcher.wrapUpdate. * These need to be consolidated and the motivations for each need to be documented. */ protected function wrapFunctionCall(thisArg:Object, wrappedFunction:Function, object:Object = null, ...args):Object { wrappedFunctionSuccessful = false; try { var result:Object = wrappedFunction.apply(thisArg, args); wrappedFunctionSuccessful = true; return result; } catch(itemPendingError:ItemPendingError) { itemPendingError.addResponder(new EvalBindingResponder(this, object)); if (BindingManager.debugDestinationStrings[destString]) { trace("Binding: destString = " + destString + ", error = " + itemPendingError); } } catch(rangeError:RangeError) { if (BindingManager.debugDestinationStrings[destString]) { trace("Binding: destString = " + destString + ", error = " + rangeError); } } catch(error:Error) { // Certain errors are normal when executing a srcFunc or destFunc, // so we swallow them: // Error #1006: Call attempted on an object that is not a function. // Error #1009: null has no properties. // Error #1010: undefined has no properties. // Error #1055: - has no properties. // Error #1069: Property - not found on - and there is no default value // We allow any other errors to be thrown. if ((error.errorID != 1006) && (error.errorID != 1009) && (error.errorID != 1010) && (error.errorID != 1055) && (error.errorID != 1069)) { throw error; } else { if (BindingManager.debugDestinationStrings[destString]) { trace("Binding: destString = " + destString + ", error = " + error); } } } return null; } /** * @private * true iff XMLLists x and y contain the same node sequence. */ private function nodeSeqEqual(x:XMLList, y:XMLList):Boolean { var n:uint = x.length(); if (n == y.length()) { for (var i:uint = 0; i < n && x[i] === y[i]; i++) { } return i == n; } else { return false; } } /** * @private */ private function innerExecute():void { var value:Object = wrapFunctionCall(document, srcFunc); if (BindingManager.debugDestinationStrings[destString]) { trace("Binding: destString = " + destString + ", srcFunc result = " + value); } if (hasHadValue || wrappedFunctionSuccessful) { // Suppress binding assignments on non-simple XML: identical single nodes, or // lists over identical node sequences. // Note: outer tests are inline for efficiency if (!(lastValue is XML && lastValue.hasComplexContent() && lastValue === value) && !(lastValue is XMLList && lastValue.hasComplexContent() && value is XMLList && nodeSeqEqual(lastValue as XMLList, value as XMLList))) { destFunc.call(document, value); // Note: state is not updated if destFunc throws lastValue = value; hasHadValue = true; } } } /** * This function is called when one of this binding's watchers * detects a property change. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public function watcherFired(commitEvent:Boolean, cloneIndex:int):void { if (isHandlingEvent) return; try { isHandlingEvent = true; execute(cloneIndex); } finally { isHandlingEvent = false; } } } }