Asynchronously Annoyed

I have been struggling with an issue on SharePoint for a few days now.  If this was some other method, besides jQuery in a SharePoint page, I’m sure it would have been much easier to do. It all starts with ExecuteQueryAsync and trying to loop through recently saved items.

The project I am working on requires a document review path, which is different based on the group or the type of document being reviewed.  After first attempting to create a list to handle this process, I went back to modifying the Document Library.  The Document Library isn’t exactly like a regular SharePoint list/app, but it does have a way to modify the Edit form.  The Edit form for a Document Library is actually the Properties form for a document after it is uploaded to the site.  As with other customization efforts in SharePoint, I have to resort to jQuery through a Script Editor Web Part.

As stated, I need to create a review path or Tasks for each document based on some routing information.  I created a list to handle my routing groups, with a column for the group name, step in the routing and then the person required to do the reviewing.  In the Properties form, I added a select list with the name of each Routing Group.  When the document is saved, it is supposed to create a Task for each person in the Routing Group and then add the predecessor Task to each appropriate Task to create sort of a review chain.  Sounds simple enough.

Well, back to ExecuteQueryAsync.  In order to get data or post data to the server, you have to use ExecuteQueryAsync.  And when you use it, the script just continues on without waiting for the call to finish (i.e. asynchronously ).  This doesn’t help at all when trying to loop through multiple items.  To top things off, an earlier item might take longer to save than a later item.  So, there is no real way of knowing if the first item is saved before the last item is kicked off.  And that is really important when you need to go back and add the predecessor Task to the recently created Tasks.  Apparently, there is an ExecuteQuery method, that doesn’t work in SharePoint 2013 cloud. Awesome!

To make things worse, I do about 4 of these calls to complete the entire script.

  1. Get Routing Group list to loop through
  2. Create new Task based on Routing Group step
  3. Get recently created Tasks based on Task Group
  4. Update each Task with predecessor based on  Routing Group step

I posted a question to SharePoint Stack Exchange and the response was to use the setTimeout function of JavaScript to slow when a portion of the script was called.  While this helped a little, in the end it really didn’t do anything to solve the overall issue.  Because after I got it working, the document wouldn’t save and redirect the page.  To solve that issue, I tried to implement the jQuery Deferred Object, so that it would return a true value and finish the PreItemSave function, which held the code for creating the Tasks.  While it did let the document save and continue on, it wasn’t waiting until I called the final Task update. So, basically, for some reason it wasn’t really working at all. Great!

After attempting to do the predecessor add and Task creation in a WorkFlow (2010), I realized that it would never work, because I couldn’t link list based on two values (step and task group).  Luckily, I still had one last thing I could do.

I had already added a form field so that the user could start the process called “Ready for Review”. It is a checkbox field, which I already had a hook into for locking the form to further edits.  I placed the code for doing the Task add and updates into that change listener.  I also added some jQuery to disable the “Save” buttons until the last Task was updated.  And, to top things off, I added a .click() event after the Save buttons were enabled again to continue on with the form.  Yeah!! That worked!  For the most part.  It seems that 1 in 5 demos has an issue with doing the Task predecessor. Arrrghh!

Here is the very long code below…without all the other bells and whistles in the script.
After the “Ready For Review” checkbox is checked, then it kicks off startGroupRouting.

I'm sure someone who knows jQuery and SharePoint much better than I could get the Deferred Object portion working.  And maybe they'll put up a good example of doing multiple loops with a couple of Deferred Objects slowing things down to keep things in order.  I know I would appreciate it.
// Get Group Routing list based off of selected value
function startGroupRouting(groupRouting) {
	// get Routing Group
	// get a new instance of the current ClientContext
	var clientContext = new SP.ClientContext.get_current();
	var approveList = clientContext.get_web().get_lists().getByTitle('Approvers');
	var approveQuery = new SP.CamlQuery();
	// Create query string
	var ql = '<View>' +
		'<Query>' +
			'<Where><Eq>' +
			  '<FieldRef Name="Routing_x0020_Group" />' +
			  '<Value Type="Text">' + groupRouting + '</Value>' +
			'</Eq></Where>' +
			'<OrderBy>' +
				'<FieldRef Name="Step" Ascending="false"></FieldRef>' +
			'</OrderBy>' +
		'</Query>' +
		'</View>';
	approveQuery.set_viewXml(ql);
	this.approveSelectList = approveList.getItems(approveQuery);
	clientContext.load(approveSelectList, 'Include(Approver,Approver_x0020_Type,Step)'); 

	// Execute query and loop through steps to create Tasks
	clientContext.executeQueryAsync(
		Function.createDelegate(this, groupRouteSuccess),
		Function.createDelegate(this, onGetFail)
	);
}
// After Success, Create Each Task
function groupRouteSuccess() {
	var memoDeliverDate = $('nobr:contains("Deliver By")').closest('tr').find('input[title^="Deliver By"]').val();
	var approveGrpEnumerator = approveSelectList.getEnumerator();
	// loop through list items
	var i = 0;
	var listCount = approveSelectList.get_count();
	var lastMemoCnt = listCount - 1;
	var lastMemo = false;
	while(i < listCount) {
		approveGrpEnumerator.moveNext();
		if(i == lastMemoCnt) {
			lastMemo = true;
		}
		var thisGrpItem = approveGrpEnumerator.get_current();
		var approver = thisGrpItem.get_item('Approver');
		var approverType = new String(thisGrpItem.get_item('Approver_x0020_Type'));
		var step = new String(thisGrpItem.get_item('Step'));						

		var clientContext = new SP.ClientContext.get_current();
		var oList = clientContext.get_web().get_lists().getByTitle('Memo Tasks');

		var today = new Date();
		var itemCreateInfo = new SP.ListItemCreationInformation();
		var oListItem = oList.addItem(itemCreateInfo);
		// Create items to insert into Task
		oListItem.set_item('Title', $.local.memoTitle + ' - ' + approverType);
		oListItem.set_item('Task_x0020_Group', $.local.memoTitle);
		oListItem.set_item('Step', step);
		oListItem.set_item('AssignedTo', approver);
		oListItem.set_item('Body', approverType);
		oListItem.set_item('DueDate', memoDeliverDate);
		oListItem.set_item('StartDate', today.format('M/dd/yyyy'));
		oListItem.update();

		clientContext.load(oListItem);
		if(i == lastMemoCnt){
			clientContext.executeQueryAsync(createFinalTaskSuccess, onGetFail);
		} else {
			clientContext.executeQueryAsync(createTaskSuccess(step), onGetFail);
		}
		i++;
	}
}
// Success for creating each task, except the last
function createTaskSuccess(step) {
	// add success message
	SP.UI.Notify.addNotification('New Task Created for Step ' + step);
}
// Success for creating each task, then start Task Update loop
function createFinalTaskSuccess() {
	// add success message
	SP.UI.Notify.addNotification('Final New Task Created');
	var clientContext = new SP.ClientContext.get_current();
	console.info("Starting Update Memo retrieve");
	var taskGrpList = clientContext.get_web().get_lists().getByTitle('Memo Tasks');
	var taskGrpQuery = new SP.CamlQuery();
	// Create query string
	console.info("Query for Get Memos of taskGroup : " + $.local.memoTitle);
	var ql = '<View>' +
		'<Query>' +
			'<Where><Eq>' +
			  '<FieldRef Name="Task_x0020_Group" />' +
			  '<Value Type="Text">' + $.local.memoTitle + '</Value>' +
			'</Eq></Where>' +
			'<OrderBy>' +
				'<FieldRef Name="Step" Ascending="true"></FieldRef>' +
			'</OrderBy>' +
		'</Query>' +
		'</View>';
	taskGrpQuery.set_viewXml(ql);
	taskSelectGrpList = taskGrpList.getItems(taskGrpQuery);
	clientContext.load(taskSelectGrpList); 

	// Execute query to get recently created Tasks
	clientContext.executeQueryAsync(
		Function.createDelegate(this,
			function() {
				var clientContext = new SP.ClientContext.get_current();
				var updTaskGrpList = clientContext.get_web().get_lists().getByTitle('Memo Tasks');
				console.info("Approval Group retrieve success");
				var taskGrpEnumerator = taskSelectGrpList.getEnumerator();
				var listCount = taskSelectGrpList.get_count();
				var lastMemoCnt = listCount - 1;

				console.info("Update memos retrieved: " + listCount + " lastMemo cnt = " + lastMemoCnt);
				while(taskGrpEnumerator.moveNext()) {
					var oListItem = taskGrpEnumerator.get_current();
					// add current item to the array
					$.local.memoArray.push(oListItem);
				}
				// loop through array to get current item and predecessor
				for(var x = 1; x < listCount; x++) {
					console.info("task(" + x
						+ ") ID = " + $.local.memoArray[x].get_item('ID')
						+ " predID = " +  $.local.memoArray[x-1].get_item('ID')
						+ " isLastUpdate = " + $.local.isLastUpdate);
					// update the Task with Predecessor of previous item
					var taskItem = updTaskGrpList.getItemById($.local.memoArray[x].get_item('ID'));
					taskItem.set_item('Predecessors', $.local.memoArray[x-1].get_item('ID'));
					taskItem.update();

					if(x == lastMemoCnt) {
						clientContext.executeQueryAsync(updateFinalTaskSuccess($.local.memoArray[x].get_item('ID')), onGetFail);
					} else {
						clientContext.executeQueryAsync(updateTaskSuccess($.local.memoArray[x].get_item('ID')), onGetFail);
					}
				}
			}
		),
		Function.createDelegate(this, onGetFail)
	);
}
// Success for every Task update, except last one
function updateTaskSuccess(taskID) {
	// add success message
	SP.UI.Notify.addNotification('Task Predecessor Added to Task ' + taskID);
}
// Success for final Task Update
function updateFinalTaskSuccess() {
	// add success message
	SP.UI.Notify.addNotification('Task Predecessor Added to Final Task');
	console.info("Final Update Task Success");
	//Enable the Save Item button by setting disabled to false
	disableSaveBtn(false);
	$('input[name$="SaveItem"]').click();
}
// all fail
function onGetFail(sender, args) {
	var msgError = 'Request failed. ' + args.get_message() + '\n' + args.get_stackTrace();
	// add error message
	SP.UI.Notify.addNotification('Error : ' + args.get_message());
	console.error('Error occured: ' + msgError);
}

About DeanLogic
Dean has been playing around with programming ever since his family got an IBM PC back in the early 80's. Things have changed since BASICA and Dean has dabbled in HTML, JavaScript, Action Script, Flex, Flash, PHP, C#, C++, J2ME and SQL. On this site Dean likes to share his adventures in coding. And since programming isn't enough of a time killer, Dean has also picked up the hobby of short film creation.

About DeanLogic

Dean has been playing around with programming ever since his family got an IBM PC back in the early 80's. Things have changed since BASICA and Dean has dabbled in HTML, JavaScript, Action Script, Flex, Flash, PHP, C#, C++, J2ME and SQL. On this site Dean likes to share his adventures in coding. And since programming isn't enough of a time killer, Dean has also picked up the hobby of short film creation.