SAP / openui5

OpenUI5 lets you build enterprise-ready web applications, responsive to all devices, running on almost any browser of your choice.
http://openui5.org
Apache License 2.0
2.96k stars 1.24k forks source link

Usage of OpenUI5 OData V4 model API from Javascript #2354

Closed cjohn001 closed 5 years ago

cjohn001 commented 5 years ago

This issue is a follow up from the discussion at https://github.com/SAP/openui5/issues/2288#issuecomment-452228266.

to dig deeper into the usage of the odata v4 model API from Javascript. Thanks for your feedback Thomas. Ok I think I need to give you a deeper look into what I am trying to implement and how the data in the backend look like. I will show 2 examples, one for a direct context binding which is used in 2 different views, and another one for the list binding which is used in 3 different views. I assume this covers most of the story.

For both examples, let's say I have a database with 3 tables.

Table 1 named "Profile" with columns:

Table 2 named "ConfigOptions" with columns:

Table 3 named "BodyWeight" with columns:


  1. The first example is a direct context binding.

View 1 called ConfgView provides configuration options for ConfigOptions(ProfileID=x). The ConfigOptions context for the user is directly bound to the layout which holds all controls. The view looks as follows:

bildschirmfoto 2019-01-08 um 16 03 31

View 2 called ProfileView is bound to Profile(ProfileID=x) and for this example shall display the BodyHeight of the user in the correctly computed weight unit. The view looks as follows

bildschirmfoto 2019-01-08 um 16 11 02

As you can see from the image, for the BodyHeight Input control I need information from 2 tables. Profile.BodyHeight for control.value and ConfigOptions.MUnitLength for control.description. Furthermore, to recompute the value for the selected unit I have written 2 customTypes, one for cm and one for inches. As I can only bind one path to the Input control at a time if I want to be able to read and write on the input control, I decided to use the ConfigOptions structure in javascript to manually update the Input control when needed. Therefore in the "onRoutePatternMatched" function I check if MUnitLength has changed in comparison to the last set configuration of the BodyHeight control. In case it changed I update the control. Code looks as follows:

            // update input field for body height
            gConfOptsContextBinding.getBoundContext().requestObject("MUnitLength")
                .then(function (strLengthUnit) {
                    if (oBodyHeightInput.getSelectedKey() !== strLengthUnit) {
                        // we use the selectedKey attribute to hold current control state
                        oBodyHeightInput.setSelectedKey(strLengthUnit);
                        if (strLengthUnit === "cm") {
                            // display adequate length units    
                            oBodyHeightInput.setDescription(this.translateText("strCm"));
                            oBodyHeightInput.bindProperty("value", {
                                path: "BodyHeight",
                                type: "mnd.model.MUnitLengthTypeCM"
                            });
                            // if control is not configured for selected unit type, do it 
                            oBodyHeightInput.setType("Number");
                        } else {
                            // display adequate length units    
                            oBodyHeightInput.setDescription(this.translateText("strFt_In_WideFormat"));
                            oBodyHeightInput.bindProperty("value", {
                                path: "BodyHeight",
                                type: "mnd.model.MUnitLengthTypeIN"
                            });
                        }
                    }
                }.bind(this), function (oResult) {
                    console.error("Profile error:" + oResult);
                });

I have tried to use the same gConfOptsContextBinding object which I use in ProfileView in Javascript, to bind it to the layout of the Controls in the ConfigView, i.e.

oVertLayout.setBindingContext(gConfOptsContextBinding.getBoundContext());

However, this does not work. When I try to change one of the values I get the following error:

2019-01-08 16:41:36.976899 Failed to update path /ConfigOptions(38)/MUnitLength - Error: Cannot set value on this binding at http://localhost:9001/resources/sap/ui/core/library-preload.js:3610:496 at http://localhost:9001/resources/sap/ui/core/library-preload.js:252:173 at c (http://localhost:9001/resources/sap/ui/core/library-preload.js:247:99) at new S (http://localhost:9001/resources/sap/ui/core/library-preload.js:249:773) at S.then (http://localhost:9001/resources/sap/ui/core/library-preload.js:252:151) at constructor.O.setValue (http://localhost:9001/resources/sap/ui/core/library-preload.js:3610:471) at constructor.P._setBoundValue (http://localhost:9001/resources/sap-ui-core.js:1655:219) at constructor.P.setExternalValue (http://localhost:9001/resources/sap-ui-core.js:1661:268) at f.j.updateModelProperty (http://localhost:9001/resources/sap-ui-core.js:385:229) at f.j.setProperty (http://localhost:9001/resources/sap-ui-core.js:341:269) sap.ui.model.odata.v4.ODataPropertyBinding

I thought that might be as I cannot reuse the same binding context and resulted in my approach to use oVertLayout.bindElement("/ConfigOptions(" + gProfileID + ")"); and do a gConfOptsContextBinding.refresh(); when a config option was changed, to push the change into the gConfOptsContextBinding binding context. So Thomas from your reply, it looks like this context sharing should work? Maybe I found a bug in OpenUI5 then?


The second example is related to the required use of a list binding from Javascript. Here I have three views. Again the ProfileView from the last example, which shows body weight in lb or kg. Again implemented with two customTypes one for lb and one for kg. Here is the view.

bildschirmfoto 2019-01-08 um 16 54 36

The table for the Body Weight just shows a single cell with an Input Control in a CustomListItem and should always show the most recent measurement. Hence it needs to get updated once a new measurement was added. The binding is given as

items="{path: '/BodyWeight', length:1, sorter: { path: 'WeightMeasurementDateTime', descending: true}}"

My choice here for a table instead of a single input control to show the most recent body weight measurement was more or less thought as a workaround, as I do not know how I could update the Input field, once BodyWeight measurements were added in a different view. My hope was that I could use the same ListBinding on different tables. This reflects probably your thinking if I correctly interpret your answer.

The second view is a dialog which opens once you press the + Button next to the Weight Measurement input field. Here one can add a new record. The Dialog looks as follows and again does Input Validation and formating with customTypes for the Input control.

bildschirmfoto 2019-01-08 um 17 06 14

The third view is now the BodyWeightView which shows a table with all body weight records and later also should show graphs with the data. It also allows to delete records.

bildschirmfoto 2019-01-08 um 17 08 18

Regarding your answer and the idea with getContexts(0, Infinity) . I do not like to load all data. There could be several hundred Weight measurements in the database, which I do not want to get loaded upfront. Only on request. A single List would get to slow if it shows all measurements, as the app should run on a mobile using Cordova, and I do not want to create more traffic than is required.

So for me now three questions are arising:

-control. getBindingContext().submitBatch("auto")... true, forgotten the getModel() when writing the issue text. It is there in code :)

-And then I thought "auto" looks a bit misleading. There is "$auto" which means you need not call submitBatch(), I tried to call submitBatch("$auto") which gave an error and I was thinking I do have to call submitBatch without dollar. Well it does what it needs, but just by luck. You are right, I unintentionally created a batch group with it. So what I have to ensure with my current solution is, that I do not call request object on my second binding before the data on the auto group were send. Is there a way that I can get informed when changes on the auto batch where commited to the server?

Thanks a lot!! The topic is not that easy for me :)

ThomasChadzelek commented 5 years ago

Wow, that's a pretty impressive description. I will take some time to study it. Thus I will answer piece by piece.

Regarding submitBatch("auto"): As long as "auto" is not used as a group ID anywhere, there is no need for this submitBatch() call. If the group ID is unused, the batch group is empty and nothing happens. I would recommend you stick with $auto for reading.

"Is there a way that I can get informed when changes on the auto batch where commited to the server?" You can use dataReceived to be notified when the read request comes back with a response.

ThomasChadzelek commented 5 years ago

You say that "for the BodyHeight Input control I need information from 2 tables": I would like to better understand what your OData service looks like. I assume there are entity sets "/Profile", "/ConfigOptions" and "/BodyWeight". Are there also navigation properties like "/Profile('foo')/toConfigOptions" or "/BodyWeight('bar')/toProfile"? These could be used to $expand related tables and get all necessary information with a single request.

ThomasChadzelek commented 5 years ago

"to recompute the value for the selected unit I have written 2 customTypes, one for cm and one for inches": maybe a composite binding would be better in your case. You could create your own CompositeType and use a binding with two parts: oBodyHeightInput.bindProperty("value", {parts: ["BodyHeight", "toConfigOptions/MUnitLength"], type: "mnd.model.MUnitLengthType"}); This way, you need less code because the binding takes care of updates and invokes the type whenever needed. But that relies on navigation properties (see above).

cjohn001 commented 5 years ago

Hello Thomas, thanks. Unfortunately I think data received will not help me here. The order of things I like to implement is 1. ConfigView updates weight unit from kg to lb. 2. Than I need to wait till data were written to server, 3. I call requestObject on the ProfileView to update display with newly set unit.

I just had a look into the error I get when I use the same contextbinding on different controls. Seems, if the contextbinding is initialized from the ConfigView it can read and write. But the contextbinding in the ProfileView then seems to get not updated. I will debug in this furhter. Does the metadata of my service help you?

metadata.txt

cjohn001 commented 5 years ago

Indeed I tried the CompositeType first. But then I found out, that the Input control would have to be read only. I also would like to change values with the control. Hence this ugly solution

ThomasChadzelek commented 5 years ago

"Cannot set value on this binding": this results from the fact that the property binding for "MUnitLenght" was sending its own data request. In this case, we do not support updates. But I think this is not what you intended. I am unsure why this happens.

This BodyHeight you are using: is that a custom control?

cjohn001 commented 5 years ago

not sure how this expand you are describing is working. Can you see from my Metadata if this should work? I need to read about how to use expand, first. Maybe with it the composite type it than would work. When I did it I had to bind with different paths, i.e. calculated filed

https://sapui5.hana.ondemand.com/1.32.5/docs/guide/a2fe8e763014477e87990ff50657a0d0.html

I than found out that these are currently read only

ThomasChadzelek commented 5 years ago

"Unfortunately I think data received will not help me here": I see, you are editing the data. Then patchCompleted might be your friend.

Thanks for the metadata, that helps a lot!

ThomasChadzelek commented 5 years ago

"Indeed I tried the CompositeType first. But then I found out, that the Input control would have to be read only." Why is that? Composite types should support two-way bindings. That's the difference between a type and a formatter and the whole point of using types. What exactly happened when you tried?

Edit: The documentation about "Calculated Fields for Data Binding" looks outdated to me. Before the introduction of composite types, they were indeed read-only.

cjohn001 commented 5 years ago

This BodyHeight you are using: is that a custom control?

No it is not a custom control. I use it as follows, it is a child of a verticalLayout

Input id="idBodyHeightInput" value="{BodyHeight}" width="144px" fieldWidth="73px" valueLiveUpdate="false"

in onInit I set the vertical layout as follows var oVertLayout = this.byId("idProfileVertLayout"); oVertLayout.bindElement("/Profile(" + gProfileID + ")");

cjohn001 commented 5 years ago

"Indeed I tried the CompositeType first. But then I found out, that the Input control would have to be read only." Why is that? Composite types should support two-way bindings. That's the difference between a type and a formatter and the whole point of using types. What exactly happened when you tried?

I cannot remember the exact errors anymore. You say it should work? Then I will retry it again. Does it make sense from a look at my metadata to try the expand idea? Or does it not work with my service?

ThomasChadzelek commented 5 years ago

Your service is full of , which is good :-) It definitely makes sense to understand $expand. That's an important piece in the puzzle of OData. https://ui5.sap.com/#/topic/54e0ddf695af4a6c978472cecb01c64d talks a bit about it, but not much. It seemed too natural to us. You will find information about the OData concept of $expand on the internet. With UI5, you can basically use navigation properties in binding paths and the autoExpandSelect feature will take care of the rest :-)

cjohn001 commented 5 years ago

one more question regarding the composite type. Do you have an example for this? I get the values from the two paths as an array in sValue

formatValue: function (sValue, sInternalType)

I remember, that it was not clear to me what I have to return here. If I just return a single value, how should openui5 know which path to update. I tried it with single value and array as return values. However, I had no success with it.

cjohn001 commented 5 years ago

Ok, than I will look at Expand first and try to rework my code. For the moment than just one more question left. Should it work to share the ContextBinding for read and write access between different Controls? and the same is true for ListBindings? I do not want to scramble my code if this does not have a chance for success :)

cjohn001 commented 5 years ago

Datamodel.txt

So attached you find the record in Profile and ConfigOptions. Should this work with expand, I was trying to get a query together in my browser. But not sure if this was correct. Looks like expnd is some type of join .

ThomasChadzelek commented 5 years ago

"Should it work to share the ContextBinding for read and write access between different Controls?" Hmm, that is a tough question. From the perspective of my team, which created the OData V4 model, I would say yes. But there is a dark side to setBindingContext which we do not fully understand :-( It might create another context binding in between your v4.ODataContextBinding and your control, and if that is not a V4 context binding, the property binding will send its own data request. I have to debug into this to better understand it, sorry.

For list bindings, you cannot share them between controls unless you do not want paging (which is unlikely).

Recommendation: Do not yet try to share bindings, just try to make use of $expand.

ThomasChadzelek commented 5 years ago

Try http://localhost:8080/odata4/svc/my_nutri_diary/Profile(38)?$expand=fkProfileInProfile($expand=ConfigOptions_fkProfileInConfigOptions) to go from profile to account to config options with a single request.

cjohn001 commented 5 years ago

when I follow your link, I do get this error

` TEIID31172

TEIID31172 Could not resolve expressions being compared to a common type excluding character conversions: g1.uuidUser = g0.fkProfile

`

ThomasChadzelek commented 5 years ago

Regarding the composite type: formatValue is called to bring values from the model to the UI; it returns the string shown inside the input field in your case. Use it to compute the right representation of "ft. / inch" or "cm". When the user edits the input field, parseValue is called instead. As an output, it returns an array corresponding to what you got in formatValue as an input. In your case, only the first element would change while the second stays the same (you cannot edit the unit). You get all current values for all binding parts as a parameter. Throw ParseException if needed.

There is also validateValue which you could use to reject negative values or other nonsense which you were able to parse nevertheless. Throw ValidateException to reject.

Does http://localhost:8080/odata4/svc/my_nutri_diary/Profile(38)?$expand=fkProfileInProfile work for you? Sounds like an issue with your server/modeling, not sure. The request URL syntax should be OK according to the spec.

I have to leave now. See you tomorrow!

cjohn001 commented 5 years ago

` TEIID31172

TEIID31172 Could not resolve expressions being compared to a common type excluding character conversions: g1.uuidUser = g0.fkProfile

`

Unfortunately, this does also not work. I assume this is due to the row-based security mechanism and not an error in the statement. I have to try if this works in code with expand. the uuidUser is from keycloak sso and should be added to the header of the http request. Probably this does not exist when I go via a browser link

cjohn001 commented 5 years ago

Ok, thanks for your help. I think I now have homework to do. I will give you feedback once I have tried out the things we have just discussed.

ThomasChadzelek commented 5 years ago

Looks like there is an issue with type mismatch here: vs. . I think that causes the issue: the framework does not know how to compare String vs. Int64.

When trying $expand=fkProfileInProfile, this is the navigation property being used:

I wonder if it is OK to have a referential constraint for idProfile which is not a key property of Account? Is that the reason why uuidUser is used instead for the comparison (it is the only key)?

cjohn001 commented 5 years ago

Hello Thomas, it seems like I hit another Bug in Teiid here which in the meantime was confirmed by one of the developers at Red Hat. I unfortunately do not have more details on it yet.

Regarding your question to uuidUser: The table Account just holds a one to one mapping between uuids like they come from a keycloak SSO server to an integer value idProfile which has a uniqueness constraint set and is also indexed. I have done this in that way, due to performance and storage considerations. I did not want to have a UUID value as a foreign key which is used for most comparison and search operations in all of my tables. I use a mysql database which has no dedicated type for UUIDs and stores it in a char(36) string instead. I am afraid that I will have to wait with further development till I got a bugfix for Teiid. However, maybe I can work around this bug by simply setting the primary key in table Account to idProfile rather than uuidUser. For my application this should not matter as both are unique and indexed and hence qualify as primary key for the table. Thanks for bringing me to this idea. I will come back on you regarding the BindingContext topic, once I have the navigation properties up and running.

Update: I probably hit another bug in the tooling for Teiid and had to manually recreate the required xml database description as the automatic update did not resolve the change of my primary key value properly. Now the expand is working smoothly. I will now have a look at example one described in my previous storyline and replace the BodyHeight calculation with a CompositeType :)

cjohn001 commented 5 years ago

Hello Thomas, the composite type is working now. I put the code in here, for others which might have the same question. However, I have one remaining problem with this control. You can see in the xml below, that I need to change the "type" property of the control from "Text" to "Number" depending on the selected LengthType. Now my Problem is, when I switch from inches display (Text) to cm display (Number), the view does not show the BodyHeight. This is because the order in which the control properties are set is wrong. I can also see this in the debug console:

sap-ui-core.js:2092 The specified value "6 / 10.7" is not a valid number. The value must match to the following regular expression: -?(\d+|\d+.\d+|.\d+)([eE][-+]?\d+)?

Is there a way how I could change the control's type value in a deferred manner instead?

Binding from xml:

<Input id="idBodyHeightInput" value="{parts: [{path: 'BodyHeight', type: 'sap.ui.model.type.Float'},
      {path: 'fkProfileToAccount/ConfigOptions_fkConfigOptionsToAccount/MUnitLength', 
                  type: 'sap.ui.model.type.String'}], 
        type: 'mnd.model.MUnitLengthType' }" 
        description="{fkProfileToAccount/ConfigOptions_fkConfigOptionsToAccount/MUnitLength}" 
        selectedKey="{fkProfileToAccount/ConfigOptions_fkConfigOptionsToAccount/MUnitLength}"
          type="{= ${fkProfileToAccount/ConfigOptions_fkConfigOptionsToAccount/MUnitLength} === 
                           'cm' ? 'Number' : 'Text'}" valueLiveUpdate="false"/>

Code for the CompositeType: MUnitLengthType.txt


Ok, now we are back at the initial topic. I would say we go step be step and start with my first example of a single context for the BodyWeight which is changed from two views. So for my understanding is, I will have to go with two separate BindingContexts in the two views which change my BodyWeight value. The code of the first one is the just presented xml. The second one will be a SegmentedButton in a second view which allows switching between cm and inches. To my understanding, having two ContextBindings means I cannot sync between them with "patchCompleted" event, as this is just signaled for the binding context which submitted a patch and not for all BindingContexts that are attached to a path. Is this correct?

So the best solution I currently came up with is to hook onto the selectionChange event of the Segmented Button in View2 and in the eventHandler store the newly set value. I could than check in the onRoutePatternMatched event of View1 if the state of the value has changed and call

bodyHeightInput.getBindingContext().refresh();

Well, this approach denotes, that I

  1. have to store the state changes of each Control that is visible in more than a single view, and
  2. with the call to .refresh() I will send a http request to fetch data that are already present in the data model of my client, just for the bindingContext to update the values displayed in the ui.

Now the great question, Thomas, do you have a better idea, how I could do things here?

cjohn001 commented 5 years ago

Hello Thomas, I am now trying to implement the approach like mentioned above. I was playing around with the context bindings today to use them from javascript. It looks like the advantage I would get out of using the bindingcontexts rather then plain ajax calls for the stuff I have to do from Javascript, would be, that I get the batch processing for free. Hence I am looking what I can to with it.

My question is now related to the following snippet of code:

var oModel = sap.ui.getCore().getModel(); var strFilter = "uuidUser eq " + "'" + gKeycloak.subject + "'"; var oConfOptsContextBinding = oModel.bindContext("/Account", undefined, { "$filter": strFilter, "$expand": { "ConfigOptions_fkConfigOptionsToAccount": { "$select": ["MUnitWeight", "MUnitLength"] } } }); var oPromise = oConfOptsContextBinding.getBoundContext().requestObject(); oPromise.then(function (oResult) { var test = oResult; }.bind(this)); return oPromise;

Why is the expand not working here? I also tried the expand without select statement. Also without success. The relevant expand part is never added to the HTTP GET request. In the debugger, I see that the filter statement in the query is correctly added:

GET Account?$filter=uuidUser%20eq%20'65820574-7525-463b-9d20-5ee133a06b94' HTTP/1.1

I tried switching the orders of the parameters as well. Also without success.

Is there maybe a piece of code in the OpenUI5 sources where I can see how the ContextBinding and ListBinding is used from the controls. I was searching something like this for half a day without success. When using the ContextBinding on a collection path, it looks like I get back the entire collection in a ContextBinding. Hence my current thinking is, that I just need this single entity to do all my stuff from Javascript. You mentioned in one of your last replies, that there is nothing like a requestObject for a ListBinding. Well, as the ContextBinding seems to be the vehicle for collections and single item contexts, it is now clear why.

cjohn001 commented 5 years ago

Creating a new entity from Javascript:

    var oListBinding = oModel.bindList("/Account");
    var oContext = oListBinding.create({
        "uuidUser": gKeycloak.subject,
        "idProfile": "0"
    }, true);

Hello Thomas, the following code is working from me. However, what is not working is, if I set bSkipRefresh = false. In this case I see the following error

2019-01-11 17:29:20.211899 Failed to refresh entity: /Account/-1[-1|transient] - Error: 404 Not Found library-preload.js:3709 Uncaught (in promise) TypeError: Cannot read property '@$ui5._' of undefined

The http message that was send is GET Account(0) HTTP/1.1

The explanation I have for it is the following. If I want to create a new entry I need to set idProfile = 0 as my odata server requires the idProfile parameter to be set to this value when creating a new entry. The column idProfile is actually autoincrement in the db, and is set to a valid value when the object is created in the database. The Get Account(0) therefore has to fail. 0 will always be the wrong id. Now my question is, is there something in the Odata specification which allows for a Get on an invalid url to request the last item? I am wondering if this is a bug in OpenUI5 or in my Teiid server. I would expect that a refresh on a newly create item can never be done if the primary keys are not generated client side. I am wondering if this bSkipRefresh option is only thought for this uncommon case?

ThomasChadzelek commented 5 years ago

Regarding https://github.com/SAP/openui5/issues/2354#issuecomment-452939097: I would first try to avoid different bindings. Can't you have a single context binding to the current profile, say in your component, and sets its bound context as a binding context for the two views? That single binding could well expand "fkProfileToAccount/ConfigOptions_fkConfigOptionsToAccount" fully and then you can edit the very same data that is used for displaying and formatting; no need to sync anything.

ThomasChadzelek commented 5 years ago

Regarding https://github.com/SAP/openui5/issues/2354#issuecomment-453578209 (bSkipRefresh = false): What does the response to the POST request look like? We assume it does not contain the key properties and that is why the GET says Account(0) instead of s.th. more useful.

cjohn001 commented 5 years ago

Hello Thomas, I did try this, but using the same context binding in two different views crashes as soon as the second binding gets active. I therefore do the syncing now via a custom data structure. Not ideal, but after all this tries I tought i need to do something to make progress.

Regarding https://github.com/SAP/openui5/issues/2354#issuecomment-453314576 I probably found the answer. My model has auto-expand activated, seems like I am not allowed to set expand or select statements on the model at this case. Is it allowed to have two models to the same data service in a single application. Might be that this would be an option to work around it. I have not tried so far but solved it by just binding to the single property which I required for my use case. Hence I could solve it without using expand.

It would be very great, if you could add a description on how to use the binding interface to the SAPUI5 documentation in future. The query interface is impossible to understand if one has to use it from javascript.

cjohn001 commented 5 years ago

Regarding #2354 (comment) (bSkipRefresh = false): Update, sorry wrong message. The request:

POST Account HTTP/1.1 Accept:application/json;odata.metadata=minimal;IEEE754Compatible=true Accept-Language:de-DE Content-Type:application/json;charset=UTF-8;IEEE754Compatible=true

{"uuidUser":"65820574-7525-463b-9d20-5ee133a06b94","idProfile":"0"} --batch_id-1547485733072-21 Content-Type:application/http Content-Transfer-Encoding:binary

The response:

HTTP/1.1 201 Created Location: http://localhost/odata4/svc/my_nutri_diary/Account(0) Content-Type: application/json;ieee754compatible=true;odata.metadata=minimal Content-Length: 147

{"@odata.context":"http://localhost/odata4/svc/my_nutri_diary/$metadata#Account","idProfile":"0","uuidUser":"65820574-7525-463b-9d20-5ee133a06b94"} --batch_e834eb7c-df0d-4e22-a423-78cd3638955f Content-Type: application/http Content-Transfer-Encoding: binary

The server does not respond with the newly created ID. Is this a server side bug? As a matter of principle, I do not have a different choice here than sending Account(0) Mysql expects the primary key to be not null and wants the 0 provided

uhlmannm commented 5 years ago

Hi,

I am a colleague of Thomas.

Regarding #2354 (comment) (bSkipRefresh = false): The request:

{"@odata.context":"http://localhost/odata4/svc/my_nutri_diary/$metadata#Account","idProfile":"0","uuidUser":"65820574-7525-463b-9d20-5ee133a06b94"} --batch_e834eb7c-df0d-4e22-a423-78cd3638955f Content-Type: application/http Content-Transfer-Encoding: binary

The response:

HTTP/1.1 201 Created Location: http://localhost/odata4/svc/my_nutri_diary/Account(0) Content-Type: application/json;ieee754compatible=true;odata.metadata=minimal Content-Length: 147

{"@odata.context":"http://localhost/odata4/svc/my_nutri_diary/$metadata#Account","idProfile":"0","uuidUser":"65820574-7525-463b-9d20-5ee133a06b94"} --batch_e834eb7c-df0d-4e22-a423-78cd3638955f Content-Type: application/http Content-Transfer-Encoding: binary

The server does not respond with the newly created ID. Is this a server side bug?

Yes. The server needs to send the Id of the newly created entity. The V4 Model will use this Id in subsequent requests.

cjohn001 commented 5 years ago

Hello uhlmannm, ok. thanks. In this case I will fill a bug report!

uhlmannm commented 5 years ago

Hi John,

regarding:

Hello Thomas, I am now trying to implement the approach like mentioned above. I was playing around with the context bindings today to use them from javascript. It looks like the advantage I would get out of using the bindingcontexts rather then plain ajax calls for the stuff I have to do from Javascript, would be, that I get the batch processing for free. Hence I am looking what I can to with it.

My question is now related to the following snippet of code:

var oModel = sap.ui.getCore().getModel(); var strFilter = "uuidUser eq " + "'" + gKeycloak.subject + "'"; var oConfOptsContextBinding = oModel.bindContext("/Account", undefined, { "$filter": strFilter, "$expand": { "ConfigOptions_fkConfigOptionsToAccount": { "$select": ["MUnitWeight", "MUnitLength"] } } }); var oPromise = oConfOptsContextBinding.getBoundContext().requestObject(); oPromise.then(function (oResult) { var test = oResult; }.bind(this)); return oPromise;

Why is the expand not working here? I also tried the expand without select statement. Also without success. The relevant expand part is never added to the HTTP GET request. In the debugger, I see that the filter statement in the query is correctly added:

GET Account?$filter=uuidUser%20eq%20'65820574-7525-463b-9d20-5ee133a06b94' HTTP/1.1

I tried switching the orders of the parameters as well. Also without success.

I have used the following code in my test using the public TripPin-service of OData.org. `

        var oModel = new sap.ui.model.odata.v4.ODataModel({
                groupId : "$auto",
                serviceUrl : "/databinding/proxy/https/services.odata.org/TripPinRESTierService/(S(cl1ihatrwll2ylrxlebkkns2))/",
                synchronizationMode : "None",
                autoExpandSelect : true
            });
    var oContextBinding = oModel.bindContext("/People('russellwhyte')",undefined,{
                $select: ["UserName","FirstName","LastName","Age"],
                $expand: {
                    Friends : {
                        $select: ["UserName","FirstName","LastName","Age"]
                    }
                }
            });
    // Also create the property binding for changing the age
    var oAgeBinding = oModel.bindProperty("Age",oContextBinding.getBoundContext());

` The result request is "GET People('russellwhyte')?$select=Age,FirstName,LastName,UserName&$expand=Friends($select=Age,FirstName,LastName,UserName)". The difference to your binding is that my path points to a specific entity. Hence I also do not need to use $filter.

I also tried your approach: binding to the entityset and using $filter to restrict the result to the entity of interest. The request was "GET People?$filter=UserName%20eq%20'russellwhyte'&$select=Age,FirstName,LastName,UserName&$expand=Friends($select=Age,FirstName,LastName,UserName)". The structure of the result returned by requestObject is wrong, though. In my case I also get drill-down errors caused from the property binding "AgeBinding" which cannot find the "Age". Please use the context binding for binding an entity and the list binding for binding an entityset!

Is there maybe a piece of code in the OpenUI5 sources where I can see how the ContextBinding and ListBinding is used from the controls. I was searching something like this for half a day without success.

No. In particular, there is currently no public method available for reading data through a list binding. We had discussed to provide methods tailored for accessing and modifying backend data in controller code. The internal reference of the requirement is CPOUI5UISERVICESV3-1315.

When using the ContextBinding on a collection path, it looks like I get back the entire collection in a ContextBinding. Hence my current thinking is, that I just need this single entity to do all my stuff from Javascript. You mentioned in one of your last replies, that there is nothing like a requestObject for a ListBinding. Well, as the ContextBinding seems to be the vehicle for collections and single item contexts, it is now clear why.

Although this might do the trick in your test, I do not recommend to use a context binding for binding an entity set/list.

Best regards Mathias.

cjohn001 commented 5 years ago

Hello Mathias, thanks for the code snippet. Indeed the use of the context binding on an entity set also does not do for me like what I expected. It just provides a list of contexts but does no filtering. Currently, I was only able to use the list binding for creation of new elements. Calling getContexts() in the list binding seems not to deliver any data. Even though I call it a second time in change event. Could you maybe show how I could implement the following path with a list binding?

oModel.sServiceUrl + "/BodyWeight" + "?$filter=fkProfile%20eq%20" + iProfileID + "&$orderby=WeightMeasurementDateTime%20desc&$top=1";

Another question I came up with. I have red somewhere that sapui5 internally uses odatajs. Could I use this library in javacode instead and the commands would be placed into batches with the requests send from the ui bindings? Or are these batch groups additional logic which is provided by sapui5 only?

And the last question hopefully :) Currently, it does not work for me to use the same context binding in two different views. When I do changes in view 1 I save the information that there was a change in a custom data structure and if I again go into a view 2 which shows the same data, I do a refresh on the context there to redraw the ui control. What happens is, that the refresh creates a new http request and loads the same data as are already available on the client. Seems, the ui controls hold a copy of the data in the odata model. Is there a why to do a kind of refresh which just forces the ui controls to refresh their data by reading it from the data model, rather than doing a refresh on the model which involves an additional http request?

uhlmannm commented 5 years ago

Hi!

Hello Mathias, thanks for the code snippet. Indeed the use of the context binding on an entity set also does not do for me like what I expected. It just provides a list of contexts but does no filtering.

Did the backend do the requested filtering?

Currently, I was only able to use the list binding for creation of new elements. Calling getContexts() in the list binding seems not to deliver any data. Even though I call it a second time in change event. Could you maybe show how I could implement the following path with a list binding?

oModel.sServiceUrl + "/BodyWeight" + "?$filter=fkProfile%20eq%20" + iProfileID + "&$orderby=WeightMeasurementDateTime%20desc&$top=1";

First of all, sap.ui.model.odata.v4.ODataListBinding.getContexts is protected. This method is used by controls for requesting data from the list binding. Protected also means that we may do incompatible changes. The general procedure you laid out is correct. The first call of getContexts() will trigger a request for the missing entities. A change event is raised when they are available. In the event handler a second getContexts() call will provide the contexts.

You would bind the list as follows. This example code uses methods filter and sort of the the list binding with sap.ui.model.Filter and sap.ui.model.Sorter objects as input. Note that system query option $top is handled by the list binding itself and must not be provided.

  var oWeightList = oModel.bindList("/BodyWeight");
  oWeightList.sort( new Sorter("WeightMeasurementDateTime",true) );
  oWeightList.filter( new Filter({
    path: "fkProfile",
    operator: FilterOperator.EQ,
    value1: iProfileID,
  });

Reading the data follows the orchestration that you have already sketched.

  oWeightList.attachChange(function(){
    aContexts = oWeightList.getContext(0,1);
    // Do whatever is required with aContexts 
  });
  // trigger the request if the data are not yet there
  aContexts = oWeightList.getContext(0,1); 
  if (aContexts.length){
    // Do whatever is required with aContexts 
  }

The better solution would be to use a designated API for fetching the data of a list binding. As written yesterday, the requirement to provide this API is logged in CPOUI5UISERVICESV3-1315. EDIT 17.09.19 As of SAPUI5 1.70, method v4.ODataListBinding.requestContexts is available. Protected method getContexts should not be used anymore in controller code. The code above then simplifies to:

oWeightList.requestContexts(0,1).then(function (aContexts){; 
  // Do whatever is required with aContexts 
});

Another question I came up with. I have red somewhere that sapui5 internally uses odatajs. Could I use this library in javacode instead and the commands would be placed into batches with the requests send from the ui bindings? Or are these batch groups additional logic which is provided by sapui5 only?

The V2 ODataModel uses datajs. The V4 model does not. You could use other libraries than UI5 for reading and writing data. But I am not sure whether I understand the purpose of doing that. Could you please explain?

The V4 model and its bindings provide an out of the box integration with UI5 controls for displaying and changing data. There is batch request handling, ETag, x-csrf token handling, grouping/consolidating of Patches, etc.

And the last question hopefully :) Currently, it does not work for me to use the same context binding in two different views. When I do changes in view 1 I save the information that there was a change in a custom data structure and if I again go into a view 2 which shows the same data, I do a refresh on the context there to redraw the ui control. What happens is, that the refresh creates a new http request and loads the same data as are already available on the client. Seems, the ui controls hold a copy of the data in the odata model. Is there a why to do a kind of refresh which just forces the ui controls to refresh their data by reading it from the data model, rather than doing a refresh on the model which involves an additional http request?

There is very limited support for synchronization of data between bindings (that raise their own requests). (Very limited == In special cases we synchronize the result of a bound operation call back to the binding parameter.) Data reuse is possible for relative bindings. The relative binding may use the data of its parent binding. If time permits I will provide a small example later.

EDIT 17.09.19 As of SAPUI5 1.70, method v4.ODataListBinding.requestContexts is available. Protected method getContexts should not be used anymore in controller code.

cjohn001 commented 5 years ago

Hello Mathias, thanks for the examples. I will try them out tonight.

Did the backend do the requested filtering?

I have not had a look into the server logs as the filtering based on lists in UI controls was working with the same filters set. However, as I understood you, this should not matter to much, as I should not use a context for collection paths anyway.

The better solution would be to use a designated API for fetching the data of a list binding. As written yesterday, the requirement to provide this API is logged in CPOUI5UISERVICESV3-1315.

Well, I assume I will not have the option to wait till this is implemented :) Hence, for the moment I will try to go with the solution you have sketched.

You could use other libraries than UI5 for reading and writing data. But I am not sure whether I understand the purpose of doing that. Could you please explain?

I am not sure what I should explain here. I suppose you mean for what use cases? So in general I have things to do on the database which are not triggered by user interaction. And for this I require access from Javascript directly. For example, if I set up a new user account from my SPA, than the init scripts should be able to detect if data structures for account or user data are already available, if not the scripts need to set them up. There are other things like diagnosis or analytics data I would like to store in the backend. For these things there is also no interoperability with UI controls given. A further point for me, why I like to access the datamodel directly, is that I do not want to take an additional api and request data for a second time, which are already available in the openui5 datamodel on my client. This is due to overhead but also due to performance considerations. I would like to implement a mobile app with openui5 where I have to expect slow connectivity. Hence I do not want to have unnecessary backend calls in my code. Finally, the point why I asked for a different API was simply as there seems to be no adequate javascript API available yet for the OpenUI5 odata v4 model. You mentioned yourself, that this is a future topic covered by CPOUI5UISERVICESV3-1315. I do not have access to this ticket. But as far as I understand, if I do not like to wait for another year to start coding, it is no option to wait on the resolution of this ticket.

Data reuse is possible for relative bindings. The relative binding may use the data of its parent binding. If time permits I will provide a small example later.

Would be great if you could provide an example for this architecture suggestion. I thought about it, but did not choose this path, as my thinking was, that I cannot have multiple relative bindings on the same control at the same time and need to have different bindings for different tables in the database. However, might be that my thinking was wrong. Since I understood the expand feature a little better,I think, it might be that I just need one absolute binding and can use expand to access all the different tables given they are in the same database, rather than creating absolute bindings for different tables. Like I mentioned, I could not get the same absolute binding working for controls on different views. The control which was the second one accessing the binding always crashed. I think it was not on reads, but writes. But it is a while ago when I tested this. Though if you could show how your architecture suggestion, having one absolute binding on top and only relative bindings for controls in different views could be made to work, this would be of great help. Maybe it would also be good to add such architecture considerations in the OpenUI5 documentation of the v4 odata model. Thanks for your help!

Best regards, Christoph

uhlmannm commented 5 years ago

Hi Christoph,

sorry for the delay!

The better solution would be to use a designated API for fetching the data of a list binding. As written yesterday, the requirement to provide this API is logged in CPOUI5UISERVICESV3-1315.

Well, I assume I will not have the option to wait till this is implemented :) Hence, for the moment I will try to go with the solution you have sketched.

As usual I cannot promise anything. But I can say that at the moment it looks as if our priorities will be different in the next few months.

You could use other libraries than UI5 for reading and writing data. But I am not sure whether I understand the purpose of doing that. Could you please explain?

I am not sure what I should explain here. I suppose you mean for what use cases? So in general I have things to do on the database which are not triggered by user interaction. And for this I require access from Javascript directly. For example, if I set up a new user account from my SPA, than the init scripts should be able to detect if data structures for account or user data are already available, if not the scripts need to set them up. There are other things like diagnosis or analytics data I would like to store in the backend. For these things there is also no interoperability with UI controls given.

You could try to model this using actions, maybe bound actions.

A further point for me, why I like to access the datamodel directly, is that I do not want to take an additional api and request data for a second time, which are already available in the openui5 datamodel on my client. This is due to overhead but also due to performance considerations. I would like to implement a mobile app with openui5 where I have to expect slow connectivity. Hence I do not want to have unnecessary backend calls in my code.

I understand that point. But I recommend data reuse using binding hierarchies as much as possible.

Finally, the point why I asked for a different API was simply as there seems to be no adequate javascript API available yet for the OpenUI5 odata v4 model. You mentioned yourself, that this is a future topic covered by CPOUI5UISERVICESV3-1315. I do not have access to this ticket. But as far as I understand, if I do not like to wait for another year to start coding, it is no option to wait on the resolution of this ticket.

Data reuse is possible for relative bindings. The relative binding may use the data of its parent binding. If time permits I will provide a small example later.

Would be great if you could provide an example for this architecture suggestion. I thought about it, but did not choose this path, as my thinking was, that I cannot have multiple relative bindings on the same control at the same time and need to have different bindings for different tables in the database. However, might be that my thinking was wrong. Since I understood the expand feature a little better,I think, it might be that I just need one absolute binding and can use expand to access all the different tables given they are in the same database, rather than creating absolute bindings for different tables. Like I mentioned, I could not get the same absolute binding working for controls on different views. The control which was the second one accessing the binding always crashed. I think it was not on reads, but writes. But it is a while ago when I tested this. Though if you could show how your architecture suggestion, having one absolute binding on top and only relative bindings for controls in different views could be made to work, this would be of great help. Maybe it would also be good to add such architecture considerations in the OpenUI5 documentation of the v4 odata model. Thanks for your help!

Let me sketch this using a test application of mine. The application has a main view which lists sales orders in a sap.m.Table. The user may either select an existing order and go the object page of that order to edit it or the user may create a new sales order. Let us focus on the first case.

I have added $select and $expand manually to my list binding although I use the auto-$expand/$select feature in this example.

                <Table id="SalesOrderTable" width="auto"
                    items="{ 
                            path: '/SalesOrderList',
                            parameters: {
                                $select: ['ChangedAt','CreatedAt','Note','SalesOrderID'],
                                $expand: {
                                    SO_2_BP : {
                                        $select: 'Address/City,Address/PostalCode,BusinessPartnerID,CompanyName,PhoneNumber'
                                    }
                                },
                                $orderby: 'SalesOrderID desc',
                                $count: true
                            }
                        }"
                    noDataText="{i18n>tableNoDataText}" busyIndicatorDelay="0" growing="true" growingScrollToLoad="true"
                    updateFinished="onUpdateFinished">
                    <headerToolbar>

When navigating to the object page, the selected context is handed over using a JSON model, here called ui.

        onPress: function(oEvent) {
            var oItem = oEvent.getSource(),
                oUIModel = this.getView().getModel("ui");

// Store context ...
            oUIModel.setProperty("/oContext",oItem.getBindingContext());

            sap.ui.core.UIComponent.getRouterFor(this).navTo("edit", {
                SalesOrderID: oItem.getBindingContext().getProperty("SalesOrderID",true),
                SalesOrderPath: oItem.getBindingContext().getPath()
            });

        }

On the object page, the context is set in the _onRouteMatched method.

            var oContext = oUIModel.getProperty("/oContext");

            if (oContext){
                this.byId("page").setBindingContext(oContext);
            } else {
                var sPath = "/SalesOrderList(\'" + sSalesOrderId + "\')";
                this.byId("page").setBindingContext(oModel.bindContext(sPath).getBoundContext());
            }           

The object page itself consists of two parts. The upper part contains a simple form showing data of the order and the business partner. This part can reuse the data of the sales order list binding. The second part is a list binding showing the items. This binding cannot reuse the data of the sales order list binding because they are not requested with the sales order list binding. (Note that it would be possible to also expand and thus read the items on the main page. However, as the V4 model cannot do paging within $expand so far, this would not meet your requirement to not load all weight measurements at once.)

Now the effect of this is that when entering the object page for a specific sales order for the first time, only the item data are requested from the backend. Any changes done on the object page are reflected automatically in the sales order list without the need to re-read data. When entering the object page for a specific sales order for the second time or later, no requests need to be triggered because the item list binding still caches the data. It is also possible to delete a sales order on its object page. The application navigates back to the main page when the deletion was successful. A refresh of the list is also not required as the context of the list was deleted. The situation is different with the creation of a new sales order though. Here the V4 model still has the restriction that the list binding needs to be refreshed before creating a second new entity.

What does this mean for your application? You can put a context binding on top of your binding hierarchy. This context binding will contain (and request) all data processed by dependent context bindings. Then you have two list bindings to the weight measurements. These cannot reuse data from the parent context binding as you want to use paging. The straightforward implementation would use two relative list bindings that raise their own data requests. This implies, unfortunately, that each list binding needs to be refreshed when the respective view is opened and there had been changes on the other list binding.

Best regards Mathias.

uhlmannm commented 5 years ago

Hi Christoph (@cjohn001),

was this helpful? Are there further questions or could we close the issue?

Best regards Mathias.

cjohn001 commented 5 years ago

Hello Mahtias, sorry for the late reply and thanks a lot for your detailed answer. I have not had much time to look into it recently. Yes, it clarified things. For the moment I think we can close the issue. In case I get further questions I would open a new ticket. Well, I assume, this means that my current approach was more or less correct. I would have one further question indeed. Are there plans in the near future to extend openui5 be means of a different synchronizationMode" for odata4 rather than "None" to get a similar syncing support between ui controls like for odata2? If not this would definitely be a great wish for a new feature request, as this manual approach on keeping views in sync makes development error-prone and cumbersome. Thanks again.

Best regards, Christoph

uhlmannm commented 5 years ago

Hi Christoph,

yes, synchronization is a topic that was already discussed. The requirement is logged as CPOUI5UISERVICESV3-127 in our backlog. For the near future, it does currently not look as if we will have time to work on the synchronization. For your case, it would also be helpful if you could get the two list bindings to share the same data. Unfortunately, also this is nothing that we could implement in the immediate future. I have logged this as CPOUI5UISERVICESV3-1724.

Best regards Mathias.

cjohn001 commented 5 years ago

@uhlmannm @ThomasChadzelek Hello Mathias and Thomas, I would like to come back on you regarding the topic, to reuse data based on a single context with relative bindings to this context.

I am currently trying to post data to a collection via a relative binding. Unfortunately, I always get an error saying

{"error":{"code":null,"message":"HTTP method 'POST' not allowed for this resource."}}

I have bound the root view to .../Account(1) from which I have navigation bindings to each path I need. Currently I have trouble to post to collections via this binding.

So it might be that my path to the collection is wrong or that my backend does not support POSTs via navigation properties. Might you tell me if my path via the navigation property should work for post requests?

Directly writing to the collection is fine, i.e. something like the following:

curl --user sap:sap -i -X POST -H 'Content-Type: application/json;charset=UTF-8;IEEE754Compatible=true' -d '{"fkProfile":"1","idCode":null,"lc":"de","product_name":"A Product","brands":"A brand","energy_100g":2259,"carbohydrates_100g":60.08,"sugars_100g":10.1,"proteins_100g":24.03,"fat_100g":15.89,"saturated_fat_100g":10.0,"salt_100g":1.0}' http://localhost:18080/odata4/svc/my_nutri_diary/UserDefinedProducts

and a GET request via the following navigation property path also works, but not when using the path in a POST request. I.e. when going via the navigation property via

curl --user sap:sap -i -X POST -H 'Content-Type: application/json;charset=UTF-8;IEEE754Compatible=true' -d '{"fkProfile":"1","idCode":null,"lc":"de","product_name":"A Product","brands":"A brand","energy_100g":2259,"carbohydrates_100g":60.08,"sugars_100g":10.1,"proteins_100g":24.03,"fat_100g":15.89,"saturated_fat_100g":10.0,"salt_100g":1.0}' http://localhost:18080/odata4/svc/my_nutri_diary/Account\(1\)/UserDefinedProducts_fKUserDefinedProductToAccount

it does not work.

Here you find the backend metadata:

https://ecubed-solutions.ddnss.de/odata4/svc/my_nutri_diary/$metadata user: sap password: sap

Thanks a lot for your help! In case the path is ok, is there some other way I could reuse data from a bound context when using POSTS?

In the meantime I think my problem boils down to the fact that I am missing some information. If I bind my root control to an entity context, it seems I am not able to use navigation properties to relatively bind to a collection via a list binding and in case I use a list binding it seems I cannot use relative bindings to bind to an item. Could you maybe explain me how the syntax for this would look like in order to reuse data from a single context in all of my views? And moreover, if I should use a list binding or entity binding as the root context?

Best regards, Christoph

pksinsik commented 5 years ago

Hello Christoph,

I expect a POST on a collection for a navigation property works / makes sense in case of a containment navigation property. Otherwise, e.g. for Account and Product, where multiple accounts can refer the same product, I expect a POST only makes sense on the EntitySet for the corresponding entity type and you have to set/add a foreign key in the source entity to refer to it; however it is still up to you and your service implementation to accept a POST in such a case to refer to an existing entity. The "clean way" however is modification of the corresponding navigation property. Note: the OData V4 model does not yet support modification of navigation properties via OData "binding information" but only via structural properties holding the foreign keys.

Best regards, Patric

cjohn001 commented 5 years ago

Hello Patrik

I expect a POST only makes sense on the EntitySet for the corresponding entity type and you have to set/add a foreign key in the source entity to refer to it;

I do not understand what you mean. As a matter of principle, the server checks if the user has read or write access to the collection. A post to the UserDefinedProduct makes sense here as on server side it is checked that the users can only add an item with his userid. The scenario is working perfectly fine if I post to the collection directly. However, in respect to the previous discussion with Mathias, I want to reuse a single top level binding. Hence, in order to reuse the existing binding I have to take the way via the navigationproperty. So from your answer it is still not clear to me if the post via the navigation property should work or should not work. If it should not work than the entire story with reuse of a single toplevel binding would be wrong

pksinsik commented 5 years ago

Hello Christoph,

sorry I did not explain better, but maybe I did not get your real question. Maybe it would help to open a new GitHub issue focussing on one concrete question instead of continuing this very general one which covers quite some topics...

My point was regarding the semantics of the navigation property in which you want to create a new entity in the sense of aggregation (1) vs. association (2):

  1. If the navigation property is a containment then you can create entities in this collection only via the navigation property. I assume that "UserDefinedProduct" is a containment, as these seem to be products aggregated by the user and not visible to other users, right?
  2. If the navigation property is not a containment, i.e. the product may also be associated with other users, then creation (POST) via the navigation property does not make sense. You may only (un-)associate the product with this user (e.g. via foreign key), but not create/delete the product itself. This would So in general you cannot POST to each and every navigation property underneath "/Account(1)" but I cannot judge that for your scenario / your service. One additional observation: In your example you mention difficulties when POSTing to UserDefinedProducts_fKUserDefinedProductToAccount - can it be that "fK" in the nav property path stands for "foreign key" and you are in case 2.?

Best regards, Patric

cjohn001 commented 5 years ago

Hello Patric, yes, in the given example I assume we speak about case 1. UserDefinedProduct is associated with the user. Here is a screenshot of the db structure. This might make things more clear.

Bildschirmfoto 2019-08-27 um 17 56 08

It seems I run into a bug in the Apache Olingo (which I am using as a backend) sources. I am currently discussing this in a different forum. However, I am not sure if this is the only problem I run into currently. But I would recommend we delay further discussions regarding the issue till this is solved. I would come back on you if this does not resolve the issue.

Please note, the only reason, I am trying to create items via navigation properties rather than the absolute path is, that I want to have a single root binding. In my mobile app I have multiple views to the same data. In order to keep them in sync I have to use a single root binding as I do not want to reload all data on each page view. Moreover, in case of a temporary offline situation I also want to be able to provide consistent data to the user. This means irrespective of aggregation or association I have to create entities via relative binding paths, hence navigation properties.

cjohn001 commented 5 years ago

Hello Patric @pksinsik , I am still dealing with the topic. As I am currently not able to create a new entity on a navigation path due to a bug in Apache Olingo app (my backend)

https://jira.apache.org/jira/browse/OLINGO-1389

I am currently trying to use an absolute binding for the create commands. Unfortunately, it seems like I cannot refresh a binding on a navigation property. I always get an error from sapui5 that this is not supported. I also tried unbindAggregation followed by bindAggregation to refresh the relevant list items. Unfortunately this also does not update the data in the model. Do you have a hint on how I can refresh the data for the relative path via the navigation property? Thanks for your help.

Best regards, Christoph

pksinsik commented 5 years ago

Hello Christoph,

as for the "refresh" question: This API is currently only allowed for absolute bindings as it reads the data for this binding and its dependent bindings completely anew; there is no "partial" refresh only reading data for some of the bindings. As only absolute bindings are guaranteed to send their own data request, refresh is only allowed there. We may improve this in the future and make refresh more flexible. So refresh is only possible on your "root" binding and would re-read all the bindings underneath it, i.e. send the same request as when you start your app. Question: Why do you need the refresh only on the relative binding on which you call create? Note that the create API by default refreshes the newly created entity, so that you see all selected properties incl. navigation properties for it after creation just as they are available for the other entities in the collection.

Best regards, Patric

cjohn001 commented 5 years ago

Hello Patric,

Question: Why do you need the refresh...

Do to the olingo bug I need to workaround currently, I need to have two absolute bindings. Say one for the list in a list view bound via a navigation binding /root(1)/nav_binding_to_List -> points to /list and a second one to the list /list Note, I need to do a create via the absolute list binding currently due to the olingo bug which does not allow it on the navigation binding. I would like to keep the data in the first absolute binding in sync with the second absolute binding. Therefore I would need to refresh its data. Well, looks like I have to wait for the bug getting fixed

uhlmannm commented 5 years ago

Hi Christoph,

the refresh function cannot do the refresh you need as Patric mentioned. However, you may get your binding refreshed using v4.Context.requestSideEffects: 1) Add binding parameter $$ownRequest:true to your relative list binding to ensure that the binding sends its own data requests. 2) Call requestSideEffects on the header context of that list binding: oTable.getBinding("items").getHeaderContext().requestSideEffects([{$NavigationPropertyPath : ""}]);

Best regards Mathias.

cjohn001 commented 5 years ago

Hello Mathias, (@uhlmannm ) thanks for letting me know. I would have never found this option. I unfortunately have not got the scenario working with your explanation yet. Not sure what I am doing wrong. Would be great if you could have a look at it. I provided an example on how I understood you in the following example:

https://ecubed-solutions.ddnss.de/#/Input/SelectFood

user: sap password: sap

Please go to the page "Eigene" according to the screenshot.

Bildschirmfoto 2019-09-05 um 18 25 00

Please open the menu option and go to edit mode "Produkt editieren" to change the values of an item. See next screenshot

Bildschirmfoto 2019-09-05 um 18 25 00

Please change the name of the product from "testproduct1" to "testproduct2" or something different. When the text edit looses focus, you should be able to save the changed value with the top right button. See picture below

Bildschirmfoto 2019-09-05 um 19 11 55

This saves the changed item according to the code in controlller/Input/EditCustomProduct.controller.js line 170 function onEditCustomProduct() via a relative binding.

The view should automatically navigate back to the view given in the first screenshot. Once this happens the view should reload the data according to your description. The code for it is in

controller/Input/CustomProducts.controller.js line 70 in the function onDisplay

var oList = this.byId("idCustomProductsList"); var oHeaderContext = oList.getBinding("items").getHeaderContext(); if (oHeaderContext) { oHeaderContext.requestSideEffects([{ $NavigationPropertyPath: "UserDefinedProducts_fKUserDefinedProductToAccount" }]); }

Like you can see, the list item still presents the old value for the name. No reload of the relative binding happened. With a look to the network traffic in the debugger one can see that no traffic appears do to the requestSideEffects call.

It would be great if you could give me an advice how I could get it running. Thanks a lot!

Best regards, Christoph