Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
MikeDoyle
Active Contributor
The Promise.all function allows you to fire off a number of asynchronous calls and then write some logic to run when they're all done.  If you haven't tried it you should.

 

What's needed?


My customer wants a UI5 form with a checklist.  The checklist must be dynamically generated.  The sections, questions and possible answers will all be maintained as Custom Business Object entries in S/4HANA Cloud.  There's a hierarchy, with each section containing one or many questions.  Each question has one or many answers, often (but not always) Yes or No.  They want to be able to add a new question without needing to change the code.

The app will run on the Cloud Foundry environment of SAP Cloud Platform, and I'm using a combination of the SAP Cloud Application Programming Model (APM) and the SAP S/4HANA Cloud SDK (S4SDK) to combine various SAP-provided APIs, Custom Business Objects and custom CDS views into a single OData service that the UI5 app can consume.

What's the tricky part?


If the back-end was accessed via a Netweaver Gateway server I would probably write the OData service to support a 'double expand'.  The url would look like this:

....../ChecklistSections?$expand=toQuestions/toAnswers

That would give me the whole hierarchy in one go (Sections->Questions->Answers), and I could then bind the UI controls to the OData model.

Sadly, this kind of request isn't supported yet by the APM/S4SDK combination that I'm using.  It looks like I could expand on a single section to get the questions, say, but not to get the whole lot in one go.  If it is possible it isn't documented!

What I can do is to retrieve the 3 entities independently.  That is to say I can make a call to get all the sections, another to get all the questions and then finally all the possible answers to the questions.  Then I can build the hierarchy using JavaScript (in the UI5 app).

What's the anti-pattern?


In the scenario I've described above we want to build the hierarchy but we can only do it when we have sections, questions and answers.  We don't care which order they come back in, but we need all of them to be available.

In the success callback of each OData read we could set a flag to say 'we're done'.  Then we could check the flags of the other calls, to see if they are done too.  Only on the final callback (whichever one that happens to be) will we proceed to build the hierarchy.   I don't like this pattern because it's messy and bug-prone.



We could run the calls in series instead of in parallel (i.e. sections callback triggers the read of questions) but obviously this will be much slower.

Promises.all to the rescue!


If you're familiar with passing success and error callback functions (e.g. with the v2.OdataModel read method) then you know the 'old way' of doing asynchronous calls.  With promises (the 'new way') you don't pass the callback functions when you make the asynchronous call. Instead the call returns a Promise object (synchronously) and you attach the callbacks to that.

UI5 does use promises too (and they are becoming more prevalent).

For a full explanation of promises see MDN or Google.  Some advantages of promises over the old pattern are:

  • they can be chained together

  • the callback is guaranteed to run even if it's added after the asynchronous call has ended(!)

  • they are easy to read

  • callback functions don't need to take care of which function gets called next

  • error handling is more sophisticated


Another advantage is the Promise.all method which is so cool and useful it gets it own blog (this one).  This method returns a kind of 'super-promise' which will resolve only when each of it's 'sub-promises' resolves (i.e. completes successfully).

In our case we can return three promises, for the sections, questions and answers calls.  Then we write the logic to combine all the results (and build the hierarchy) in the Promise.all success callback.

In the meantime we can sit back and relax, safe in the knowledge that we will be informed when all the callbacks are done!


What does the code look like?


readChecklist: function() {
var that = this;

Promise.all([ this.readChecklistEntity("/Section"),
this.readChecklistEntity("/Question"),
this.readChecklistEntity("/Answer")
]).then(that.buildChecklist.bind(that),
that.handleChecklistError.bind(that));
},

readChecklistEntity: function(path) {
var that = this;

return new Promise(
function(resolve, reject) {
that.getModel().read(path, {
success: function(oData) {
resolve(oData);
},
error: function(oResult) {
reject(oResult);
}
});
});
},

buildChecklist: function(values) {
var aSections = values[0].results;
var aQuestions = values[1].results;
var aAnswers = values[2].results;
//...now build the hierarchy
},

handleChecklistError: function(reason) {
//handle errors
},

We use a single read function (readChecklistEntity) and call it three times, passing in the entityset name.  That function returns a promise in each case, and we pass the three promises to the Promise.all method.  Then we attach success and error callback functions to the 'super-promise' that Promise.all returns.

Notice that this approach allows us to use promises for our calls even though, as noted before, the v2.ODataModel uses the 'old-style' callbacks.

Our buildChecklist function will be called when all three of the reads have returned successfully.  We don't care which comes back first and which comes back last.  The ordering of the result sets matches the order in which we passed the three promises to Promise.all.  It too doesn't depend on which read is first to complete.

If any of the reads fail, our handleChecklistError function will be called.

Alternatives


As it happens, with default settings our three reads will be combined into one batch request. We could just use the attachBatchRequestCompleted method on v2.ODataModel, which would give us an event handler that is called when the batch request returns.  However, we won't have handy access to the response data. Also, we might want to combine reads from different OData services and these obviously won't be combined into a single batch.

Further Reading


Want to learn more about using promises with UI5 and OData? Check out this blog from c3d1947136cd4c748a7aa794001af496 which outlines a 'Core Service' approach built using promises

nathan.hand has started a blog series on using promises with UI5
13 Comments
Labels in this area