UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

1,091 lines (973 loc) 38.6 kB
/*! * 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; });