Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

odatamodel v4 - having a hard time to manipulate the data (doc bug) #2766

Closed
wernerdaehn opened this issue Dec 23, 2019 · 23 comments
Closed

Comments

@wernerdaehn
Copy link

wernerdaehn commented Dec 23, 2019

OpenUI5 version: 1.73.2

In a JsonModel and using the Controller to create the models, things are easy. You define the model and assign it to the view. In case you need to load the model because the value is needed for another control (master-detail), the loaddata method has a parameter for synchronous loading. You can access the jsonmodel after it had been read and manipulate the data, e.g. add a default row, set the selected value to the first entry and the such.

I have to admit, I fail doing that in the v4 odatamodel and with manifests.

Need to look into the details more but found a preload=true property at the model declaration. So that's good.
Now I want to add an event listener to modify the data after it had been loaded (list of all users plus a row with the value "*" to search in all users).
The detail control should not load the data until the filter has been set.

This is a simple master-detail case with both using odata v4.

I believe some more documentation would be needed for that and maybe a feature request or two.

Enhancement 1: support model references in the odata filter
My example of a list referencing a filter from another model, thus the system knows the order of the models to load and what to wait for.

<list items= "{path: 'Objects>/TABLE',
    filters: [  { path: 'SCHEMA_NAME', operator: 'EQ', value1: '{=${usermodel>/user} }' } ]">
  <items>

Enhancement 2: Some sort of "default row" property to be included in the data.

Thanks!

@uhlmannm
Copy link
Member

Hi @wernerdaehn ,

how to modify data in controller code is described in https://sapui5.hana.ondemand.com//#/topic/17b30ac2d5474078be31e695e97450cc.html.
After loading data, the 'dataReceived' event is fired, see also https://sapui5.hana.ondemand.com//#/topic/1a010d3b92c34226a96f202ec27e9217.

Does this help?

As to your enhancements:
Enhancement 1: This looks rather complicated to realize in a general way. An order of loading could also be achieved by creating a binding as suspended and resuming it when the filters have been set.

<list items= "{path: 'Objects>/TABLE', suspended:true}">

Enhancement 2: How should this work? The list binding would contain the additional default row? And this one would exist only on the client?
Would this be helpful beside the described use case if enhancement 1 is available?

Best regards
Mathias.

@wernerdaehn
Copy link
Author

wernerdaehn commented Jan 12, 2020

Thanks Mathias, that changes a lot. When I read "binding" I interpreted that as "Control-to-Model Binding". It seems the term is used for "data-to-odatamodel binding" as well. Okay, understood.

At least the things I'd like to do are possible. Then the enhancements are just to make life easier.

Regarding the Enhancement 1, parameters within filters, this was asked quite often in stack overflow. That proofs I am not the only one. How would you build a master-detail screen using oData? A table at the top half allowing you to browse through the order header table. And a second table at the bottom showing the currently selected order's line items? I can think of a single solution only and that is the $expand, but that requires the odata service to be prepared for that. That does not need to be that case always. Then you have to synchronize the master/detail oData models somehow.

Regarding Enhancement 2, the default row, this is again something very common. Most combobox controls or dropdown boxes are used as value help, showing all possible values for this field. Plus the option to specify NULL. For example the screen to enter a material has a drop down box for the material type and shows the allowed values only: Raw Material, Replenishable good,... or NULL.
But if that is the case, adding a default-row option to the controls might actually be the better solution?

@uhlmannm
Copy link
Member

Hi Werner,

yes, with the V4 Model the binding object is used for more than the binding of model and control.

As for master-detail: The idea is to use a relative binding for the detail page and set the context of the selected row of the master table. This is demonstrated in our Sales Orders sample application. The detail page binding starts in line 199 of Main.view.xml. Setting the binding context is done in line 42 of Main.controller.js.

Using this approach you will also get a caching of the detail data per context, i.e. when switching the selected row to a row for which details were already displayed, these detail data do not need to be reloaded.

Already reading all items via a $expand in the header binding is not a good option from my perspective. This would read all items for the read headers even though it is unclear whether they are required. The loading of the header list would be significantly slower.
For performance it seems better to read the items only when the respective header is selected. And this is what happens in our Sales Orders sample.

For enhancement 2 I will need to talk to some colleagues.

Best regards
Mathias.

@wernerdaehn
Copy link
Author

Thank you so much for your precise and timely responses!

@uhlmannm
Copy link
Member

Hi Werner,

regarding enhancement 2. First of all, let me shortly mention how you could achieve this today with the V4 model.
Taken you have a list binding bound to some kind of list control. The default row can be added using ODataListBinding#create.
Set the $$updateGroupId of the list binding to an API group for which submitBatch is not called to prevent that this new row is sent to the backend. Note that this will also mean that other changes to the list are not sent to the server.

As to when to create the new row: In general, the new row can be created as soon as the list binding is available, i.e. also before data is read. The dataReceived event could also be used.

Now to the requirement part: Our architect asked whether the default row would not fit better in the controls. Could you please provide which controls you plan to use here? I could then get in contact with the respective colleagues.

Best regards
Mathias.

@wernerdaehn
Copy link
Author

Thanks. Will try the things out but will need time. I am sure your proposal will work.

In regards to controls I would see

  • ComboBox
  • Multi Combo Box
  • Search Field (the suggestions)
  • TreeTable for a hierarchical search
  • Select

@uhlmannm
Copy link
Member

Thank you!
Just a note on the Tree Table: There is no tree support for OData V4 so far.

Best regards
Mathias.

@uhlmannm
Copy link
Member

I mailed colleagues responsible for controls. Let's see ...

@nikolay-kolarov
Copy link
Contributor

Hello @wernerdaehn ,

Thanks for your feedback.
We have checked you feature request and as well discussed it with the responsible colleagues from UX.
For the ComboBox, MultiComboBox and SearchField there is an easy way just to delete the entered value and such an enhancement on control level will bring additional complexity in the code with small benefit for the users.
For the Select such an enhancement could conflict with all of the user-handled scenarios that are out there already. Additionally Select API and existing behavior diverge from other similar controls and this is no more something to change.

Best Regards,
Nikolay

@uhlmannm
Copy link
Member

uhlmannm commented Feb 4, 2020

Hi Werner,

has my proposal worked out?

Best regards
Mathias.

@wernerdaehn
Copy link
Author

Yes, what has been said makes total sense. I still believe the documentation could be a bit more clear and complete, though.

For example, at the moment I am stuck here: Goal is to read the odata v4 manually...

	    var oModel = new sap.ui.model.odata.v4.ODataModel({
    		serviceUrl : "/HanaAppContainer/protected/odata/SYS/M_CS_TABLES/", 
    		"autoExpandSelect": true,
			"operationMode": "Server",
			"groupId": "$direct",
			"synchronizationMode": "None"
	    });

	    var oList = oModel.bindList("/TABLE", undefined, undefined, undefined, {
	          $$ownRequest: true,
	          $filter : "SCHEMA_NAME eq 'SAPHANADB'",
	          $select: ["TABLE_NAME", "MEMORY_SIZE_IN_TOTAL", "RECORD_COUNT"]
	        });
	    
	    oList.requestContexts().then(function (aContexts) {
	    	var oData = [];
	    	aContexts.forEach(function (oContext) {
	    		oData.push(oContext.getObject());
	    	});
	    });

Question 1:
The $select is ignored. The docs say it is an allowed parameter, so I assume the second statement counts: property bindings overrule the $select.
https://sapui5.hana.ondemand.com/#/api/sap.ui.model.odata.v4.ODataModel%23methods/bindList

Question 2:
Is there a better option to get an array of rows into the oData[] other than a forEach and getObject()?

Question 3:
How do I bind single properties to the list binding so I do not have to specify the $select manually but the odatamodel does that automatically just like it does when binding controls to the model?

But these are really follow up questions. The case as such can be closed - I step back from my request, you convinced me your approach is better.

@uhlmannm
Copy link
Member

@wernerdaehn :
Question 1: I think this needs to be checked. I have created an internal ticket (2070245603) to this end.
Question 2: I like to use map:

oListBinding.requestContexts(0,100).then(function (aContexts) {
	var aData = aContexts.map(oContext => oContext.getObject());
	oJSONModel.setProperty("/People", aData);
},

But in essence, the code still needs to handle each context individually.

Question 3: When using autoExpandSelect:true it is still possible to specify additional $select and $expand parameters for the binding (though not to change them later using #changeParameters). That this does not seem to work for requestContexts needs to be investigated. I do not think that it is a solution to create all the property bindings in controller code when using requestContexts.

Best regards
Mathias.

@uhlmannm
Copy link
Member

Yes, what has been said makes total sense. I still believe the documentation could be a bit more clear and complete, though.

Could you please outline where you see potential for documentation improvements?

Many thanks
Mathias.

@wernerdaehn
Copy link
Author

Regarding question 3, how would you use the oDataModel manually, without a control. A model is created with URL etc, now I would like to execute an odata request equivalent to http://..../srv/ENTITY?$select=COL1.
Getting an entity collection with a defined set of columns. That is in essence my goal.

Just some arbitrary thoughts regarding docs...

Under Essentials:
https://sapui5.hana.ondemand.com//#/topic/95cf4b16762a465b9237b18d033f0cd2

  • In the oDataModel v4 it is said that "typically the model is not bound directly but indirectly via controls". Fine, but there I would love to see a few lines on how to use the model standalone.
  • Lifecycle: When does the model query the metadata, when is the select list created, when is the data received, how about the skipToken (oData paging), who sets $offset and $top.
  • Priorities: how is e.g. the filter and the select constructed? I assume first the (control) bindings, this is augmented with the parameters in the binding?

API Reference: Some more details about the binding per my questions.
https://openui5.hana.ondemand.com/api/sap.ui.model.odata.v4.ODataModel#constructor

  • in the method descriptions for bindContext and bindList show all allowed parameters including $select

Things I struggled with for a long time for whatever reason and don't know where to put it and if I am the only one:

  • I did not get the point initially that Model-Control binding and oDataModel-Fieldbinding is something different.
  • What is a binding context? Element, Property/List Binding is clear but BinsingContext? (It is one "row" of data, a collection entity so to speak, isn't it?)

@ThomasChadzelek
Copy link
Member

Hello @wernerdaehn !

"I would love to see a few lines on how to use the model standalone" Did you spot Accessing Data in Controller Code already?

"When does the model query the metadata" When needed. We hope you do not need to know more. It should just work.

"how about the skipToken (oData paging), who sets $offset and $top" Normally, a control is using a list binding and then paging happens automatically, via $skip and $top. Server-driven paging is also supported since 1.72.

"I did not get the point initially that Model-Control binding and oDataModel-Fieldbinding is something different." I do not get your point here, sorry.

A binding context is used on a control level to provide a reference point for relative bindings, for example, the entity instance that "{FirstName}" would refer to. For a (property) binding, this is what you define via Binding#setContext. I agree that there are some confusing terms.

Best regards,
Thomas

@wernerdaehn
Copy link
Author

ad 1) "Accessing Data in the Controller Code"? Initially no, but it was the first response and very helpful. But it answers only questions once the data request was defined, how to read the data.

ad 2) As said, with controls all works perfectly. All questions are about cases where the model is used manually. For the JSONModel this is described very well but also simple.

ad 3) Should be mentioned somewhere, in my opinion.

ad 4) Control vs Model binding: When I hear the term Binding, it is about XMLView and mappings like items={/data}. Hence the text about odata binding a model etc I did not get initially at all, as I did not see any relationship to controls. Only later in this thread it was pointed out that you use the term binding to bind fields to the model also, not only controls. That cleared up a lot and I am not sure if I am the only one.

@uhlmannm
Copy link
Member

Hi @wernerdaehn ,

I think Thomas and I are struggling a bit to understand what could be improved.

ad 1) "Accessing Data in the Controller Code"? Initially no, but it was the first response and very helpful. But it answers only questions once the data request was defined, how to read the data.

I do not get what you mean by "once the data request was defined"? You create a context or list binding with a path, parameters, ... This already defines most of the data request. Then you access the data. v4.Context#requestObject, v4.Context#requestProperty and v4.ODataListBinding#requestContexts will create and fire the request if necessary. The range of records requested through v4.ODataListBinding#requestContexts will hereby have an impact on $top and $skip of the generated request.

ad 2) As said, with controls all works perfectly. All questions are about cases where the model is used manually. For the JSONModel this is described very well but also simple.

With JSONModel or v2.ODataModel data requests may be triggered explicitly using methods like JSONModel#loadData or v2.ODataModel#read. With the V4 Model, the requests are generated under the hood if this is required for a specific API call. For example, if you call v4.ODataListBinding#requestContexts on a list binding that has not read any data, a GET will be triggered. The parameters of the GET are defined by the binding parameters. $top and $skip are determined by parameters iStart and iLength of requestContexts. So, an application will not create a request but request data, modify data or request some specific activity (create, delete, action) through the specific API of the model. (Where "model" might again be misleading as it means v4.ODataModel, v4.ODataPropertyBinding, v4.ODataContextBinding, v4.ODataListBinding and v4.Context and the private classes used in that context.)
And the V4 model determines whether a request to the backend is required for fulfilling that request.

ad 3) Should be mentioned somewhere, in my opinion.

It seems that we have nothing on Paging in the documentation and hence also nothing on Server-Driven Paging. The What's New of SAPUI5 1.72 contains some information.
We will check whether and how to provide some insight into paging.

ad 4) Control vs Model binding: When I hear the term Binding, it is about XMLView and mappings like items={/data}. Hence the text about odata binding a model etc I did not get initially at all, as I did not see any relationship to controls. Only later in this thread it was pointed out that you use the term binding to bind fields to the model also, not only controls. That cleared up a lot and I am not sure if I am the only one.

In the end, bindings are object instances. These bindings are for providing access to data in the model. There is a set of defined methods by which controls access and modify data through bindings. And the model creates requests for the backend as required for fulfilling the API calls.
In models like the JSONModel or the v2.ODataModel data on the client is stored centrally at the model and hence data access in controller code can be performed with APIs of the model.
In the v4.ODataModel this has changed. Data on the client is stored with relation to the binding (though not always at the same binding). Hence it is no longer possible to use model APIs for accessing data in controller code. The data access has to take place through APIs of the bindings. And there are actually two options how to get to a binding instance for accessing data:

  1. The application could create a new instance by using v4.ODataModel#bindList or #bindContext.
  2. The application could reuse the binding instance that is used for a control (e.g. o.Table.getBinding("rows")) and access data through that binding.

So with the V4 model bindings have become more than the glue between model and controls.

Is that going in the direction of the missing piece?

Best regards
Mathias.

@wernerdaehn
Copy link
Author

Thanks Mathias. I have the feeling we should simply close the case. You have helped a lot already, I have learned much and yes, your answers are going into the right direction.
These are rather special cases, hence I don't want you to spend too much of your time for this.

I also believe I got an answer to my most important question, the "how to use the v4.oDataModel to retrieve data manually. Per my understanding I need to bind the properties also. Anyway, worst case I will debug a bound sap.m.table in order to get the missing pieces.

Deal?

Thank you so much

Werner

@uhlmannm
Copy link
Member

Hi Werner,

I also believe I got an answer to my most important question, the "how to use the v4.oDataModel to retrieve data manually. Per my understanding I need to bind the properties also. Anyway, worst case I will debug a bound sap.m.table in order to get the missing pieces.

It must not be necessary to bind the properties relative to the list binding with autoExpandSelect when using requestContexts. We are looking into this.

Deal?

Deal.

Thank you so much
Werner

You are welcome.

Best regards
Mathias.

@wernerdaehn
Copy link
Author

wernerdaehn commented May 28, 2020

Regarding the manual binding, I am very close.

This code works, except that I request a list of objects, hence bindContext is wrong, should be bindList.

	    var oModel = new sap.ui.model.odata.v4.ODataModel({
    		serviceUrl : "/HanaAppContainer/protected/odata/RTDI/SALES_DATA_WEEKLY/", 
    		"autoExpandSelect": true,
			"operationMode": "Server",
			"groupId": "$direct",
			"synchronizationMode": "None"
	    });

	    var oBindingContext = oModel.bindContext("/TABLE", undefined, {
	          $$ownRequest: true,
	          $filter : "YEAR_ID eq 2003"
	        });
	    
	    var oContext = oBindingContext.getBoundContext();
	    var oProperty = oModel.bindProperty("WEEK_START_DATE", oContext);
	    var oProperty = oModel.bindProperty("YEAR_ID", oContext);
	    var oProperty = oModel.bindProperty("WEEK_ID", oContext);
	    var oProperty = oModel.bindProperty("REVENUE", oContext);
	    
	    
	    oBindingContext.initialize();
	    
	    oBindingContext.requestObject().then(function (o) {
                var data = o; 
	    }); */

It does everything I want, most important the requested URL contains the bound fields.
http://localhost:8080/HanaAppContainer/protected/odata/RTDI/SALES_DATA_WEEKLY/TABLE?$filter=YEAR_ID%20eq%202003&$select=REVENUE,WEEK_ID,WEEK_START_DATE,YEAR_ID

It throws an error "failed to drill-down" as I request an Object but it actually is an array (list). Fine.

What would be the equivalent for a list binding???

	    var oModel = new sap.ui.model.odata.v4.ODataModel({
    		serviceUrl : "/HanaAppContainer/protected/odata/RTDI/SALES_DATA_WEEKLY/", 
    		"autoExpandSelect": true,
			"operationMode": "Server",
			"groupId": "$direct",
			"synchronizationMode": "None"
	    });

	    var oList = oModel.bindList("/TABLE", undefined, undefined, undefined, {
	          $$ownRequest: true,
	          $filter : "YEAR_ID eq 2003"
	        });
	    var oListContext = oList.getContext(); // !!!!!!!!!!!!!!!!!!!!!!!!
	    var oProperty = oModel.bindProperty("WEEK_START_DATE", oListContext);
	    var oProperty = oModel.bindProperty("YEAR_ID", oListContext);
	    var oProperty = oModel.bindProperty("WEEK_ID", oListContext);
	    var oProperty = oModel.bindProperty("REVENUE", oListContext);
	    
	    oList.initialize();
	    
	    oList.requestContexts().then(function (aContexts) {
	        aContexts.forEach(function (oContext) {
	            var data = oContext.getObject(); 
	        });
	    });

Per my understanding the oModel.bindProperty("WEEK_START_DATE", oListContext); needs the list-binding's context at that point in time, to tell that this binding is relative to the list. But its value is undefined at that point in time as oList.getContext() returns undefined and there are no other suitable methods. In the first example the bindContext().getBoundContext() provided that...

@uhlmannm
Copy link
Member

Hi Werner,

let me paste my examples using the TripPin service and the change that we are preparing to fix that $select and $expand are not taken into account if autoExpandSelect is used..

Context Binding

oContextBinding = oModel.bindContext("/People('angelhuffman')", undefined, {
	$select: ["UserName", "FirstName", "LastName"]
});
oContextBinding.requestObject().then(function (oObject) {
	// process response
},
function (oError) {
});

Resulting request: GET People('angelhuffman')?$select=FirstName,LastName,UserName

List Binding

oListBinding = oModel.bindList("/People", undefined, undefined, undefined, {
	$select: ["UserName", "FirstName", "LastName"]
});
oListBinding.requestContexts(0,100).then(function (aContexts) {
	var aData = aContexts.map(oContext => oContext.getObject());
	oJSONModel.setProperty("/People", aData);
},
function (oError) {
});

Resulting request: GET People?$select=FirstName,LastName,UserName&$skip=0&$top=100

The creation of the property bindings as in your example is possible and will also work. But it adds a lot of complication and hence must not be necessary when using requestContexts or requestObject.
For the context binding it is straightforward to create the property bindings relative to the bound context of the context binding. For the list binding the situation is more complex and it is not possible to perform this by using only public methods. The property bindings have to be relative to a specific row context, the so-called virtual context. This is part of the orchestration of how the table/list can create the property bindings to tell the list binding about the required properties before the first data request is fired. Effectively, you would need to repeat what the table does.

As written above, we are preparing a change by which the provided $select parameter is taken into account to fix the issue you have observed. Will it be sufficient for you if we fix this in the next version?

Best regards
Mathias.

@wernerdaehn
Copy link
Author

Absolutely, Mathias.

openui5bot pushed a commit that referenced this issue Jun 2, 2020
…, $select and no children

PS1: integration test & POC
PS2: improved POC
PS3: TDD
PS4: Github reference
PS5-6: review comments

Change-Id: I2532f45a3878fc0ec1b2f9261101e37716898e7c
BCP: 2070245603
Fixes: #2766
@wernerdaehn
Copy link
Author

And here an example I used. Super simple to use, this oData V4!

This is the desired oData query:

http://localhost:8080/HanaAppContainer/protected/odata/WEBPROJEKT/V_SKITRACKS_DISTANCE/TABLE?$skip=10&$top=20

var oModelDistance = new sap.ui.model.odata.v4.ODataModel( {
  "groupId": "$direct",
  synchronizationMode : "None",
  "autoExpandSelect": true,
  "operationMode": "Server",
  serviceUrl : "/HanaAppContainer/protected/odata/WEBPROJEKT/V_SKITRACKS_DISTANCE/"
} );

var oDistanceList = oModelDistance.bindList("/TABLE");

var oDinstanceChart = this.getView().byId("distancechart")
oDistanceList.requestContexts(10, 20).then(function (aContexts) {
  var aLabels = [];
  var aData = [];
  aContexts.forEach(function (oContext) {
    aLabels.push(oContext.getProperty("SUBSEGMENTSTARTTS"));
    aData.push(oContext.getProperty("DISTANCE"));
  });
  oDinstanceChart.setModel( new JSONModel({
    labels: aLabels,
    datasets: [{ data: aData }]
  }));
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants