The OData V4 Model supports features of the OData Extension for Data Aggregation V4.0 specification.
The $$aggregation binding parameter at sap.ui.model.odata.v4.ODataModel#bindList holds the information needed for data aggregation. It may be changed by sap.ui.model.odata.v4.ODataListBinding#setAggregation. It cannot be combined with an explicit system query option $apply, because it implicitly derives $apply. For more information, see the OData Extension for Data Aggregation Version 4.0 specification.
Since 1.117.0, either a read-only recursive hierarchy (see below) or (pure) data aggregation is supported, but no mix; hierarchyQualifier is the leading property that decides between these two use cases. Since 1.125.0, maintenance of a recursive hierarchy is supported.
Note:
Data aggregation or a recursive hierarchy cannot be combined with grouping via a list binding’s first sorter.For more information, see the
vGroupparameter ofsap.ui.model.Sorter.
For every aggregatable property, you can provide the name of the custom aggregate for a corresponding currency or unit of measure. That custom aggregate must return the single value of a unit in case there is only one, or null otherwise (“multi-unit situation”). In the special case that the single value is null, an empty string "" has to be returned.
Normally, there is also a structural property of the same name as the custom aggregate, providing type information, etc. In case of a multi-unit situation, v4.Context#getFilter may be helpful to send a request for more details.
You can use the client-side instance annotations listed below to access a node level, expansion state, and other node properties.
Note:
For property bindings, the usual syntax like
{= %{@$ui5.node.level} }can be used, and automatic type determination is available.For expression bindings, use the
%{binding}syntax instead of${binding}to avoid type conversion errors, for example:
<m:Button enabled="{= %{@$ui5.node.level} > 1 }"/><m:Button enabled="{= %{@$ui5.node.isExpanded} !== undefined }"/>The
%{binding}syntax is a shortcut for${path : 'binding', targetType : 'any'}and prevents automatic type detection that would otherwise format the value according to the control property’s target type.
@$ui5.node.level – A non-negative integer which describes the node level; “0” is the single root node which corresponds to the grand total row, “1” are the top-level group nodes, etc.
@$ui5.node.isExpanded – A boolean which determines whether this node is currently expanded. true means yes, false means no, undefined means that (the state is undefined because) this node is a leaf. As an implementation detail, the annotation might simply be missing for leaves.@$ui5.node.groupLevelCount – An integer value which determines the count of the direct children of a group node. As an implementation detail, the annotation is only available if the corresponding node is expanded.
@$ui5.node.isTotal – A boolean which indicates whether the node represents a subtotal or grand total row.Two scenarios are supported:
You can provide properties for grouping and aggregation. An appropriate system query option $apply is derived from those. The list binding then still provides a flat list of contexts (“rows”), but with additional aggregated properties (“columns”). In addition, you can request grand total values for aggregatable properties. In this case, an extra row appears at the beginning of the flat list of contexts that contains the grand total values, as well as empty values for all other properties.
Sample Code:
Example XML View With Grand Total
<table:Table fixedRowCount="1" rows="{ path : '/BusinessPartners', parameters : { $$aggregation : { aggregate : { SalesAmount : { grandTotal : true, unit : 'Currency' } }, group : { Country : {additionally : ['Texts/Country']} } }, $filter : 'SalesAmount gt 1000000', $orderby : 'SalesAmount desc' } }"> <table:Column template="Texts/Country"> <Label text="Country"/> </table:Column> <table:Column hAlign="End" template="SalesAmount"> <Label text="Sales Amount"/> </table:Column> <table:Column template="Currency"> <Label text="Currency"/> </table:Column> </table:Table>
You can provide group levels to determine a hierarchy of expandable group levels in addition to the leaf nodes determined by the groupable and aggregatable properties. To achieve this, specify the names of the group levels in the groupLevels property of $$aggregation. If no other groupable properties are given except those named as levels, the last group level determines the leaf nodes and is not expandable.
Group levels can be combined with the system query option $count : true; for more information, see Binding the Count of a Collection. Group levels can only be combined with filtering before the aggregation (see below). Note how an $orderby option can address groups across all levels. For every aggregatable property, you can request subtotals and a grand total individually.
Sample Code:
Example XML View With Hierarchy
<table:Table fixedRowCount="1" rows="{ path : '/BusinessPartners', parameters : { $$aggregation : { aggregate : { SalesAmount : { grandTotal : true, subtotals : true, unit : 'Currency' } }, group : { Country : {additionally : ['CountryText']}, Region : {additionally : ['RegionText']} }, groupLevels : ['Country','Region','Segment'] }, $count : false, $orderby : 'Country,Region desc,Segment', filters : {path : \'Region\', operator : \'GE\', value1 : \'Mid\'} } }"> <table:Column template="CountryText"> <Label text="Country"/> </table:Column> <table:Column template="RegionText"> <Label text="Region"/> </table:Column> <table:Column template="Segment"> <Label text="Segment"/> </table:Column> <table:Column hAlign="End" template="SalesAmount"> <Label text="Sales Amount"/> </table:Column> <table:Column template="Currency"> <Label text="Currency"/> </table:Column> </table:Table>
For aggregatable properties where grand total or subtotal values are requested, you can globally choose where these should be displayed:
Use the grandTotalAtBottomOnly or subtotalsAtBottomOnly property with values true or false, respectively, or simply omit it. For more information, see the API Reference in the Demo Kit.
Filters are provided to the list binding as described in Filtering. The Filter objects are analyzed automatically to perform the filtering before the aggregation where possible using the filter() transformation. The remaining filters, including the provided $filter parameter of the binding, are applied after the aggregation either via the system query option $filter or within the system query option $apply, using again the filter() transformation.
Note that Filter objects are only supported for aggregatable properties with an alias if neither grandTotal nor groupLevels are used. For more information, see the name property of the aggregate map of the oAggregation parameter of v4.ODataListBinding#setAggregation.
You can provide a search string to be applied before data aggregation via the oAggregation.search parameter of ODataListBinding#setAggregation. It works like the “5.1.8 System Query Option $search”, but is applied before data aggregation, not after it. Note that certain content will break the syntax of the $apply system query option when embedded into a search() transformation and thus result in an invalid request. If the OData service supports the ODATA-1452 proposal, then the command system query option when embedded into a ODataUtils.formatLiteral(sSearch, "Edm.String"); should be used to encapsulate the whole search string beforehand (see sap.ui.model.odata.v4.ODataUtils.formatLiteral). Otherwise, it might be wise to restrict your search input accordingly.
For each groupable property, you can define an optional list of strings that provides the paths to properties (like texts or attributes) related to this groupable property in a 1:1 relation. They are requested additionally via groupby and must not change the actual grouping; a unit for an aggregatable property must not be repeated there.
Since OpenUI5 1.151.0, editing is supported for lists of unaggregated records with a grand total. Since OpenUI5 1.154.0, editing is supported as an experimental feature in combination with group levels. This is ensured by providing all key properties of the entity type for grouping in the group property of $$aggregation. The following operations are possible:
Note:
- The application of side effects will in some cases fall back to the refresh, for example, when requesting to update a property for all records in a table.
- Refreshing the analytical table as a side effect closes all previously expanded group levels.
- The keep-alive mechanism described in Data Reuse is only supported without group levels.
The header context and the contexts of grand total and group levels may be outdated. The header context is set to outdated when a manual refresh by the user is required. For example, when the user has changed a property used for sorting, the changed record may appear in a different place after the refresh. The grand total context and the group level contexts are set to outdated when their aggregated data, the grand totals and the sub-totals, should not be shown to the user. This happens, for example, when the user changes a property used for filtering. In this case, the changed record is shown in the table until the user manually refreshes. Upon refresh, the changed record will no longer be part of the list if it does not match the filter. However, automatic updates of the totals values would apply the filter already. To ensure that shown information is consistent, the totals should not be shown in this case.
You can display hierarchical data (a “tree”) inside a table using a list binding. Read-only hierarchies are supported since 1.117.0 while maintenance is supported since 1.125.0. Such a recursive hierarchy is described by a pair of “Org.OData.Aggregation.V1.RecursiveHierarchy” and “com.sap.vocabularies.Hierarchy.v1.RecursiveHierarchy” annotations at the list binding’s entity type. You need to use the same qualifier for both of these annotations, which is known as the hierarchy qualifier.
If the hierarchyQualifier property of $$aggregation is present, a recursive hierarchy without data aggregation is defined. The only other supported properties are expandTo, which optionally specifies the number of initially expanded levels as a positive integer, and search (see Search Before Data Aggregation above). Sorting and filtering can be done as usual (both as system query options and as OpenUI5 objects), but $search is not supported (use search instead, see above).
Note how this influences v4.ODataListBinding#getCount. You can use the v4.ODataListBinding#getAggregation method with the new bVerbose parameter to access some details from the above-mentioned annotations. The v4.ODataListBinding#setAggregation method can be used to change $$aggregation.
The following properties are required from a “com.sap.vocabularies.Hierarchy.v1.RecursiveHierarchy” annotation:
DistanceFromRootDrillStateLimitedDescendantCountLimitedRankNote:
These hierarchical properties are used internally by the list binding. They are not intended for use by applications for property bindings.
Actions and functions can be invoked as usual. Side effects are supported both for single rows and the entire list (“side-effects refresh”; see v4.Context#requestSideEffects for details), even if they affect the hierarchy (node IDs, parent/child relations, or sibling order) itself. The current tree state with respect to expanded and collapsed nodes (see v4.Context#isExpanded) is kept even in case of such a side-effects refresh.
The @$ui5.node.level and @$ui5.node.isExpanded client-side instance annotations can be used as described above to access a node level or expansion state. A context’s index refers to its position in the list binding’s “flat” collection. You can use v4.Context#getParent to access a node’s parent. If the parent is not yet known, v4.Context#requestParent can be used to request it from the server. The v4.Context#isAncestorOf API also helps to inspect the parent/child relationship (note that v4.ODataListBinding#getRootBinding is unrelated).
Since 1.125.0, a recursive hierarchy need not be read-only, but maintenance is supported, namely:
v4.ODataListBinding#create and “@$ui5.node.parent” therein. Since 1.130.0, the createInPlace option is supported for the$$aggregation binding parameter and the v4.ODataListBinding#setAggregation method. When set, newly created nodes are shown “in place”, i.e. at the position specified by the service . Otherwise, created nodes are displayed out of place as the first children of their parent or as the first roots, but not in their usual position as defined by the service and the current sort order.v4.Context#delete. Note that the deletion is first done on the server and only later shown on the client. Thus, the group ID must not have submit mode “API”.v4.Context#move. Note that nextSibling requires a “com.sap.vocabularies.Hierarchy.v1.RecursiveHierarchyActions” annotation with at least a ChangeNextSiblingAction. An out-of-place node has no preceding sibling and thus cannot be moved up. Each out-of-place node with the same parent has the same following sibling, namely that parent’s first in-place (that is, not out-of-place) node. That following sibling is null if the parent has no in-place children. A similar consideration applies for out-of-place root nodes. This way, you can move out-of-place nodes down so that they become in-place nodes. Thus, you actively determine their position among their siblings. A first in-place node has no preceding sibling even if out-of-place nodes are present. Thus, an in-place node cannot be moved up in order to become out-of-place (again).copy parameter of v4.Context#move. Note that the copying of nodes requires a “com.sap.vocabularies.Hierarchy.v1.RecursiveHierarchyActions” annotation with the CopyAction.Note that only one such change must be pending at any point in time. That is, you must wait for one change to be completed before starting the next change. The only exception is property updates, for which multiple properties can be combined as usual.
The TopLevels function is fundamental for recursive hierarchies. It describes the input set underlying the hierarchy (see the list binding’s path) and specifies which recursive hierarchy is built on top (see hierarchyQualifier above). It takes care to initially expand a certain number of levels (see expandTo above) and later to expand or collapse certain nodes in order to keep the tree state during a side-effects refresh.
A typical request to read the first page of a hierarchical table may look like this:
GET /sap/opu/odata4/IWBEP/TEA/default/IWBEP/TEA_BUSI/0001/EMPLOYEES?$apply=orderby(AGE)/com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/EMPLOYEES,HierarchyQualifier='OrgChart',NodeProperty='ID',Levels=2)&$select=AGE,DescendantCount,DistanceFromRoot,DrillState,ID,MANAGER_ID,Name&$count=true&$skip=0&$top=115
Note how the sibling(!) order is specified via an orderby transformation (see the list binding’s sorters as well as $orderby). The list binding’s path would be “/EMPLOYEES”, and $$aggregation looks as follows. The model’s autoExpandSelect parameter does its magic, and $count&$skip&$top is taken care of automatically by the list binding.
<Table rows="{
path: '/EMPLOYEES',
parameters: {
$count: false,
$orderby: 'AGE',
$$aggregation: {
expandTo: 2,
hierarchyQualifier: 'OrgChart'
},
$$patchWithoutSideEffects: true
},
...
}">
If the list binding uses $count: true, for example, to show this count as part of a title, an extra request is sent once (not each time when scrolling, but of course again after a (side-effects) refresh). It includes any custom query options as well as filter and search criteria: GET /sap/opu/odata4/IWBEP/TEA/default/IWBEP/TEA_BUSI/0001/EMPLOYEES/$count?sap-client=123&$filter=AGE ge 0 and (Is_Manager)&$search=developer
With filter and search, the main request looks a bit more complicated and includes an ancestors transformation beforehand: GET /sap/opu/odata4/IWBEP/TEA/default/IWBEP/TEA_BUSI/0001/EMPLOYEES?$apply=ancestors($root/EMPLOYEES,OrgChart,ID,filter(AGE ge 0 and (Is_Manager))/search(developer),keep start)/orderby(AGE)/com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/EMPLOYEES,HierarchyQualifier='OrgChart',NodeProperty='ID',Levels=2) &$select=AGE,DescendantCount,DistanceFromRoot,DrillState,ID,MANAGER_ID,Name&$count=true&$skip=0&$top=115
When a node is expanded individually, a request for its children is sent using a descendants transformation, for example: GET /sap/opu/odata4/IWBEP/TEA/default/IWBEP/TEA_BUSI/0001/EMPLOYEES?$apply=descendants($root/EMPLOYEES,OrgChart,ID,filter(ID eq '0'),1)/orderby(AGE)&$select=AGE,DrillState,ID,MANAGER_ID,Name&$count=true&$skip=0&$top=6
When keeping the expand/collapsed state of nodes, the TopLevels function’s ExpandLevels parameter is needed, for example: GET /sap/opu/odata4/IWBEP/TEA/default/IWBEP/TEA_BUSI/0001/EMPLOYEES?$apply=com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/EMPLOYEES,HierarchyQualifier='OrgChart',NodeProperty='ID',Levels=2,ExpandLevels=[{NodeID : "8", Levels : 0}, {NodeID : "1", Levels : 1}])&...
When moving a node, it is PATCHed with a payload like "EMPLOYEE_2_MANAGER@odata.bind" : "EMPLOYEES('9')", which points to the new parent. When creating a new node, a similar payload is used to point to the new parent as part of the POST. For root nodes, a null value is sent. To determine a node’s sibling position, the “com.sap.vocabularies.Hierarchy.v1.RecursiveHierarchyActions” annotation’s ChangeNextSiblingAction is invoked with a payload like NextSibling : {ID : "3" }. A null value is used to make a node the last sibling.