SharePoint: Javascript from asynchronous to synchronous with Deferred and Promise
by mysticslayer on Jun.14, 2016, under blog, javascript, SharePoint
The last few years it has become more and more common that we use JavaScript for our SharePoint Projects. Whether it’s Angular, Knockout or another preferred framework it’s all very usable. But sometimes JavaScript can be a pain to use. For example, handling data in a synchronous way in Custom Actions. Recently I had to write code for sending document data to Dynamics AX. Since we use Business Connectivity Services to send the data to Dynamics AX and we are still using farm solutions I had to write a Custom Action.
When selecting multiple documents and handling the data in two for loops it’s a pain, because if you are using variables in your class that must be used between functions it will be overwritten while your process is still running. Let’s give a code example:
var ctx = ''; var web; var list; var listOrg; var item; var listItem; var props; var xmlDoc; function startWork() { ctx = new SP.ClientContext.get_current(); var items = SP.ListOperation.Selection.getSelectedItems(ctx); if (items.length >= 1) { for (idx in items) { var listId = SP.ListOperation.Selection.getSelectedList(); listOrg = ctx.get_web().get_lists().getById(listId); web = ctx.get_web(); list = web.get_lists().getByTitle('AX Documents'); var camlQuery = new SP.CamlQuery(); camlQuery.set_viewXml('<View><RowLimit>100</RowLimit></View>'); listItem = list.getItems(camlQuery); item = listOrg.getItemById(id); props = web.get_allProperties(); ctx.load(web); ctx.load(listOrg); ctx.load(props); ctx.load(listItem); ctx.load(item, 'EncodedAbsUrl', 'AX_Nummer', 'AX_Nummer2', 'AX_Nummer3', 'AX_Nummer4', 'AX_Nummer', 'LookupSoort', 'LookupSoort2', 'LookupSoort3', 'LookupSoort4', 'LookupSoort5'); ctx.executeQueryAsync(Function.createDelegate(this, onQuerySucceeded, Function.createDelegate(this, onQueryFailed)); } } } Function onQuerySucceeded() { var myProps = props; var myPropValues = myProps.get_fieldValues(); var myValue = myPropValues['Sil.AA.DMS.Common.Configurations']; var xmlDoc = $.parseXML(myValue); if (xmlDoc) { var areaId = $(xmlDoc).find('Configuration').find('AreaId').text(); for (var j = 0; j <= 6; j++) { if (j === 0) { var lookupField = item.get_item('LookupSoort'); var lookupValue = lookupField.get_lookupValue(); var updateItem = listItem.itemAt(0); updateItem.set_item('DocumentIdentificationType', lookupValue); updateItem.set_item('DocumentIdentification1', item.get_item('AX_Nummer')); updateItem.set_item('AreaId', areaId); updateItem.set_item('DocumentUrl', item.get_item('EncodedAbsUrl')); updateItem.update(); ctx.executeQueryAsync(onUploadSucceeded, onQueryFailed); } else { var lookupFieldName = 'LookupSoort' + (j + 1); var lookupField = item.get_item('LookupSoort' + (j + 1)); if (lookupField !== null) { var updateItem = listItem.itemAt(0); var lookupValue = lookupField.get_lookupValue(); updateItem.set_item('DocumentIdentificationType', lookupValue); updateItem.set_item('DocumentIdentification1', item.get_item('AX_Nummer' + (j + 1))); updateItem.set_item('AreaId', areaId); updateItem.set_item('DocumentUrl', item.get_item('EncodedAbsUrl')); updateItem.update(); ctx.executeQueryAsync(onUploadSucceeded, onQueryFailed); } } } } } Function onUploadSucceeded() { Alert(‘File has been processed’); } Function onQueryFailed(sender, args) { Alert(‘File not processed: ‘ + args); }
Let’s say I run the above code with two items, it will see both items, but the processing will be asynchronous. Since executeQueryAsync will be run the first time, but the first for loop will be processed at the same time. I have two items:
FileLeafReg | Title | AX_Number | LookupSoort | AX_Number2 | LookupSoort2 | AX_Number3 | LookupSoort3 | AX_Number4 | LookupSoort4 | AX_Number5 | LookupSoort5 |
http://sp2013/Documents/picture1.png | Picture 1 | VN12345 | Sales | VN12346 | Sales | VN12347 | Sales | Null | Null | Null | Null |
http://sp2013/Documents/picture2.png | Picture 2 | VN67890 | Sales | VN67891 | Sales | Null | Null | Null | Null | Null | Null |
It will result in processing the second items 5 times, with the properties of the second file. Because the for loop will put the values of the second file over the variables of the first file, since the first for loop is faster in processing. And the onQuerySucceeded will only be processed when the first for loop is done. So it will break every piece of code in the chain.
I talked with several people, experts in JavaScript and even on StackOverflow, and I never got a decent answer how to fix it properly. It always resulted that the chain was broken. I tried promise and deferred in different manners, still not as it should be. I was getting headaches because of it, since none of the answers sufficed in a proper way.
Normally I am absolutely not a morning person, and the breakthroughs are normally at night when everybody is asleep. But two weeks ago, when it was still very early in the morning I woke up and started to code, just before driving to the office, I found my answer in a way that I could live with it.
I shuffled and refactored all my notepad++ tabs in one singular tab and there it was. A solution that was clear to read. Let’s go through the promise and deferred part. The steps are marked in Red from Step 1 ‘till 6. So that the code is easy to follow.
var ctx = ''; function startWork() { ctx = new SP.ClientContext.get_current(); var items = SP.ListOperation.Selection.getSelectedItems(ctx); if (items.length >= 1) { for (idx in items) { fixLinkInAxapta(items[idx].id).then( // Step 1: Instead of running the first clientContext executeQueryAsync. We first go into the function and prepare the Promise function (listItem, item, props) { // Step 4: is running this in synchronous mode. var myProps = props; var myPropValues = myProps.get_fieldValues(); var myValue = myPropValues['Sil.AA.DMS.Common.Configurations']; var xmlDoc = $.parseXML(myValue); if (xmlDoc) { var areaId = $(xmlDoc).find('Configuration').find('AreaId').text(); for (var j = 0; j <= 6; j++) { if (j === 0) { var lookupField = item.get_item('LookupSoort'); var lookupValue = lookupField.get_lookupValue(); var updateItem = listItem.itemAt(0); updateItem.set_item('DocumentIdentificationType', lookupValue); updateItem.set_item('DocumentIdentification1', item.get_item('AX_Nummer')); updateItem.set_item('AreaId', areaId); updateItem.set_item('DocumentUrl', item.get_item('EncodedAbsUrl')); updateItem.update(); ctx.executeQueryAsync(onUploadSucceeded, onQueryFailed); } else { var lookupFieldName = 'LookupSoort' + (j + 1); var lookupField = item.get_item('LookupSoort' + (j + 1)); if (lookupField !== null) { var updateItem = listItem.itemAt(0); var lookupValue = lookupField.get_lookupValue(); updateItem.set_item('DocumentIdentificationType', lookupValue); updateItem.set_item('DocumentIdentification1', item.get_item('AX_Nummer' + (j + 1))); updateItem.set_item('AreaId', areaId); updateItem.set_item('DocumentUrl', item.get_item('EncodedAbsUrl')); updateItem.update(); ctx.executeQueryAsync(onUploadSucceeded, onQueryFailed); } else { break; } } if (j === 6) dfd.resolve(); // Step 5: Resolve the Deferred } } }, function (sender, args) { }); } } alert('All items have been processed to Dynamics AX'); } function fixLinkInAxapta(id) { var dfd = $.Deferred(); // Step 2: Setup the Deferred method var listId = SP.ListOperation.Selection.getSelectedList(); var listOrg = ctx.get_web().get_lists().getById(listId); var item; var props; var list; var listItem; var web; web = ctx.get_web(); list = web.get_lists().getByTitle('AX Documents'); var camlQuery = new SP.CamlQuery(); camlQuery.set_viewXml('<View><RowLimit>1</RowLimit></View>'); listItem = list.getItems(camlQuery); item = listOrg.getItemById(id); props = web.get_allProperties(); ctx.load(web); ctx.load(listOrg); ctx.load(props); ctx.load(listItem); ctx.load(item, 'EncodedAbsUrl', 'AX_Nummer', 'AX_Nummer2', 'AX_Nummer3', 'AX_Nummer4', 'AX_Nummer', 'LookupSoort', 'LookupSoort2', 'LookupSoort3', 'LookupSoort4', 'LookupSoort5'); // Step 3: Resolve the listItem, item and the properties and call the function(listItem, item, props) in the previous method and wait for it to finish. If an error occurs go to reject ctx.executeQueryAsync(Function.createDelegate(this, function () { dfd.resolve(listItem, item, props); }), Function.createDelegate(this, function (sender, args) { dfd.reject(sender, args); })); return dfd.promise(); // Step 6: When the ClientContext.executeQueryAsync is resolved make it promise and return to the For Loop }
Of course if you have any questions, don’t hesitate to comment on this blogpost and I will get back to you as soon as possible. More blog posts will follow soon.