@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,091 lines (973 loc) • 38.6 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
sap.ui.define([
'../json/JSONModel', '../json/JSONPropertyBinding', '../json/JSONListBinding', 'sap/ui/base/ManagedObject', 'sap/ui/base/ManagedObjectObserver', '../Context', '../ChangeReason', "sap/base/util/uid", "sap/base/Log", "sap/base/util/isPlainObject", "sap/base/util/deepClone", "sap/base/util/deepEqual", "sap/ui/model/FilterType"
], function (JSONModel, JSONPropertyBinding, JSONListBinding, ManagedObject, ManagedObjectObserver, Context, ChangeReason, uid, Log, isPlainObject, deepClone, deepEqual, FilterType) {
"use strict";
var CUSTOMDATAKEY = "@custom",
ID_DELIMITER = "--";
/**
* Adapt the observation of child controls in order to be able to react when e.g. the value
* of a select inside a list changed. Currently the MOM is not updated then
*
* @param {object} caller the caller, here the managed object model
* @param {sap.ui.base.ManagedObject} oControl the control which shall be (un)observed
* @param {object} oAggregation the observed aggregation
* @param {boolean} bObserve <code>true</code> for observing and <code>false</code> for unobserving
*
* @private
*/
function _adaptDeepChildObservation(caller, oControl, oAggregation, bObserve) {
var aChildren = oAggregation.get(oControl) || [], oChild, bRecord;
if (aChildren && !Array.isArray(aChildren) && !oAggregation.multiple) {
aChildren = [aChildren];
}
for (var i = 0; i < aChildren.length; i++) {
oChild = aChildren[i];
if (!(oChild instanceof ManagedObject)) {
continue;
}
bRecord = true;
if (bObserve) {
caller._oObserver.observe(oChild, {
properties: true,
aggregations: true
});
} else {
caller._oObserver.unobserve(oChild, {
properties: true,
aggregations: true
});
}
var mAggregations = oChild.getMetadata().getAllAggregations();
for (var sKey in mAggregations) {
_adaptDeepChildObservation(caller, oChild, mAggregations[sKey], bObserve);
}
}
if (bRecord) {
var sKey = oControl.getId() + "/@" + oAggregation.name;
if (bObserve) {
if (!caller._mObservedCount.aggregations[sKey]) {
caller._mObservedCount.aggregations[sKey] = 0;
}
caller._mObservedCount.aggregations[sKey]++;
} else {
delete caller._mObservedCount.aggregations[sKey];
}
}
}
/**
* Traverses the recorded node stack of an object retrieval to the last used
* managed object to get the value/binding to use this for further treatment.
*
* @param {Object[]} aNodeStack
* @returns {array} An array containing:
* <ul>
* <li>the last managed object</li>
* <li>a map with the value of the last direct child and its path</li>
* <li>an array of the remaining parts in the traversal</li>
* <li>a string that represents the remaining path from the last managed object to the value</li>
* </ul>
*
*/
function _traverseToLastManagedObject(aNodeStack) {
//Determine last managed object via node stack of getProperty
var sMember, i = aNodeStack.length - 1, aParts = [];
while (!(aNodeStack[i].node instanceof ManagedObject)) {
if (sMember) {
aParts.splice(0, 0, sMember);
}
sMember = aNodeStack[i].path;
i--;
}
return [aNodeStack[i].node, aNodeStack[i + 1], aParts, sMember];
}
/**
* Serialize the object to a string to support change detection
*
* @param {Object} vObject
* @returns {string} A serialization of the object to a string
* @private
*/
function _stringify(vObject) {
var sData = "", sProp;
var type = typeof vObject;
if (vObject == null || (type != "object" && type != "function")) {
sData = vObject;
} else if (isPlainObject(vObject)) {
sData = JSON.stringify(vObject);
} else if (vObject instanceof ManagedObject) {
sData = vObject.getId();//add the id
for (sProp in vObject.mProperties) {
sData = sData + "$" + _stringify(vObject.mProperties[sProp]);
}
} else if (Array.isArray(vObject)) {
for (var i = 0; vObject.length; i++) {
sData = sData + "$" + _stringify(vObject);
}
} else {
Log.warning("Could not stringify object " + vObject);
sData = "$";
}
return sData;
}
var ManagedObjectModelAggregationBinding = JSONListBinding.extend("sap.ui.model.base.ManagedObjectModelAggregationBinding", {
constructor: function(oModel, sPath, oContext, aSorters, aFilters, mParameters) {
JSONListBinding.apply(this, arguments);
this._getOriginOfManagedObjectModelBinding();
},
/**
* Checks if this list binding might by affected by changes inside the given control.
* This means the control is inside the subtree spanned by the managed object whose
* aggregation or property represents this list binding.
*
* @param {sap.ui.base.ManagedObject} oControl The possible descendant
* @returns {boolean}
* <code>true</code> if the list binding might be affected by changes inside the given
* control, <code>false</code> otherwise
* @private
*/
_mightBeAffectedByChangesInside : function(oControl) {
while ( oControl ) {
if ( oControl.getParent() === this._oOriginMO ) {
// Note: No check for _sParentAggregation because of possible aggregation
// forwarding.
return true;
}
// Note: For aggregation forwarding the parent is hopefully contained in the
// origin managed object otherwise this binding is not refreshed correct
oControl = oControl.getParent();
}
return false;
},
/**
* Use the id of the ManagedObject instance as the unique key to identify
* the entry in the extended change detection. The default implementation
* in the parent class which uses JSON.stringify to serialize the instance
* doesn't fit here because none of the ManagedObject instance can be
* Serialized.
*
* @param {sap.ui.model.Context} oContext the binding context object
* @return {string} The identifier used for diff comparison
* @see sap.ui.model.ListBinding.prototype.getEntryData
*
*/
getEntryKey: function(oContext) {
// use the id of the ManagedObject instance as the identifier
// for the extended change detection
var oObject = oContext.getObject();
if (oObject instanceof ManagedObject) {
return oObject.getId();
}
return JSONListBinding.prototype.getEntryData.apply(this, arguments); // as ListBinding.prototype.getContextData falls back to getEntryData if getEntryKey is not defined
},
getEntryData: function(oContext) {
// use the id of the ManagedObject instance as the identifier
// for the extended change detection
var oObject = oContext.getObject();
if (oObject instanceof ManagedObject) {
return _stringify(oObject);
}
return JSONListBinding.prototype.getEntryData.apply(this, arguments);
},
/**
* In order to be able to page from an outer control an inner aggregation binding
* must be forced to page also
*
* @override
*/
_getContexts: function(iStartIndex, iLength) {
var iSizeLimit;
if (iStartIndex < 0) {
// set StartIndex to fetch last items
var iCurrentLength = this.getLength();
iStartIndex = iCurrentLength + iStartIndex;
if (iStartIndex < 0) {
iStartIndex = 0;
}
}
if (this._oAggregation) {
var oInnerListBinding = this._oOriginMO.getBinding(this._sMember);
//check if the binding is a list binding
if (oInnerListBinding) {
var oModel = oInnerListBinding.getModel();
iSizeLimit = oModel.iSizeLimit;
}
var oBindingInfo = this._oOriginMO.getBindingInfo(this._sMember);
//sanity check for paging exceeds model size limit
if (oBindingInfo && iStartIndex >= 0 && iLength &&
iSizeLimit && iLength > iSizeLimit) {
var bUpdate = false;
if (iStartIndex != oBindingInfo.startIndex) {
oBindingInfo.startIndex = iStartIndex;
bUpdate = true;
}
if (iLength != oBindingInfo.length) {
oBindingInfo.length = iLength;
bUpdate = true;
}
if (bUpdate) {
this._oAggregation.update(this._oOriginMO, "change");
}
}
}
return JSONListBinding.prototype._getContexts.apply(this, [iStartIndex, iLength]);
},
/**
* Determines the managed object that is responsible resp. triggering the list binding.
* There are two different cases: A binding of the form
* <ul>
* <li>{../table/items}, with the aggregation 'items' here the origin is the table</li>
* <li>{../field/conditions/0/value}, with the array property 'conditions' here the origin is the field</li>
* </ul>
*
* For identifying this object we need to traverse through the complete binding using the record functionality
* of the _getObject method.
*
* @private
*/
_getOriginOfManagedObjectModelBinding: function() {
if (!this._oOriginMO) {
var oMOM = this.oModel, aNodeStack = [];
oMOM._getObject(this.sPath, this.oContext, aNodeStack);
var aValueAndMO = _traverseToLastManagedObject(aNodeStack);
this._oOriginMO = aValueAndMO[0];
this._aPartsInJSON = aValueAndMO[2];
this._sMember = aValueAndMO[3];
this._oAggregation = this._oOriginMO.getMetadata().getAggregation(this._sMember);
}
},
getLength: function() {
// Note: Only use the original bindings length, if no filters are applied.
const aFilters = [...this.getFilters(FilterType.Application), ...this.getFilters(FilterType.Control)];
if (this._aPartsInJSON.length == 0 && aFilters.length == 0) {
//this is only valid if the binding points directly to the member of the Managed Object
var oInnerListBinding = this._oOriginMO.getBinding(this._sMember);
//check if the binding is a list binding
if (oInnerListBinding && oInnerListBinding.isA("sap.ui.model.ListBinding")) {
return oInnerListBinding.getLength();
}
}
return JSONListBinding.prototype.getLength.apply(this, arguments);
},
isLengthFinal: function() {
const aFilters = [...this.getFilters(FilterType.Application), ...this.getFilters(FilterType.Control)];
if (this._aPartsInJSON.length == 0 && aFilters.length == 0) {
//this is only valid if the binding points directly to the member of the Managed Object
var oInnerListBinding = this._oOriginMO.getBinding(this._sMember);
if (oInnerListBinding && oInnerListBinding.isA("sap.ui.model.ListBinding")) {
return oInnerListBinding.isLengthFinal();
}
}
return true;
}
});
var ManagedObjectModelPropertyBinding = JSONPropertyBinding.extend("sap.ui.model.base.ManagedObjectModelPropertyBinding");
/**
* The ManagedObjectModel class allows you to bind to properties and aggregations of managed objects.
*
* @class The ManagedObjectModel class can be used for data binding of properties and aggregations for managed objects.
*
* Provides model access to a given {@link sap.ui.base.ManagedObject}. Such access allows to bind to properties and aggregations of
* this object.
*
* <b>Note:</b> Use with care! Creating complex control trees based on ManagedObjectModel state may lead to performance issues.
*
* @param {sap.ui.base.ManagedObject} oObject the managed object models root object
* @param {object} [oData] an object for custom data
* @alias sap.ui.model.base.ManagedObjectModel
* @extends sap.ui.model.json.JSONModel
* @private
* @ui5-restricted sap.m, sap.ui.comp, sap.ui.core, sap.ui.fl, sap.ui.mdc
* @since 1.58
*/
var ManagedObjectModel = JSONModel.extend("sap.ui.model.base.ManagedObjectModel", /** @lends sap.ui.model.base.ManagedObjectModel.prototype */
{
constructor: function (oObject, oData) {
if (!oData && typeof oData != "object") {
oData = {};
}
oData[CUSTOMDATAKEY] = {};
this._oObject = oObject;
this._mObservedCount = {
properties: {},
aggregations: {}
};
this.mListBinding = {};
JSONModel.apply(this, [
oData
]);
this._oObserver = new ManagedObjectObserver(this.observerChanges.bind(this));
this.setSizeLimit(1000000); // SizeLimit should be set on Model the control is bound to, the ManagedObjectModel should not limit aggregations inside.
}
});
/**
* The purpose of the getProperty is to retrieve properties or aggregations from the root managed object.
*
* Depending on the requesting path the result could be one of the following:
* <ul>
* <li> The value of a property, for example, <code>oTextModel.getProperty("/text")</code>
* <li> A managed object as a result from a non-multiple aggregation, for example, <code>oColumn.getProperty("/label")</code>
* <li> A list of managed objects for multiple aggregations, for example, <code>oTable.getProperty("/columns")</code>.
* </ul>
*
* In addition a deep dive into the aggregations is also possible.
*
* To retrieve special settings or custom data from the managed object model, there is a special syntax for the selector parts:
* <ul>
* <li> A part starting with <code>@</code> can be used to access special settings like for example, the <code>id</code>
* <li> A path starting with <code>/@custom</code> is used to access the user-defined custom data.
* <li> A part containing <code>#</code> can be used to access controls by their <code>id</code>. This is currently used for the managed object model of the view.
* </ul>
* @param {string} sPath The path or name of a property of the root managed object
* @param {object} [oContext=null] The context with which the path can be resolved
* @returns {any} The value of the property, an array, or a managed object
* @public
* @name ManagedObjectModel.prototype.getProperty
*/
/**
* Convenience functionality to distinguish the goal of the access to the managed object.
*
* For example, it is more intuitive to say <code>oTableModel.getAggregation("/columns")</code>
* than <code>oTableModel.getProperty("/columns")</code> as the columns are an aggregation and not a property.
*
* @see ManagedObjectModel.prototype#getProperty
* @param {string} sPath The path or name of a property of the root managed object
* @param {object} [oContext=null] The context with which the path can be resolved
* @returns {any} The value of the property, an array, or a managed object
* @private
*/
ManagedObjectModel.prototype.getAggregation = JSONModel.prototype.getProperty;
/**
* Inserts the user-defined custom data into the model.
*
* @param {object} oData The data as JSON object to be set on the model
* @param {boolean} [bMerge=false] If set to <code>true</code>, the data is merged instead of replaced
* @public
*/
ManagedObjectModel.prototype.setData = function (oData, bMerge) {
var _oData = {};
_oData[CUSTOMDATAKEY] = oData;
JSONModel.prototype.setData.apply(this, [
_oData, bMerge
]);
};
/**
* Serializes the current custom JSON data of the model into a string.
*
* @return {string} sJSON The JSON data serialized as string
* @private
*/
ManagedObjectModel.prototype.getJSON = function () {
return JSON.stringify(this.oData[CUSTOMDATAKEY]);
};
/**
* Modifies the property of a child control for a given path and context.
*
* Example:
* <code>oTableModel.setProperty("/columns/0/visible", false)</code> hides the first column of the table
*
* <b>Note:</b> This is only restricted to properties not to aggregations. This means it is not possible to add an aggregation within the managed object model.
* @param {string} sPath The path to the property of the corresponding managed object, for example, <code>/text</code> for the text property of the root object
* @param {any} oValue The new value to be set for this property
* @param {sap.ui.model.Context} [oContext] The context used to set the property
* @param {boolean} [bAsyncUpdate] Whether to update bindings dependent on this property asynchronously
* @returns {boolean} <code>true</code> if the property was set, <code>false</code> otherwise
* @private
*/
ManagedObjectModel.prototype.setProperty = function (sPath, oValue, oContext, bAsyncUpdate) {
var sResolvedPath = this.resolve(sPath, oContext), iLastSlash, sObjectPath, sProperty;
// return if path / context is invalid
if (!sResolvedPath) {
return false;
}
// handling custom data and store it in the original this.oData object of the JSONModel
if (sResolvedPath.indexOf("/" + CUSTOMDATAKEY) === 0) {
return JSONModel.prototype.setProperty.apply(this, arguments);
}
iLastSlash = sResolvedPath.lastIndexOf("/");
// In case there is only one slash at the beginning, sObjectPath must contain this slash
sObjectPath = sResolvedPath.substring(0, iLastSlash || 1);
sProperty = sResolvedPath.substr(iLastSlash + 1);
var aNodeStack = [], oObject = this._getObject(sObjectPath, null, aNodeStack);
if (oObject) {
if (oObject instanceof ManagedObject) {
var oProperty = oObject.getMetadata().getManagedProperty(sProperty);
if (oProperty) {
if (!deepEqual(oProperty.get(oObject), oValue)) {
oProperty.set(oObject, oValue);
//update only property and sub properties
var fnFilter = function (oBinding) {
var sPath = this.resolve(oBinding.sPath, oBinding.oContext);
return sPath ? sPath.startsWith(sResolvedPath) : false;
}.bind(this);
this.checkUpdate(false, bAsyncUpdate, fnFilter);
return true;
}
} else {
Log.warning("The setProperty method only supports properties, the path " + sResolvedPath + " does not point to a property", null, "sap.ui.model.base.ManagedObjectModel");
}
} else if (oObject[sProperty] !== oValue) {
// get get an update of a property that was bound on a target
// control but which is only a data structure
var aValueAndMO = _traverseToLastManagedObject(aNodeStack);
//change the value of the property with structure
//to obtain a change we need to clone the property value
//as we will retrigger a setting of the complete property via API
var oMOMValue = deepClone(aValueAndMO[1].node), aParts = aValueAndMO[2];
var oPointer = oMOMValue;
for (var i = 0; i < aParts.length; i++) {
oPointer = oPointer[aParts[i]];
}
oPointer[sProperty] = oValue;
//determine the path of the property that is now to be changed
//aParts.join("/") + "/" + sProperty is the complete update path inside the propety
var sPathInsideProperty = "/" + sProperty;
if (aParts.length > 0) {
sPathInsideProperty = "/" + aParts.join("/") + sPathInsideProperty;
}
var iDelimiter = sResolvedPath.lastIndexOf(sPathInsideProperty);
var sPathUpToProperty = sResolvedPath.substr(0, iDelimiter);
//re-invoke now instead of:
// -> array case /objectArray/0/value/0 directly to /objectArray
// -> object case /objectValue/value directly to /objectValue
return this.setProperty(sPathUpToProperty, oMOMValue, oContext);
}
}
return false;
};
/**
* Adds the binding to the model.
*
* @param {sap.ui.model.Binding} oBinding The binding to be added
*/
ManagedObjectModel.prototype.addBinding = function (oBinding) {
JSONModel.prototype.addBinding.apply(this, arguments);
if (oBinding instanceof ManagedObjectModelAggregationBinding) {
var sAggregationName = oBinding.sPath.replace("/", "");
this.mListBinding[sAggregationName] = oBinding;
}
oBinding.checkUpdate(false);
};
ManagedObjectModel.prototype.removeBinding = function (oBinding) {
JSONModel.prototype.removeBinding.apply(this, arguments);
if (oBinding instanceof ManagedObjectModelAggregationBinding) {
var sAggregationName = oBinding.sPath.replace("/", "");
delete this.mListBinding[sAggregationName];
}
this._observeBeforeEvaluating(oBinding, false);
};
/**
* Overwrites the default property change event and enriches its parameters with the resolved path for convenience.
*
* @see sap.ui.model.Model.prototype.firePropertyChange
*
* @param {object} [oParameters] Parameters to pass along with the event
* @param {sap.ui.model.ChangeReason} [oParameters.reason] The reason of the property change
* @param {string} [oParameters.path] The path of the property
* @param {object} [oParameters.context] The context of the property
* @param {object} [oParameters.value] The value of the property
*
* @returns {this} Reference to <code>this</code> in order to allow method chaining
* @private
*/
ManagedObjectModel.prototype.firePropertyChange = function (oParameters) {
if (oParameters.reason === ChangeReason.Binding) {
oParameters.resolvedPath = this.resolve(oParameters.path, oParameters.context);
}
return JSONModel.prototype.firePropertyChange.call(this, oParameters);
};
/**
* @see sap.ui.model.Model.prototype.bindProperty
* @param {string} sPath The path pointing to the property that should be bound
* @param {sap.ui.model.Context} [oContext] The context object for this databinding
* @param {object} [mParameters] Additional model-specific parameters
*
* @return {sap.ui.model.PropertyBinding} The newly created binding
*/
ManagedObjectModel.prototype.bindAggregation = function (sPath, oContext, mParameters) {
return JSONModel.prototype.bindProperty.apply(this, arguments);
};
/**
* @see sap.ui.model.Model.prototype.bindProperty
* @param {string} sPath The path pointing to the property that should be bound
* @param {sap.ui.model.Context} [oContext] The context object for this databinding
* @param {object} [mParameters] Additional model-specific parameters
*
* @return {sap.ui.model.PropertyBinding} The newly created binding
*/
ManagedObjectModel.prototype.bindProperty = function (sPath, oContext, mParameters) {
var oBinding = new ManagedObjectModelPropertyBinding(this, sPath, oContext, mParameters);
return oBinding;
};
/**
* @see sap.ui.model.Model.prototype.bindList
* @param {string} sPath The path pointing to the list / array that should be bound
* @param {sap.ui.model.Context} [oContext] The context object for this databinding
* @param {sap.ui.model.Sorter[]|sap.ui.model.Sorter} [aSorters=[]] The sorters used initially; call {@link sap.ui.model.ListBinding#sort} to replace them
* @param {sap.ui.model.Filter[]|sap.ui.model.Filter} [aFilters=[]] The filters to be used initially with type {@link sap.ui.model.FilterType.Application};
* call {@link sap.ui.model.ListBinding#filter} to replace them
* @param {object} [mParameters] Additional model-specific parameters
* @throws {Error} If the {@link sap.ui.model.Filter.NONE} filter instance is contained in <code>aFilters</code> together with other filters
* @return {sap.ui.model.ListBinding} The newly created binding
*/
ManagedObjectModel.prototype.bindList = function (sPath, oContext, aSorters, aFilters, mParameters) {
var oBinding = new ManagedObjectModelAggregationBinding(this, sPath, oContext, aSorters, aFilters, mParameters);
return oBinding;
};
/**
* Returns the object for a given path and/or context, if exists.
*
* Private for now, might become public later.
* @param {string} sPath the path
* @param {string} [oContext] the context
* @returns {sap.ui.base.ManagedObject} The object for a given path and/or context, if exists, <code>null</code> otherwise.
* @private
*/
ManagedObjectModel.prototype.getManagedObject = function (sPath, oContext) {
if (sPath instanceof Context) {
oContext = sPath;
sPath = oContext.getPath();
}
var oObject = this.getProperty(sPath, oContext);
if (oObject instanceof ManagedObject) {
return oObject;
}
return null;
};
/**
* Returns the managed object that is the basis for this model.
*
* @returns {sap.ui.base.ManagedObject} The managed object that is basis for the model
* @private
*/
ManagedObjectModel.prototype.getRootObject = function () {
return this._oObject;
};
/*************************************************************************************************************************************************
* Private Helpers
************************************************************************************************************************************************/
/**
* Registers to the _change event for a property on an object
* Each property has its own handler registered to the _change.
* @param {sap.ui.base.ManagedObject} oObject the object for the property
* @param {object} oProperty the property object from the metadata of the object
* @private
*/
ManagedObjectModel.prototype._observePropertyChange = function (oObject, oProperty) {
if (!oObject || !oProperty) {
return;
}
var sKey = oObject.getId() + "/@" + oProperty.name;
// only register in case the property is not already observed
if (!this._oObserver.isObserved(oObject, {
properties: [
oProperty.name
]
})) {
this._oObserver.observe(oObject, {
properties: [
oProperty.name
]
});
this._mObservedCount.properties[sKey] = 1;
} else {
this._mObservedCount.properties[sKey]++;
}
};
/**
* Deregisters the handler from a property.
* Each property has its own handler to registered to the _change.
* @param {sap.ui.base.ManagedObject} oObject the object for the property
* @param {object} oProperty the property object from the metadata of the object
* @private
*/
ManagedObjectModel.prototype._unobservePropertyChange = function (oObject, oProperty) {
if (!oObject || !oProperty) {
return;
}
var sKey = oObject.getId() + "/@" + oProperty.name;
this._mObservedCount.properties[sKey]--;
if (this._mObservedCount.properties[sKey] == 0) {
this._oObserver.unobserve(oObject, {
properties: [
oProperty.name
]
});
delete this._mObservedCount.properties[sKey];
}
};
/**
* Registers the handler for an aggregation.
*
* @param {sap.ui.base.ManagedObject} oObject the object for the property
* @param {object} oAggregation the aggregation object from the metadata of the object
* @private
*/
ManagedObjectModel.prototype._observeAggregationChange = function (oObject, oAggregation) {
if (!oObject || !oAggregation) {
return;
}
var sKey = oObject.getId() + "/@" + oAggregation.name;
// only register in case the aggregation is not already observed
if (!this._oObserver.isObserved(oObject, {
aggregations: [
oAggregation.name
]
})) {
this._oObserver.observe(oObject, {
aggregations: [
oAggregation.name
]
});
this._mObservedCount.aggregations[sKey] = 1;
//also observe already present children
//note BCP 1870551736 where there where children present
//and the MOM did not realize changes on them
_adaptDeepChildObservation(this, oObject, oAggregation, true);
} else {
this._mObservedCount.aggregations[sKey]++;
}
};
/**
* Deregisters the handler from an aggregation.
*
* @param {sap.ui.base.ManagedObject} oObject the object for the property
* @param {object} oAggregation the aggregation object from the metadata of the object
* @private
*/
ManagedObjectModel.prototype._unobserveAggregationChange = function (oObject, oAggregation) {
if (!oObject || !oAggregation) {
return;
}
var sKey = oObject.getId() + "/@" + oAggregation.name;
this._mObservedCount.aggregations[sKey]--;
if (this._mObservedCount.aggregations[sKey] == 0) {
this._oObserver.unobserve(oObject, {
aggregations: [
oAggregation.name
]
});
delete this._mObservedCount.aggregations[sKey];
}
};
/**
* Convert the given local object id to a globally unique id
* by prefixing it with the object id of this model.
*
* @param {string} sId local Id of the object
* @return {string} prefixed id
* @private
*/
ManagedObjectModel.prototype._createId = function (sId) {
var oObject = this._oObject;
if (typeof oObject.createId === "function") {
return oObject.createId(sId);
}
if (!sId) {
return oObject.getId() + ID_DELIMITER + uid();
}
if (sId.indexOf(oObject.getId() + ID_DELIMITER) != 0) { // ID not already prefixed
return oObject.getId() + ID_DELIMITER + sId;
}
return sId;
};
/**
* Handles special paths that use the @ char and return the corresponding JSON structures
* @private
*/
ManagedObjectModel.prototype._getSpecialNode = function (oNode, sSpecial, oParentNode, sParentPart) {
if (oNode instanceof ManagedObject) {
if (sSpecial === "className") {
if (oNode.getMetadata) {
return oNode.getMetadata().getName();
} else {
return typeof oNode;
}
} else if (sSpecial === "id") {
return oNode.getId();
} else if (sSpecial === "metadataContexts") {
return oNode._oProviderData;
}
} else if (sSpecial === "binding" && oParentNode && sParentPart) {
return oParentNode.getBinding(sParentPart);
} else if (sSpecial === "bound" && oParentNode && sParentPart) {
return oParentNode.isBound(sParentPart);
} else if (sSpecial === "bindingInfo" && oParentNode && sParentPart) {
return oParentNode.getBindingInfo(sParentPart);
} else if (Array.isArray(oNode)) {
if (sSpecial === "length") {
return oNode.length;
} else if (sSpecial.indexOf("id=") === 0) {
var sId = sSpecial.substring(3), oFoundNode = null;
for (var i = 0; i < oNode.length; i++) {
if (oNode[i].getId() === this._createId(sId) || oNode[i].getId() === sId) { // TBD: Or should be avoided
oFoundNode = oNode[i];
break;
}
}
return oFoundNode;
}
}
return null;
};
/**
* Returns the corresponding object for the given path and/or context.
* Supported selectors -> see _getSpecialNode
* @private
*/
ManagedObjectModel.prototype._getObject = function (sPath, oContext, aNodeStack) {
var oNode = this._oObject, sResolvedPath = "", that = this;
if (aNodeStack) {
aNodeStack.push({path: "/", node: oNode}); // remember first node
}
this.aBindings.forEach(function (oBinding) {
if (!oBinding._bAttached) {
that._observeBeforeEvaluating(oBinding, true);
}
});
if (typeof sPath === "string" && sPath.indexOf("/") != 0 && !oContext) {
return null;
}
if (oContext instanceof ManagedObject) {
oNode = oContext;
sResolvedPath = sPath;
} else if (!oContext || oContext instanceof Context) {
sResolvedPath = this.resolve(sPath, oContext);
if (!sResolvedPath) {
return oNode;
}
// handling custom data stored in the original this.oData object of the JSONModel
if (sResolvedPath.indexOf("/" + CUSTOMDATAKEY) === 0) {
return JSONModel.prototype._getObject.apply(this, [
sPath, oContext
]);
}
} else {
oNode = oContext;
sResolvedPath = sPath;
}
if (!oNode) {
return null;
}
var aParts = sResolvedPath.split("/"), iIndex = 0;
if (!aParts[0]) {
// absolute path starting with slash
iIndex++;
}
var oParentNode = null, sParentPart = null, sPart;
while (oNode !== null && aParts[iIndex]) {
sPart = aParts[iIndex];
if (sPart == "id") {
//Managed Object Model should accept also /id as path to be used for templating
sPart = "@id";
}
if (sPart.indexOf("@") === 0) {
// special properties
oNode = this._getSpecialNode(oNode, sPart.substring(1), oParentNode, sParentPart);
} else if (oNode instanceof ManagedObject) {
var oNodeMetadata = oNode.getMetadata();
// look for the marker interface
if (oNodeMetadata.isInstanceOf("sap.ui.core.IDScope") && sPart.indexOf("#") === 0) {
oNode = oNode.byId(sPart.substring(1));
} else {
oParentNode = oNode;
sParentPart = sPart;
var oProperty = oNodeMetadata.getManagedProperty(sPart);
if (oProperty) {
oNode = oProperty.get(oNode);
} else {
var oAggregation = oNodeMetadata.getManagedAggregation(sPart);
if (oAggregation) {
oNode = oAggregation.get(oNode);
} else {
if (oNode && oNode[sPart] && typeof oNode[sPart] === "function") {
oNode = oNode[sPart]();
} else {
oNode = null;
}
}
}
}
} else if (Array.isArray(oNode) || isPlainObject(oNode)) {
oNode = oNode[sPart];
} else {
if (oNode && oNode[sPart] && typeof oNode[sPart] === "function") {
oNode = oNode[sPart]();
} else {
oNode = null;
}
}
if (aNodeStack) {
aNodeStack.push({path: sPart, node: oNode});
}
iIndex++;
}
return oNode;
};
/**
* @see sap.ui.model.Model.prototype.firePropertChange
*/
ManagedObjectModel.prototype.destroy = function () {
for (var n in this._mAggregationObjects) {
var o = this._mAggregationObjects[n];
// o.object._detachModifyAggregation(o.aggregationName, this._handleAggregationChange, this);
if (o.object.invalidate.fn) {
o.object.invalidate = o.object.invalidate.fn;
}
}
JSONModel.prototype.destroy.apply(this, arguments);
};
ManagedObjectModel.prototype._observeBeforeEvaluating = function (oBinding, bObserve) {
if (!oBinding.isResolved()) {
return;
}
var sPath = oBinding.getPath();
var oContext = oBinding.getContext(), oNode = this._oObject, sResolvedPath;
if (oContext instanceof ManagedObject) {
oNode = oContext;
sResolvedPath = sPath;
} else if (!oContext || oContext instanceof Context) {
sResolvedPath = this.resolve(sPath, oContext);
if (!sResolvedPath) {
return;
}
// handling custom data stored in the original this.oData object of the JSONModel
if (sResolvedPath.indexOf("/" + CUSTOMDATAKEY) === 0) {
return;
}
} else {
return;
}
var aParts = sResolvedPath.split("/");
if (!aParts[0]) {
// absolute path starting with slash
aParts.shift();
}
var sPart = aParts[0];
//handling of # for byId case of view
if (oNode.getMetadata().isInstanceOf("sap.ui.core.IDScope") && sPart.indexOf("#") === 0) {
oNode = oNode.byId(sPart.substring(1));
sPart = aParts[1];
}
if (oNode instanceof ManagedObject) {
var oNodeMetadata = oNode.getMetadata(), oProperty = oNodeMetadata.getManagedProperty(sPart);
if (oProperty) {
if (bObserve === true) {
this._observePropertyChange(oNode, oProperty);
} else if (bObserve === false) {
this._unobservePropertyChange(oNode, oProperty);
}
} else {
var oAggregation = oNodeMetadata.getAggregation(sPart) || oNodeMetadata.getAllPrivateAggregations()[sPart];
if (oAggregation) {
if (bObserve === true) {
this._observeAggregationChange(oNode, oAggregation);
} else if (bObserve === false) {
this._unobserveAggregationChange(oNode, oAggregation);
}
}
}
oBinding._bAttached = bObserve;
}
};
ManagedObjectModel.prototype.observerChanges = function (oChange) {
if (oChange.type == "aggregation") {
var mAggregations = {};
if (oChange.child instanceof ManagedObject) {
mAggregations = oChange.child.getMetadata().getAllAggregations();
}
if (oChange.mutation == "insert") {
// listen to inner changes only in case there is no alternative type used
if (oChange.child instanceof ManagedObject) {
this._oObserver.observe(oChange.child, {
properties: true,
aggregations: true
});
}
for (var sKey in mAggregations) {
_adaptDeepChildObservation(this, oChange.child, mAggregations[sKey], true);
}
if (this.mListBinding[oChange.name]) {
var oListBinding = this._oObject.getBinding(oChange.name);
var oAggregation = this._oObject.getAggregation(oChange.name);
//in case of paging wait till the last length is available, else take the length
if (oListBinding && oListBinding.getCurrentContexts().length != oAggregation.length) {
return;
}
}
} else {
// stop listening inner changes
if (oChange.child instanceof ManagedObject) {
this._oObserver.unobserve(oChange.child, {
properties: true,
aggregations: true
});
}
for (var sKey in mAggregations) {
_adaptDeepChildObservation(this, oChange.child, mAggregations[sKey], false);
}
}
} else if (oChange.type === "property") {
// list bindings can be affected
this.aBindings.forEach(function (oBinding) {
if (oBinding._mightBeAffectedByChangesInside
&& oBinding._mightBeAffectedByChangesInside(oChange.object)) {
oBinding.checkUpdate(true/*bForceUpdate*/);
}
});
}
var fnFilter;
if (oChange.object === this._oObject) { // if root object, only bindings starting with the changed property/aggregation are from interest
fnFilter = function (oBinding) { // update only bindings that might be affected
var sPath = this.resolve(oBinding.sPath, oBinding.oContext);
return sPath ? sPath.startsWith("/" + oChange.name) : true;
}.bind(this);
}
this.checkUpdate(false, false, fnFilter);
};
/**
* Private method iterating the registered bindings of this model instance and initiating their check for update
* @param {boolean} [bForceUpdate=false] The parameter <code>bForceUpdate</code> for the <code>checkUpdate</code> call on the bindings
* @param {boolean} [bAsync=false] Whether this function is called in a new task via <code>setTimeout</code>
* @param {function} [fnFilter] an optional test function to filter the binding
* @returns {number|undefined} The number of bindings which were checked synchronously for updates; 0 if <code>bAsync</code> is set.
* Subclasses overwriting this method may also return <code>undefined</code>.
* @protected
*/
ManagedObjectModel.prototype.checkUpdate = function (bForceUpdate, bAsync, fnFilter) {
if (bAsync) {
this.bForceUpdate = this.bForceUpdate || bForceUpdate;
if (!this.sUpdateTimer) {
this.fnFilter = this.fnFilter || fnFilter;
this.sUpdateTimer = setTimeout(function () {
this.checkUpdate(this.bForceUpdate, false, this.fnFilter);
}.bind(this), 0);
} else if (this.fnFilter && this.fnFilter !== fnFilter) {
this.fnFilter = undefined; // if different filter set use no filter
}
return 0;
}
bForceUpdate = this.bForceUpdate || bForceUpdate;
if (this.sUpdateTimer) {
clearTimeout(this.sUpdateTimer);
this.sUpdateTimer = null;
this.bForceUpdate = undefined;
fnFilter = this.fnFilter === fnFilter ? fnFilter : undefined; // if different filter or no filter set -> use no filter
this.fnFilter = undefined;
}
var aBindings = this.getBindings();
var iUpdatedBindings = 0;
aBindings.forEach(function (oBinding) {
if (!fnFilter || fnFilter(oBinding)) {
oBinding.checkUpdate(bForceUpdate);
iUpdatedBindings++;
}
});
return iUpdatedBindings;
};
return ManagedObjectModel;
});