PortiBlog

Calling asynchronous JavaScript functions in a sequential manner

24 september 2015

Background

In the current SharePoint and O365 world we use more and more JavaScript to call resources on the server directly from the browser. Most, if not all, of these requests are done asynchronously. This means that we have less control on the program flow, as we do not know exactly when an async call finishes.

The problem

For simple scenarios or for one-time requests, the callback method provides sufficient handling functionality. For example, if we are requesting one set of data, let’s say a set of SharePoint list items, handling the result in a callback function works perfectly fine:

The following code, taken from msdn.microsoft.com, illustrates this behavior:


function retrieveListItems(siteUrl) {
    var clientContext = new SP.ClientContext(siteUrl);
    var oList = clientContext.get_web().get_lists().getByTitle('Announcements');
        
    var camlQuery = new SP.CamlQuery();
    camlQuery.set_viewXml(
        '<View><Query><Where><Geq><FieldRef Name=\'ID\'/>' + 
        '<Value Type=\'Number\'>1</Value></Geq></Where></Query>' + 
        '<RowLimit>10</RowLimit></View>'        
    );
    this.collListItem = oList.getItems(camlQuery);
        
    clientContext.load(collListItem);
    clientContext.executeQueryAsync(
        Function.createDelegate(this, this.onQuerySucceeded), 
        Function.createDelegate(this, this.onQueryFailed)
    ); 
}

function onQuerySucceeded(sender, args) {
    var listItemInfo = '';
    var listItemEnumerator = collListItem.getEnumerator();
        
    while (listItemEnumerator.moveNext()) {
        var oListItem = listItemEnumerator.get_current();
        listItemInfo += '\nID: ' + oListItem.get_id() + 
            '\nTitle: ' + oListItem.get_item('Title') + 
            '\nBody: ' + oListItem.get_item('Body');
    }

    alert(listItemInfo.toString());
}

function onQueryFailed(sender, args) {
    alert('Request failed. ' + args.get_message() + 
        '\n' + args.get_stackTrace());
}

In such scenario the onQuerySucceeded function uses the response and, in this example, shows a dialog with some list info.

The problem that I`ve encountered in several projects comes when we try to do several asynchronous calls to the server in a sequential manner.

For example, we want to issue the three requests to the server, in the following order:

Requests

If we use the asynchronous callback pattern we have no idea in what order the responses will come. It is quite possible that we receive them in different order like this:

Response

The other problem is that we don’t know which response exactly is the last one. This is useful if we would like to execute some additional logic after all the responses are received.

So what we actually want to achieve can be illustrated with the following diagram:

Flow

Promises

The Promises pattern helps in such situations. There are many resources on the internet that describe it in details, but the main purpose is that it simplifies the handling of callbacks and chaining functions.

In our case we use the JQuery.Deferred implementation, more info can be found here: https://api.jquery.com/deferred.promise/

 

The solution

In the solution that I`m going to present we are creating several Site Columns in the HostWeb of a SharePoint-Hosted Add-in. After all the site columns are created, we would like to execute the functionality for creating Site Content Types.

First we prepare the function that creates a single site column. The key here is this function returns a Deferred object, which we use later on for determining when the call is successfully executed.


createField = function (fieldType, fieldName, fieldDisplayName, fieldGroup, fieldHidden, fieldChoices) {
    var deferred = $.Deferred();

    var fields = hostWebContext.get_web().get_fields();

    if (fieldType == 'Choice')
    {
        formattedChoices = "";
        for (var i = 0; i < fieldChoices.length; i++) {
            formattedChoices += " " + fieldChoices[i] + "";
        }
        formattedChoices += "";
    }

    var fieldXml = ""+ formattedChoices + "";


    createdField = fields.addFieldAsXml(fieldXml, false, SP.AddFieldOptions.AddToNoContentType);

    hostWebContext.load(fields);
    hostWebContext.load(createdField);
    hostWebContext.executeQueryAsync(
        Function.createDelegate(this,
            function () {
                deferred.resolve(fieldName);
            }),	
        Function.createDelegate(this,
            function (sender, args) {
                deferred.reject(sender, args);
            }));	

    return deferred.promise();
},

Note that we still have two callbacks but their only purpose is to call deferred.resolve or deferred.reject, based on whether the call was successful or not.

The next step is to call the createField function multiple times, while waiting for the execution of each call. To do this we need a chain of promises and each one triggers the next one. The contents of the promise.then() function is executed when the Deferred object is either resolved or rejected. This way we are able to wait for the completion of the request in the foreach loop.


provisionFields: function () {

    var fields = [
        {
            type: 'Text',
            name: 'PortivaField1',
            displayName: "Portiva Field 1",
        },
        {
            type: 'Text',
            name: 'PortivaField2',
            displayName: "Portiva Field 2",
        }
    ];

    var groupName = 'Portiva Site Columns';

    // starting with a dummy promise, which is always resolved
    var deferred = $.Deferred();
    var prevPromise = deferred.resolve();

    fields.forEach(function (field) {
        prevPromise = prevPromise.then(function () {
            // call the async function after the previous one is completed
            return createField(field.type, field.name, field.displayName, groupName, 'false', field.choices);
        }).then(function (data) {

        });
    });

    prevPromise.then(function () {
        // Additional logic after all the requests are completed
        createSiteContentTypes();
    });
},

You can use the other methods of the deferred object, like .done, .fail, etc, to get more granular control based on the success or failure of the request.

 

Submit a comment