SharePoint JSOM and Promises - copying items from one list to another
-
09-10-2020 - |
Domanda
I need to use JSOM to copy a set of items (found from a CAML query) from one SharePoint list to another.
I'm pretty experienced doing this type of thing on the server side, but relatively new with it on the client side, with the async model. Based on my research, I need to use promises
to do this, since I need to know when the copy is complete and whether or not any errors occurred.
I also need to copy the items one at a time, rather than batching them, because there can be a large number of items to copy, with many fields, and I run into message size errors from SharePoint if I try to do them all as one batch.
Below is a snippet of my code (with some irrelevant things removed). This appears to be working - except for one thing. As it does the copy of the items, I update a div
on the page with the item number (just a counter) being copied as a progress indicator. Two things I notice:
The div is not updated with the counter sequentially. With the promise, I guess I expected to see
1, 2, 3, 4...
but the numbers jump around, not sequential. Not a big deal, just not what I expected.This is more of an issue. In my code, I have a function
updateSuccessFinal
that I call after the.done()
, which callsalert()
to notify the user the copy is complete. However, while that alert is displayed, I can see thediv
still being updated with item numbers, meaning it is still doing the copying. I need this alert to only display when all of the items have been written. If the user dismisses the alert, there could be items that were not copied because they were still queued up.
Am I using the promise
incorrectly in my code? How can I insure that the function updateSuccessFinal
is not called until all items have been written?
I am using d = $.Deferred() inside my addItem
function. I have also tried moving that outside the loop and passing d to addItem
and that did not seem to make any difference.
function snapshotItems(sourceListName, queryText, targetListName)
{
// Snapshot all of the items picked up in the criteria of view view into the snapshot list
var context = new SP.ClientContext.get_current();
var srcList = context.get_web().get_lists().getByTitle(sourceListName);
// Create query
var query = new SP.CamlQuery();
query.set_viewXml(queryText);
// Items matching the query
var items = srcList.getItems(query);
context.load(items);
// Insert the rows in the Snapshot table
context.executeQueryAsync(
function()
{
var listEnumerator = items.getEnumerator();
var i = 0;
while (listEnumerator.moveNext())
{
var curListItem = listEnumerator.get_current();
addItem(curListItem, i).done(
function() {
}
);
i++;
}
snapshotSuccessFinal();
},
function(sender, args)
{
alert('Get snapshot query failed: ' + args.get_message());
}
);
function addItem(curListItem, i)
{
// Need to use promises because batching all the adds results in a "message too large" error from SharePoint
// So uses promises to execute one add at a time
var d = $.Deferred();
var context = new SP.ClientContext.get_current();
var targetList = context.get_web().get_lists().getByTitle(targetListName);
var itemCreateInfo = new SP.ListItemCreationInformation();
var listItem = targetList.addItem(itemCreateInfo);
// Copy the custom fields
listItem.set_item("Title", curListItem.get_item("Title"));
// Write it
listItem.update();
context.load(listItem);
var passedData = { dfd: d, idx: i };
context.executeQueryAsync(Function.createDelegate(passedData,onUpdateOneSuccess), Function.createDelegate(passedData,onUpdateOneFailed));
return d.promise();
}
function onUpdateOneSuccess()
{
$("workingDiv").text("Snapshot item #" + this.idx.toString());
this.dfd.resolve();
}
function onUpdateOneFailed()
{
alert("Add item failed: " + args.get_message())
}
}
function snapshotSuccessFinal()
{
$("workingDiv").text("Snapshot populated.");
alert("Snapshot created successfully.");
}
Soluzione
At first glance (and its late saturday afternoon here, and the sun is out and its above 20 celcius for the first time, and I have had some beer)
You are running into JavaScrip scoping issues:
var passedData = { dfd: d, idx: i };
is the line that worries me, (it is almost a philosophical question... but that must be the beer)
where does i come from?
You are passing it so it will be the 'this' scope in the succesfunction. Which is highlevel JavaScript but not required here if you move the onUpdate function declarations inside the addItem function... that way those variables will be available and you don't have to pass a scope.
...that is the problem I have with your i
Move additem inside the calling function, where i is declared, and I think (can't be sure because of another beer) your problem is solved.
But Then
Promises are great, and advanced as well, but not really required here as all you use them for is waiting. This can be done with JavaScript 'executor' Functions in ES6
I think you can go back to the old (and simpler) POP pattern in this case.
Have your Iterator loop store all information as Objects in an Array. Then loop over the Array, make your addItem function POP a value, set executeQuery at work, and in the successFunction call addItem (recursion!) again ONLY IF the Array is not empty yet.
pseudo code:
var update=[1,2,3,4,5];
function additem(){
var item;
function handleerror(){ //so item is available in here!
//cool! We can handle an error
//even add the (modified) item with update.push(item)
//and continue with:
additem();
}
if(update.length>0){
item=update.pop();
//add item
executequeryAsync(additem , handleerror );
} else {
//https://msdn.microsoft.com/en-us/library/office/ff412058(v=office.14).aspx
SP.UI.Status.addStatus('Add items', 'All Done!');
}
}
PS. If you do want to pass Scopes, ditch Function.CreateDelegate
(which is old IE-ancient Microsoft only code) and learn .bind()
(which Function.CreateDelegate does for you you under the hood because SharePoint needed to be backwards compatible with IE-ancient)
Differing ways to make executeQueryAsync calls - Function.createDelegate (is old IE8 code)
PS2. I am on motorbike roadtrip in the USA in September, if you are anywhere between Denver and Washington, or Ignite in Atlanta, you could buy me a beer :-)