blob: a8c569a2a3fae9fbfd8a069ef5083fc30e2545bd [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// 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;
this.destFuncFailed = false;
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;
/**
* @private
*/
mx_internal var destFuncFailed:Boolean;
/**
* 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++]];
//if the element does not exist : avoid exception as it's heavy on memory allocations
if (element == null) {
destFuncFailed = true;
if (BindingManager.debugDestinationStrings[destString])
{
trace("Binding: destString = " + destString + ", error = 1009");
}
return;
}
}
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);
if(destFuncFailed == true) {
destFuncFailed = false;
return null;
}
wrappedFunctionSuccessful = true;
return result;
}
catch(error:Error)
{
if (error is ItemPendingError) {
(error as ItemPendingError).addResponder(new EvalBindingResponder(this, object));
if (BindingManager.debugDestinationStrings[destString])
{
trace("Binding: destString = " + destString + ", error = " + error);
}
} else if (error is RangeError) {
if (BindingManager.debugDestinationStrings[destString])
{
trace("Binding: destString = " + destString + ", error = " + error);
}
} else {
// 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 if XMLLists x and y contain the same node sequence.
*/
private static 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
{
destFuncFailed = false;
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);
if(destFuncFailed == false) {
// 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;
}
}
}
}