@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,280 lines (1,213 loc) • 81 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2009-2023 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
//Provides class sap.ui.model.odata.v4.Context
sap.ui.define([
"./lib/_Helper",
"sap/base/Log",
"sap/ui/base/SyncPromise",
"sap/ui/model/Context"
], function (_Helper, Log, SyncPromise, BaseContext) {
"use strict";
var sClassName = "sap.ui.model.odata.v4.Context",
// generation counter to distinguish old from new
iGenerationCounter = 0,
oModule,
// index of virtual context used for auto-$expand/$select
iVIRTUAL = -9007199254740991/*Number.MIN_SAFE_INTEGER*/,
/**
* @alias sap.ui.model.odata.v4.Context
* @author SAP SE
* @class Implementation of an OData V4 model's context.
*
* The context is a pointer to model data as returned by a query from an
* {@link sap.ui.model.odata.v4.ODataContextBinding} or an
* {@link sap.ui.model.odata.v4.ODataListBinding}. Contexts are always and only
* created by such bindings. A context for a context binding points to the complete query
* result. A context for a list binding points to one specific entry in the binding's
* collection. A property binding does not have a context, you can access its value via
* {@link sap.ui.model.odata.v4.ODataPropertyBinding#getValue}.
*
* Applications can access model data only via a context, either synchronously with the
* risk that the values are not available yet ({@link #getProperty} and
* {@link #getObject}) or asynchronously ({@link #requestProperty} and
* {@link #requestObject}).
*
* Context instances are immutable except for their indexes.
* @extends sap.ui.model.Context
* @hideconstructor
* @public
* @since 1.39.0
* @version 1.111.5
*/
Context = BaseContext.extend("sap.ui.model.odata.v4.Context", {
constructor : constructor
});
//*********************************************************************************************
// Context
//*********************************************************************************************
/**
* Do <strong>NOT</strong> call this private constructor. In the OData V4 model you cannot
* create contexts at will: retrieve them from a binding or a view element instead.
*
* @param {sap.ui.model.odata.v4.ODataModel} oModel
* The model
* @param {sap.ui.model.odata.v4.ODataContextBinding|sap.ui.model.odata.v4.ODataListBinding} oBinding
* A binding that belongs to the model
* @param {string} sPath
* An absolute path without trailing slash
* @param {number} [iIndex]
* Index of item (within the collection addressed by <code>sPath</code>) represented
* by this context; used by list bindings, not context bindings
* @param {sap.ui.base.SyncPromise} [oCreatePromise]
* A promise which is resolved with the created entity when the POST request has been
* successfully sent and the entity has been marked as non-transient; used as base for
* {@link #created}
* @param {number} [iGeneration=0]
* The unique number for this context's generation, which can be retrieved via
* {@link #getGeneration}
* @param {boolean} [bInactive]
* Whether this context is inactive and will only be sent to the server after the first
* property update
* @throws {Error}
* If an invalid path is given
*/
function constructor(oModel, oBinding, sPath, iIndex, oCreatePromise, iGeneration,
bInactive) {
if (sPath[0] !== "/") {
throw new Error("Not an absolute path: " + sPath);
}
if (sPath.endsWith("/")) {
throw new Error("Unsupported trailing slash: " + sPath);
}
BaseContext.call(this, oModel, sPath);
this.oBinding = oBinding;
this.oCreatedPromise = oCreatePromise
// ensure to return a promise that is resolved w/o data
&& Promise.resolve(oCreatePromise).then(function () {});
this.oSyncCreatePromise = oCreatePromise;
// a promise waiting for the deletion, also used as indicator for #isDeleted
this.oDeletePromise = null;
this.iGeneration = iGeneration || 0;
this.bInactive = bInactive || undefined; // be in sync with the annotation
this.iIndex = iIndex;
this.bKeepAlive = false;
this.bSelected = false;
this.fnOnBeforeDestroy = undefined;
}
/**
* Adjusts this context's path by replacing the given transient predicate with the given
* predicate. Recursively adjusts all child bindings.
*
* @param {string} sTransientPredicate
* The transient predicate to be replaced
* @param {string} sPredicate
* The new predicate
* @param {function} [fnPathChanged]
* A function called with the old and the new path
*
* @private
*/
Context.prototype.adjustPredicate = function (sTransientPredicate, sPredicate, fnPathChanged) {
var sTransientPath = this.sPath;
this.sPath = sTransientPath.replace(sTransientPredicate, sPredicate);
if (fnPathChanged) {
fnPathChanged(sTransientPath, this.sPath);
}
this.oModel.getDependentBindings(this).forEach(function (oDependentBinding) {
oDependentBinding.adjustPredicate(sTransientPredicate, sPredicate);
});
};
/**
* Updates all dependent bindings of this context.
*
* @private
*/
Context.prototype.checkUpdate = function () {
this.oModel.getDependentBindings(this).forEach(function (oDependentBinding) {
oDependentBinding.checkUpdate();
});
};
/**
* Updates all dependent bindings of this context.
*
* @returns {sap.ui.base.SyncPromise}
* A promise resolving without a defined result when the update is finished
* @private
*/
Context.prototype.checkUpdateInternal = function () {
return SyncPromise.all(
this.oModel.getDependentBindings(this).map(function (oDependentBinding) {
return oDependentBinding.checkUpdateInternal();
})
);
};
/**
* Collapses the group node that this context points to.
*
* @throws {Error}
* If the context points to a node that is not expandable, already collapsed, or
* is a grand total.
*
* @public
* @see #expand
* @see #isExpanded
* @since 1.83.0
*/
Context.prototype.collapse = function () {
switch (this.getProperty("@$ui5.node.level") === 0 ? undefined : this.isExpanded()) {
case true:
this.oBinding.collapse(this);
break;
case false:
throw new Error("Already collapsed: " + this);
default:
throw new Error("Not expandable: " + this);
}
};
/**
* Returns a promise that is resolved without data when the entity represented by this context
* has been created in the back end and all selected properties of this entity are available.
* Expanded navigation properties are only available if the context's binding is refreshable.
* {@link sap.ui.model.odata.v4.ODataContextBinding#refresh} and
* {@link sap.ui.model.odata.v4.ODataListBinding#refresh} describe which bindings are
* refreshable.
*
* As long as the promise is not yet resolved or rejected, the entity represented by this
* context is transient.
*
* Once the promise is resolved, {@link #getPath} returns a path including the key predicate
* of the new entity. This requires that all key properties are available.
*
* @returns {Promise<void>|undefined}
* A promise that is resolved without data when the entity represented by this context has
* been created in the back end. It is rejected with an <code>Error</code> instance where
* <code>oError.canceled === true</code> if the transient entity is deleted before it is
* created in the back end, for example via {@link sap.ui.model.odata.v4.Context#delete},
* {@link sap.ui.model.odata.v4.ODataListBinding#resetChanges} or
* {@link sap.ui.model.odata.v4.ODataModel#resetChanges}. It is rejected with an
* <code>Error</code> instance without <code>oError.canceled</code> if loading of $metadata
* fails. Returns <code>undefined</code> if the context has not been created using
* {@link sap.ui.model.odata.v4.ODataListBinding#create}.
*
* @public
* @since 1.43.0
*/
Context.prototype.created = function () {
return this.oCreatedPromise;
};
/**
* Deletes the OData entity this context points to. The context is removed from the binding
* immediately, even if {@link sap.ui.model.odata.v4.SubmitMode.API} is used, and the request is
* only sent later when {@link sap.ui.model.odata.v4.ODataModel#submitBatch} is called. As soon
* as the context is deleted on the client, {@link #isDeleted} returns <code>true</code> and the
* context must not be used anymore (except for status APIs like {@link #isDeleted},
* {@link #isKeepAlive}, {@link #hasPendingChanges}, {@link #resetChanges}), especially not as a
* binding context.
*
* Since 1.105 such a pending deletion is a pending change. It causes
* <code>hasPendingChanges</code> to return <code>true</code> for the context, the binding
* containing it, and the model. The <code>resetChanges</code> method called on the context
* (since 1.109.0), the binding, or the model cancels the deletion and restores the context.
*
* If the DELETE request succeeds, the context is destroyed and must not be used anymore. If it
* fails or is canceled, the context is restored, reinserted into the list, and fully functional
* again.
*
* If the deleted context is used as binding context of a control or view, the application is
* advised to unbind it via
* <code>{@link sap.ui.base.ManagedObject#setBindingContext setBindingContext(null)}</code>
* before calling <code>delete</code>, and to possibly rebind it after reset or failure. The
* model itself ensures that all bindings depending on this context become unresolved, but no
* attempt is made to restore these bindings in case of reset or failure.
*
* @param {string} [sGroupId]
* The group ID to be used for the DELETE request; if not specified, the update group ID for
* the context's binding is used, see {@link #getUpdateGroupId}. Since 1.81, if this context
* is transient (see {@link #isTransient}), no group ID needs to be specified. Since 1.98.0,
* you can use <code>null</code> to prevent the DELETE request in case of a kept-alive context
* that is not in the collection and of which you know that it does not exist on the server
* anymore (for example, a draft after activation). Since 1.108.0 the usage of a group ID with
* {@link sap.ui.model.odata.v4.SubmitMode.API} is possible.
* @param {boolean} [bDoNotRequestCount]
* Whether not to request the new count from the server; useful in case of
* {@link #replaceWith} where it is known that the count remains unchanged (since 1.97.0).
* Since 1.98.0, this is implied if a <code>null</code> group ID is used.
* @returns {Promise}
* A promise which is resolved without a result in case of success, or rejected with an
* instance of <code>Error</code> in case of failure, for example if:
* <ul>
* <li> the given context does not point to an entity,
* <li> the deletion on the server fails,
* <li> the deletion is canceled via <code>resetChanges</code> (in this case the error
* instance has the property <code>canceled</code> with value <code>true</code>).
* </ul>
* The error instance has the property <code>isConcurrentModification</code> with value
* <code>true</code> in case a concurrent modification (e.g. by another user) of the entity
* between loading and deletion has been detected; this should be shown to the user who needs
* to decide whether to try deletion again. If the entity does not exist, we assume it has
* already been deleted by someone else and report success.
* @throws {Error} If
* <ul>
* <li> the given group ID is invalid,
* <li> this context's root binding is suspended,
* <li> a <code>null</code> group ID is used with a context which is not
* {@link #isKeepAlive kept alive},
* <li> the context is already being deleted,
* <li> the context's binding is a list binding with data aggregation,
* </ul>
*
* @function
* @public
* @see #hasPendingChanges
* @see #resetChanges
* @see sap.ui.model.odata.v4.ODataContextBinding#hasPendingChanges
* @see sap.ui.model.odata.v4.ODataListBinding#hasPendingChanges
* @see sap.ui.model.odata.v4.ODataModel#hasPendingChanges
* @see sap.ui.model.odata.v4.ODataContextBinding#resetChanges
* @see sap.ui.model.odata.v4.ODataListBinding#resetChanges
* @see sap.ui.model.odata.v4.ODataModel#resetChanges
* @since 1.41.0
*/
Context.prototype.delete = function (sGroupId, bDoNotRequestCount/*, bRejectIfNotFound*/) {
var oEditUrlPromise,
oGroupLock = null,
that = this;
if (this.isDeleted()) {
throw new Error("Must not delete twice: " + this);
}
if (this.oBinding.mParameters.$$aggregation) {
throw new Error("Cannot delete " + this + " when using data aggregation");
}
this.oBinding.checkSuspended();
if (this.isTransient()) {
sGroupId = null;
} else if (sGroupId === null) {
if (!(this.bKeepAlive && this.iIndex === undefined)) {
throw new Error("Cannot delete " + this);
}
}
if (sGroupId === null) {
oEditUrlPromise = SyncPromise.resolve();
bDoNotRequestCount = true;
} else {
_Helper.checkGroupId(sGroupId);
oEditUrlPromise = this.fetchCanonicalPath().then(function (sCanonicalPath) {
return sCanonicalPath.slice(1);
});
oGroupLock = this.oBinding.lockGroup(sGroupId, true, true);
}
return Promise.resolve(
oEditUrlPromise.then(function (sEditUrl) {
return that.oBinding.delete(oGroupLock, sEditUrl, that, /*oETagEntity*/null,
bDoNotRequestCount, function () {
that.oDeletePromise = null;
}
);
}).catch(function (oError) {
if (oGroupLock) {
oGroupLock.unlock(true);
}
throw oError;
})
);
};
/**
* Destroys this context, that is, it removes this context from all dependent bindings and drops
* references to binding and model, so that the context cannot be used anymore; it keeps path
* and index for debugging purposes.
*
* <b>BEWARE:</b> Do not call this function! The lifetime of an OData V4 context is completely
* controlled by its binding.
*
* @public
* @see sap.ui.base.Object#destroy
* @since 1.41.0
*/
// @override sap.ui.base.Object#destroy
Context.prototype.destroy = function () {
var fnOnBeforeDestroy = this.fnOnBeforeDestroy;
if (fnOnBeforeDestroy) {
// avoid second call through a destroy inside the callback
this.fnOnBeforeDestroy = undefined;
fnOnBeforeDestroy();
}
this.oModel.getDependentBindings(this).forEach(function (oDependentBinding) {
oDependentBinding.setContext(undefined);
});
this.oBinding = undefined;
this.oCreatedPromise = undefined;
// keep oDeletePromise so that isDeleted does not unexpectedly become false
this.oSyncCreatePromise = undefined;
this.bInactive = undefined;
this.bKeepAlive = undefined;
this.bSelected = false;
// When removing oModel, ManagedObject#getBindingContext does not return the destroyed
// context although the control still refers to it
this.oModel = undefined;
BaseContext.prototype.destroy.call(this);
};
/**
* Deletes the OData entity this context points to.
*
* @param {sap.ui.model.odata.v4.lib._GroupLock} [oGroupLock]
* A lock for the group ID to be used for the DELETE request; w/o a lock, no DELETE is sent.
* For a transient entity, the lock is ignored (use NULL)!
* @param {string} [sEditUrl]
* The entity's edit URL to be used for the DELETE request; only required with a lock
* @param {string} sPath
* The path of the entity relative to this binding
* @param {object} [oETagEntity]
* An entity with the ETag of the binding for which the deletion was requested. This is
* provided if the deletion is delegated from a context binding with empty path to a list
* binding. W/o a lock, this is ignored.
* @param {sap.ui.model.odata.v4.ODataParentBinding} oBinding
* The binding to perform the deletion at
* @param {function} fnCallback
* A function which is called immediately when an entity has been deleted from the cache, or
* when it was re-inserted; the index of the entity and an offset (-1 for deletion, 1 for
* re-insertion) are passed as parameter
* @returns {sap.ui.base.SyncPromise}
* A promise which is resolved without a result in case of success, or rejected with an
* instance of <code>Error</code> in case of failure
*
* @private
* @see sap.ui.model.odata.v4.Context#delete
*/
Context.prototype.doDelete = function (oGroupLock, sEditUrl, sPath, oETagEntity, oBinding,
fnCallback) {
var oModel = this.oModel,
that = this;
this.oDeletePromise = oBinding.deleteFromCache(
oGroupLock, sEditUrl, sPath, oETagEntity, fnCallback
).then(function () {
var sResourcePathPrefix = that.sPath.slice(1);
// Messages have been updated via _Cache#_delete; "that" is already destroyed; remove
// all dependent caches in all bindings
oModel.getAllBindings().forEach(function (oBinding) {
oBinding.removeCachesAndMessages(sResourcePathPrefix, true);
});
}).catch(function (oError) {
oModel.reportError("Failed to delete " + that.getPath(), sClassName, oError);
that.checkUpdate();
throw oError;
});
if (oGroupLock && this.oModel.isApiGroup(oGroupLock.getGroupId())) {
oModel.getDependentBindings(this).forEach(function (oDependentBinding) {
oDependentBinding.setContext(undefined);
});
}
return this.oDeletePromise;
};
/**
* Sets the new current value and updates the cache.
*
* @param {string} sPath
* A path relative to this context
* @param {any} vValue
* The new value which must be primitive
* @param {sap.ui.model.odata.v4.lib._GroupLock} [oGroupLock]
* A lock for the group ID to be used for the PATCH request; without a lock, no PATCH is sent
* @param {boolean} [bSkipRetry]
* Whether to skip retries of failed PATCH requests and instead fail accordingly, but still
* fire "patchSent" and "patchCompleted" events
* @param {boolean} [bUpdating]
* Whether the given property will not be overwritten by a creation POST(+GET) response
* @returns {sap.ui.base.SyncPromise}
* A promise which is resolved without a result in case of success, or rejected with an
* instance of <code>Error</code> in case of failure, for example if the annotation belongs to
* the read-only namespace "@$ui5.*"
* @throws {Error} If the context is deleted
*
* @private
*/
Context.prototype.doSetProperty = function (sPath, vValue, oGroupLock, bSkipRetry, bUpdating) {
var oModel = this.oModel,
oMetaModel = oModel.getMetaModel(),
oPromise,
oValue,
that = this;
if (this.isDeleted()) {
if (oGroupLock) {
oGroupLock.unlock();
}
throw new Error("Must not modify a deleted entity: " + this);
}
if (oGroupLock && this.isTransient() && !this.isInactive()) {
oValue = this.getValue();
oPromise = oValue && _Helper.getPrivateAnnotation(oValue, "transient");
if (oPromise instanceof Promise) {
oGroupLock.unlock();
oGroupLock = oGroupLock.getUnlockedCopy();
this.doSetProperty(sPath, vValue, null, true, true) // early UI update
.catch(this.oModel.getReporter());
return SyncPromise.resolve(oPromise).then(function (bSuccess) {
// in case of success, wait until creation is completed because context path's
// key predicate is adjusted
return bSuccess && that.created();
}).then(function () {
return that.doSetProperty(sPath, vValue, oGroupLock, bSkipRetry);
});
}
}
if (this.oModel.bAutoExpandSelect) {
sPath = oMetaModel.getReducedPath(
this.oModel.resolve(sPath, this),
this.oBinding.getBaseForPathReduction());
}
return this.withCache(function (oCache, sCachePath, oBinding) {
return oBinding.doSetProperty(sCachePath, vValue, oGroupLock)
|| oMetaModel.fetchUpdateData(sPath, that, !oGroupLock).then(function (oResult) {
var sEntityPath = _Helper.getRelativePath(oResult.entityPath,
oBinding.oReturnValueContext
? oBinding.oReturnValueContext.getPath()
: oBinding.getResolvedPath()),
// If a PATCH is merged into a POST request, firePatchSent is not called,
// thus don't call firePatchCompleted
bFirePatchCompleted = false;
/*
* Error callback to report the given error and fire "patchCompleted"
* accordingly.
*
* @param {Error} oError
*/
function errorCallback(oError) {
oModel.reportError("Failed to update path " + oModel.resolve(sPath, that),
sClassName, oError);
firePatchCompleted(false);
}
/*
* Fire "patchCompleted" according to the given success flag, if needed.
*
* @param {boolean} bSuccess
*/
function firePatchCompleted(bSuccess) {
if (bFirePatchCompleted) {
oBinding.firePatchCompleted(bSuccess);
bFirePatchCompleted = false;
}
}
/*
* Fire "patchSent" and remember to later fire "patchCompleted".
*/
function patchSent() {
bFirePatchCompleted = true;
oBinding.firePatchSent();
}
if (!oGroupLock) {
return oCache.setProperty(oResult.propertyPath, vValue, sEntityPath,
bUpdating);
}
if (that.isInactive()) {
// early cache update so that the new value is properly available on the
// event listener
// runs synchronously - setProperty calls fetchValue with $cached
oCache.setProperty(oResult.propertyPath, vValue, sEntityPath, bUpdating)
.catch(that.oModel.getReporter());
if (oBinding.fireCreateActivate(that)) {
that.bInactive = false;
} else {
that.bInactive = 1;
}
}
// if request is canceled fnPatchSent and fnErrorCallback are not called and
// returned Promise is rejected -> no patch events
return oCache.update(oGroupLock, oResult.propertyPath, vValue,
bSkipRetry ? undefined : errorCallback, oResult.editUrl, sEntityPath,
oMetaModel.getUnitOrCurrencyPath(that.oModel.resolve(sPath, that)),
oBinding.isPatchWithoutSideEffects(), patchSent,
that.isKeepAlive.bind(that), that.bInactive
).then(function () {
firePatchCompleted(true);
}, function (oError) {
firePatchCompleted(false);
throw oError;
});
});
}, sPath, /*bSync*/false, /*bWithOrWithoutCache*/true);
};
/**
* Expands the group node that this context points to.
*
* @throws {Error}
* If the context points to a node that is not expandable or already expanded
*
* @public
* @see #collapse
* @see #isExpanded
* @since 1.77.0
*/
Context.prototype.expand = function () {
switch (this.isExpanded()) {
case false:
this.oBinding.expand(this).catch(this.oModel.getReporter());
break;
case true:
throw new Error("Already expanded: " + this);
default:
throw new Error("Not expandable: " + this);
}
};
/**
* Returns a promise for the "canonical path" of the entity for this context.
*
* @returns {sap.ui.base.SyncPromise}
* A promise which is resolved with the canonical path (e.g. "/SalesOrderList('0500000000')")
* in case of success, or rejected with an instance of <code>Error</code> in case of failure,
* e.g. if the given context does not point to an entity
*
* @private
*/
Context.prototype.fetchCanonicalPath = function () {
return this.oModel.getMetaModel().fetchCanonicalPath(this);
};
/**
* Fetches and formats the primitive value at the given path.
*
* @param {string} sPath The requested path, absolute or relative to this context
* @param {boolean} [bExternalFormat]
* If <code>true</code>, the value is returned in external format using a UI5 type for the
* given property path that formats corresponding to the property's EDM type and constraints.
* @param {boolean} [bCached]
* Whether to return cached values only and not trigger a request
* @returns {sap.ui.base.SyncPromise} a promise on the formatted value
*
* @private
*/
Context.prototype.fetchPrimitiveValue = function (sPath, bExternalFormat, bCached) {
var oError,
aPromises = [this.fetchValue(sPath, null, bCached)],
sResolvedPath = this.oModel.resolve(sPath, this);
if (bExternalFormat) {
aPromises.push(
this.oModel.getMetaModel().fetchUI5Type(sResolvedPath));
}
return SyncPromise.all(aPromises).then(function (aResults) {
var oType = aResults[1],
vValue = aResults[0];
if (vValue && typeof vValue === "object") {
oError = new Error("Accessed value is not primitive: " + sResolvedPath);
oError.isNotPrimitive = true;
throw oError;
}
return bExternalFormat ? oType.formatValue(vValue, "string") : vValue;
});
};
/**
* Delegates to the <code>fetchValue</code> method of this context's binding which requests
* the value for the given path. A relative path is assumed to be relative to this context and
* is reduced before accessing the cache if the model uses autoExpandSelect.
*
* @param {string} [sPath]
* A path (absolute or relative to this context)
* @param {sap.ui.model.odata.v4.ODataPropertyBinding} [oListener]
* A property binding which registers itself as listener at the cache
* @param {boolean} [bCached]
* Whether to return cached values only and not trigger a request
* @returns {sap.ui.base.SyncPromise}
* A promise on the outcome of the binding's <code>fetchValue</code> call; it is rejected
* in case cached values are asked for, but not found
*
* @private
*/
Context.prototype.fetchValue = function (sPath, oListener, bCached) {
var oBinding = this.oBinding;
if (this.iIndex === iVIRTUAL) {
return SyncPromise.resolve(); // no cache access for virtual contexts
}
if (oBinding.getHeaderContext && oBinding.getHeaderContext() === this) {
if (sPath && sPath.startsWith(this.sPath)) {
sPath = sPath.slice(this.sPath.length + 1);
}
if (!sPath) {
return oBinding.fetchValue(this.sPath, oListener, bCached)
.then(function (aElements) {
return {$count : aElements.$count};
});
} else if (sPath !== "$count") {
throw new Error("Invalid header path: " + sPath);
}
}
if (!sPath || sPath[0] !== "/") {
// Create an absolute path based on the context's path and reduce it. This is only
// necessary for data access via Context APIs, bindings already use absolute paths.
sPath = this.oModel.resolve(sPath, this);
if (this.oModel.bAutoExpandSelect) {
sPath = this.oModel.getMetaModel()
.getReducedPath(sPath, this.oBinding.getBaseForPathReduction());
}
}
return this.oBinding.fetchValue(sPath, oListener, bCached);
};
/**
* Returns the binding this context belongs to.
*
* @returns {sap.ui.model.odata.v4.ODataContextBinding|sap.ui.model.odata.v4.ODataListBinding}
* The context's binding
*
* @public
* @since 1.39.0
*/
Context.prototype.getBinding = function () {
return this.oBinding;
};
/**
* Returns the "canonical path" of the entity for this context. According to <a href=
* "https://docs.oasis-open.org/odata/odata/v4.0/odata-v4.0-part2-url-conventions.html#canonical-urlurl4.1.1"
* >"4.3.1 Canonical URL"</a> of the specification "OData Version 4.0 Part 2: URL Conventions",
* this is the "name of the entity set associated with the entity followed by the key predicate
* identifying the entity within the collection". Use the canonical path in
* {@link sap.ui.core.Element#bindElement} to create an element binding.
*
* Note: For a transient context (see {@link #isTransient}) a wrong path is returned unless all
* key properties are available within the initial data.
*
* @returns {string}
* The canonical path (e.g. "/SalesOrderList('0500000000')")
* @throws {Error}
* If the canonical path cannot be determined yet or in case of failure, e.g. if the given
* context does not point to an entity
*
* @function
* @public
* @since 1.39.0
*/
Context.prototype.getCanonicalPath = _Helper.createGetMethod("fetchCanonicalPath", true);
/**
* Returns the unique number of this context's generation, or <code>0</code> if it does not
* belong to any specific generation. This number can be inherited from a parent binding.
*
* @param {boolean} [bOnlyLocal]
* Whether the local generation w/o inheritance is returned
* @returns {number}
* The unique number of this context's generation, or <code>0</code>
*
* @private
* @see sap.ui.model.odata.v4.Context.createNewContext
* @see #setNewGeneration
*/
Context.prototype.getGeneration = function (bOnlyLocal) {
if (this.iGeneration || bOnlyLocal) {
return this.iGeneration;
}
return this.oBinding.getGeneration();
};
/**
* Returns the group ID of the context's binding that is used for read requests. See
* {@link sap.ui.model.odata.v4.ODataListBinding#getGroupId} and
* {@link sap.ui.model.odata.v4.ODataContextBinding#getGroupId}.
*
* @returns {string}
* The group ID
*
* @public
* @since 1.81.0
*/
Context.prototype.getGroupId = function () {
return this.oBinding.getGroupId();
};
/**
* Returns the context's index within the binding's collection. The return value changes when a
* new entity is added via {@link sap.ui.model.odata.v4.ODataListBinding#create} without
* <code>bAtEnd</code>, and when a context representing a created entity is deleted again.
*
* @returns {number|undefined}
* The context's index within the binding's collection. It is <code>undefined</code> if
* <ul>
* <li> it does not belong to a list binding,
* <li> it is {@link #isKeepAlive kept alive}, but not in the collection currently.
* </ul>
*
* @public
* @since 1.39.0
*/
Context.prototype.getIndex = function () {
if (this.iIndex === undefined) {
return undefined;
}
if (this.oBinding.isFirstCreateAtEnd()) {
if (this.iIndex < 0) { // this does not include undefined for a kept-alive context
return this.oBinding.bLengthFinal
? this.oBinding.iMaxLength - this.iIndex - 1
: -this.iIndex - 1;
}
return this.iIndex;
}
return this.getModelIndex();
};
/**
* Returns the model index, which is the context's index in the binding's collection. This
* differs from the view index if entities have been created at the end. Internally such
* contexts still are kept at the start of the collection. For this reason the return value
* changes if a new entity is added via {@link sap.ui.model.odata.v4.ODataListBinding#create}
* or deleted again.
*
* @returns {number}
* The context's index within the binding's collection. It is <code>undefined</code> if
* <ul>
* <li> it does not belong to a list binding,
* <li> it is {@link #isKeepAlive kept alive}, but not in the collection currently.
* </ul>
*
* @private
*/
Context.prototype.getModelIndex = function () {
if (this.iIndex !== undefined && this.oBinding.iCreatedContexts) {
return this.iIndex + this.oBinding.iCreatedContexts;
}
return this.iIndex;
};
/**
* Returns the value for the given path relative to this context. The function allows access to
* the complete data the context points to (if <code>sPath</code> is "") or any part thereof.
* The data is a JSON structure as described in <a href=
* "https://docs.oasis-open.org/odata/odata-json-format/v4.0/odata-json-format-v4.0.html"
* >"OData JSON Format Version 4.0"</a>.
* Note that the function clones the result. Modify values via
* {@link sap.ui.model.odata.v4.ODataPropertyBinding#setValue}.
*
* Returns <code>undefined</code> if the data is not (yet) available; no request is triggered.
* Use {@link #requestObject} for asynchronous access.
*
* The header context of a list binding only delivers <code>$count</code> (wrapped in an object
* if <code>sPath</code> is "").
*
* @param {string} [sPath=""]
* A path relative to this context
* @returns {any}
* The requested value
* @throws {Error}
* If the context's root binding is suspended or if the context is a header context and the
* path is neither empty nor "$count".
*
* @public
* @see sap.ui.model.Context#getObject
* @since 1.39.0
*/
// @override sap.ui.model.Context#getObject
Context.prototype.getObject = function (sPath) {
return _Helper.publicClone(this.getValue(sPath));
};
/**
* Returns the property value for the given path relative to this context. The path is expected
* to point to a structural property with primitive type. Returns <code>undefined</code>
* if the data is not (yet) available; no request is triggered. Use {@link #requestProperty}
* for asynchronous access.
*
* @param {string} sPath
* A path relative to this context
* @param {boolean} [bExternalFormat]
* If <code>true</code>, the value is returned in external format using a UI5 type for the
* given property path that formats corresponding to the property's EDM type and constraints.
* If the type is not yet available, <code>undefined</code> is returned.
* @returns {any}
* The requested property value
* @throws {Error} If
* <ul>
* <li> the context's root binding is suspended,
* <li> the value is not primitive,
* <li> or the context is a header context and the path is not "$count"
* </ul>
*
* @public
* @see sap.ui.model.Context#getProperty
* @see sap.ui.model.odata.v4.ODataMetaModel#requestUI5Type
* @since 1.39.0
*/
// @override sap.ui.model.Context#getProperty
Context.prototype.getProperty = function (sPath, bExternalFormat) {
var oError, oSyncPromise;
this.oBinding.checkSuspended();
oSyncPromise = this.fetchPrimitiveValue(sPath, bExternalFormat, true);
if (oSyncPromise.isRejected()) {
oSyncPromise.caught();
oError = oSyncPromise.getResult();
if (oError.isNotPrimitive) {
throw oError;
} else if (!oError.$cached) {
// Note: errors due to data requests have already been logged
Log.warning(oError.message, sPath, sClassName);
}
}
return oSyncPromise.isFulfilled() ? oSyncPromise.getResult() : undefined;
};
/**
* Returns the query options from the associated binding for the given path.
*
* @param {string} sPath
* The relative path for which the query options are requested
* @returns {object}
* The query options from the associated binding (live reference, no clone!)
*
* @private
*/
Context.prototype.getQueryOptionsForPath = function (sPath) {
return this.oBinding.getQueryOptionsForPath(sPath);
};
/**
* Returns the group ID of the context's binding that is used for update requests. See
* {@link sap.ui.model.odata.v4.ODataListBinding#getUpdateGroupId} and
* {@link sap.ui.model.odata.v4.ODataContextBinding#getUpdateGroupId}.
*
* @returns {string}
* The update group ID
*
* @public
* @since 1.81.0
*/
Context.prototype.getUpdateGroupId = function () {
return this.oBinding.getUpdateGroupId();
};
/**
* Returns the value for the given path relative to this context. The function allows access to
* the complete data the context points to (if <code>sPath</code> is "") or any part thereof.
* The data is a JSON structure as described in <a href=
* "https://docs.oasis-open.org/odata/odata-json-format/v4.0/odata-json-format-v4.0.html"
* >"OData JSON Format Version 4.0"</a>.
* Note that the function returns the cache instance. Do not modify the result, use
* {@link sap.ui.model.odata.v4.ODataPropertyBinding#setValue} instead.
*
* Returns <code>undefined</code> if the data is not (yet) available; no request is triggered.
*
* @param {string} [sPath=""]
* A path relative to this context
* @returns {any}
* The requested value
* @throws {Error}
* If the context's root binding is suspended
*
* @private
*/
Context.prototype.getValue = function (sPath) {
var oSyncPromise,
that = this;
this.oBinding.checkSuspended();
oSyncPromise = this.fetchValue(sPath, null, true)
.catch(function (oError) {
if (!oError.$cached) {
that.oModel.reportError("Unexpected error", sClassName, oError);
}
});
if (oSyncPromise.isFulfilled()) {
return oSyncPromise.getResult();
}
};
/**
* Returns whether there are pending changes for bindings dependent on this context, or for
* unresolved bindings (see {@link sap.ui.model.Binding#isResolved}) which were dependent on
* this context at the time the pending change was created. This includes the context itself
* being {@link #isTransient transient} or {@link #delete deleted} on the client, but not yet on
* the server. Since 1.98.0, {@link #isInactive inactive} contexts are ignored, unless their
* {@link sap.ui.model.odata.v4.ODataListBinding#event:createActivate activation} has been
* prevented and therefore {@link #isInactive} returns <code>1</code>.
*
* @returns {boolean}
* Whether there are pending changes
*
* @public
* @since 1.53.0
*/
Context.prototype.hasPendingChanges = function () {
return this.isTransient() && this.isInactive() !== true
|| this.oDeletePromise && this.oDeletePromise.isPending()
|| this.getBinding().hasPendingChangesForPath(this.sPath)
|| this.oModel.getDependentBindings(this).some(function (oDependentBinding) {
return oDependentBinding.oCache
? oDependentBinding._hasPendingChanges(false, true)
: oDependentBinding.hasPendingChangesInDependents(false, true);
})
|| this.oModel.withUnresolvedBindings("hasPendingChangesInCaches", this.sPath.slice(1));
};
/**
* Returns whether this context is deleted. It becomes <code>true</code> immediately after
* calling {@link #delete}, even while the request is waiting for
* {@link sap.ui.model.odata.v4.ODataModel#submitBatch submitBatch} or is in process. It becomes
* <code>false</code> again when the DELETE request fails or is canceled. The result of this
* function can also be accessed via the "@$ui5.context.isDeleted" instance annotation at the
* entity.
*
* @returns {boolean} <code>true</code> if this context is deleted
*
* @public
* @see #delete
* @since 1.105.0
*/
Context.prototype.isDeleted = function () {
return !!this.oDeletePromise;
};
/**
* Tells whether the group node that this context points to is expanded.
*
* @returns {boolean|undefined}
* Whether the group node that this context points to is expanded, or <code>undefined</code>
* if the node is not expandable
*
* @public
* @see #collapse
* @see #expand
* @since 1.77.0
*/
Context.prototype.isExpanded = function () {
return this.getProperty("@$ui5.node.isExpanded");
};
/**
* Returns whether this context is inactive. The result of this function can also be accessed
* via instance annotation "@$ui5.context.isInactive" at the entity.
*
* Since 1.110.0, <code>1</code> is returned in case
* {@link sap.ui.model.odata.v4.ODataListBinding#event:createActivate activation} has been
* prevented. Note that
* <ul>
* <li> it is truthy: <code>!!1 === true</code>,
* <li> it is almost like <code>true</code>: <code>1 == true</code>,
* <li> but it can easily be distinguished: <code>1 !== true</code>,
* <li> and <code>if (oContext.isInactive()) {...}</code> treats inactive contexts the same,
* no matter whether activation has been prevented or not.
* </ul>
*
* @returns {boolean|number|undefined} <code>true</code> if this context is inactive,
* <code>false</code> if it was created in an inactive state and has been activated,
* <code>1</code> in case activation has been prevented (since 1.110.0), and
* <code>undefined</code> otherwise.
*
* @public
* @see #isTransient
* @see sap.ui.model.odata.v4.ODataListBinding#create
* @since 1.98.0
*/
Context.prototype.isInactive = function () {
return this.bInactive;
};
/**
* Returns whether this context is kept alive even when it is removed from its binding's
* collection, for example if a filter is applied and the entity represented by this context
* does not match the filter criteria.
*
* @returns {boolean} <code>true</code> if this context is kept alive
*
* @public
* @see #setKeepAlive
* @since 1.81.0
*/
Context.prototype.isKeepAlive = function () {
return this.bKeepAlive;
};
/**
* Tells whether this context is currently selected.
*
* @returns {boolean} Whether this context is currently selected
*
* @experimental As of version 1.111.0
* @public
* @see #setSelected
*/
Context.prototype.isSelected = function () {
return this.bSelected;
};
/**
* For a context created using {@link sap.ui.model.odata.v4.ODataListBinding#create}, the
* method returns <code>true</code> if the context is transient, meaning that the promise
* returned by {@link #created} is not yet resolved or rejected, and returns <code>false</code>
* if the context is not transient. The result of this function can also be accessed via
* instance annotation "@$ui5.context.isTransient" at the entity.
*
* @returns {boolean|undefined}
* Whether this context is transient if it is created using
* {@link sap.ui.model.odata.v4.ODataListBinding#create}; <code>undefined</code> if it is not
* created using {@link sap.ui.model.odata.v4.ODataListBinding#create}
*
* @public
* @see #isInactive
* @since 1.43.0
*/
Context.prototype.isTransient = function () {
return this.oSyncCreatePromise && this.oSyncCreatePromise.isPending();
};
/**
* Patches the context data with the given patch data.
*
* @param {object} oData
* The data to patch with
* @returns {sap.ui.base.SyncPromise}
* A promise that is resolved without a result when the patch is done.
*
* @private
*/
Context.prototype.patch = function (oData) {
return this.withCache(function (oCache, sPath) {
oCache.patch(sPath, oData);
}, "");
};
/**
* Refreshes the single entity represented by this context. Use {@link #requestRefresh} if you
* want to wait for the refresh.
*
* @param {string} [sGroupId]
* The group ID to be used for the refresh; if not specified, the group ID for the context's
* binding is used, see {@link #getGroupId}.
* @param {boolean} [bAllowRemoval]
* If the context belongs to a list binding, the parameter allows the list binding to remove
* the context from the list binding's collection because the entity does not match the
* binding's filter anymore, see {@link sap.ui.model.odata.v4.ODataListBinding#filter};
* a removed context is destroyed, see {@link #destroy}. If the context belongs to a context
* binding, the parameter must not be used.
* Supported since 1.55.0
*
* Since 1.84.0, if this context is {@link #isKeepAlive kept alive}, it is only destroyed if
* the corresponding entity does no longer exist in the back end. In this case, the
* <code>fnOnBeforeDestroy</code> callback passed with {@link #setKeepAlive}) is called.
* @throws {Error}
* If the group ID is not valid, if this context has pending changes or does not represent a
* single entity (see {@link sap.ui.model.odata.v4.ODataListBinding#getHeaderContext}), if the
* binding is not refreshable or is a list binding with data aggregation, if its root binding
* is suspended, or if the parameter <code>bAllowRemoval</code> is set for a context belonging
* to a context binding.
*
* @public
* @since 1.53.0
*/
Context.prototype.refresh
= function (sGroupId, bAllowRemoval) { // eslint-disable-line no-unused-vars
this.requestRefresh.apply(this, arguments).catch(this.oModel.getReporter());
};
/**
* Refreshes all dependent bindings with the given parameters and waits for them to have
* finished.
*
* @param {string} sResourcePathPrefix
* The resource path prefix which is used to delete the dependent caches and corresponding
* messages; may be "" but not <code>undefined</code>
* @param {string} [sGroupId]
* The group ID to be used for refresh
* @param {boolean} [bCheckUpdate]
* If <code>true</code>, a property binding is expected to check for updates
* @param {boolean} [bKeepCacheOnError]
* If <code>true</code>, the binding data remains unchanged if the refresh fails
* @returns {sap.ui.base.SyncPromise}
* A promise resolving when all dependent bindings are refreshed; it is rejected
* when the refresh fails; the promise is resolved immediately on a suspended binding
* @throws {Error}
* If the binding's root binding is suspended and a group ID different from the binding's
* group ID is given
*
* @private
*/
Context.prototype.refreshDependentBindings = function (sResourcePathPrefix, sGroupId,
bCheckUpdate, bKeepCacheOnError) {
return SyncPromise.all(
this.oModel.getDependentBindings(this).map(function (oDependentBinding) {
return oDependentBinding.refreshInternal(sResourcePathPrefix, sGroupId,
bCheckUpdate, bKeepCacheOnError);
})
);
};
/**
* Replaces this context with the given other context "in situ", that is, at the index it
* currently has in its list binding's collection. You probably want to delete this context
* afterwards without requesting the new count from the server, see the
* <code>bDoNotRequestCount</code> parameter of {@link #delete}.
*
* @param {sap.ui.model.odata.v4.Context} oOtherContext - The other context
* @throws {Error} If
* <ul>
* <li> this context's root binding is suspended,
* <li> this context is {@link #isTransient transient},
* <li> this context is {@link #isDeleted deleted},
* <li> the given other context
* <ul>
* <li> does not belong to the same list binding as this context,
* <li> is already in the collection (has an {@link #getIndex index}),
* <li> is {@link #delete deleted},
* <li> or is not {@link #isKeepAlive kept alive}.
* </ul>
* </ul>
*
* @public
* @since 1.97.0
*/
Context.prototype.replaceWith = function (oOtherContext) {
var oElement;
this.oBinding.checkSuspended();
if (this.isTransient() || this.isDeleted()) {
throw new Error("Cannot replace " + this);
}
if (oOtherContext.oBinding !== this.oBinding || oOtherContext.iIndex !== undefined
|| oOtherContext.isDeleted() || !oOtherContext.bKeepAlive) {
throw new Error("Cannot replace with " + oOtherContext);
}
oElement = oOtherContext.getValue();
this.oBinding.doReplaceWith(this, oElement,
_Helper.getPrivateAnnotation(oElement, "predicate"));
};
/**
* Returns a promise for the "canonical path" of the entity for this context. According to
* <a href=
* "https://docs.oasis-open.org/odata/odata/v4.0/odata-v4.0-part2-url-conventions.html#canonical-urlurl4.1.1"
* >"4.3.1 Canonical URL"</a> of the specification "OData Version 4.0 Part 2: URL Conventions",
* this is the "name of the entity set associated with the entity followed by the key predicate
* identifying the entity within the collection". Use the canonical path in
* {@link sap.ui.core.Element#bindElement} to create an element binding.
*
* Note: For a transient context (see {@link #isTransient}) a wrong path is returned unless all
* key properties are available within the initial data.
*
* @returns {Promise}
* A promise which is resolved with the canonical path (e.g. "/SalesOrderList('0500000000')")
* in case of success, or rejected with an instance of <code>Error</code> in case of failure,
* e.g. if the given context does not point to an entity
*
* @function
* @public
* @since 1.39.0
*/
Context.prototype.requestCanonicalPath = _Helper.createRequestMethod("fetchCanonicalPath");
/**
* Returns a promise on the value for the given path relative to this context. The function
* allows access to the complete data the context points to (if <code>sPath</code> is "") or
* any part thereof. The data is a JSON structure as described in <a href=
* "https://docs.oasis-open.org/odata/odata-json-format/v4.0/odata-json-format-v4.0.html"
* >"OData JSON Format Version 4.0"</a>.
* Note that the function clones the result. Modify values via
* {@link sap.ui.model.odata.v4.Context#setProperty}.
*
* The header context of a list binding only delivers <code>$count</code> (wrapped in an object
* if <code>sPath</code> is "").
*
* If you want {@link #requestObject} to read fresh data, call {@link #refresh} first.
*
* @param {string} [sPath=""]
* A path relative to this context
* @returns {Promise}
* A promise on the requested value; it is rejected if the context is a header context and the
* path is neither empty nor "$count".
* @throws {Error}
* If the context's root binding is suspended
*
* @public
* @see #getBinding
* @see sap.ui.model.odata.v4.ODataContextBinding#refresh
* @see sap.ui.model.odata.v4.ODataListBinding#refresh
* @since 1.39.0
*/
Context.prototype.requestObject = function (sPath) {
this.oBinding.checkSuspended();
return Promise.resolve(this.fetchValue(sPath)).then(_Helper.publicClone);
};
/**
* Returns a promise on the property value for the given path relative to this context. The path
* is expected to point to a structural property with primitive type.
* Since 1.81.1 it is possible to request more than one property. Property values that are not
* cached yet are requested from the back end.
*
* @param {string|string[]} [vPath]
* One or multiple paths relative to this context
* @param {boolean} [bExternalFormat]
* If <code>true</code>, the values are returned in external format using UI5 types for the
* given property paths that format corresponding to the properties' EDM types and constraints
* @returns {Promise}
* A promise on the requested value or values; it is rejected if a value is not primitive or
* if the context is a header context and a path is not "$count"
* @throws {Error}
*