1

I have a 100 or so Word Open XML (.xml, not .docx, saved as "Word XML Document")documents (components) stored on SharePoint.

I use AJAX to load these by selection, as xml, 1 to many into an array, in which I also manage the selection sequence.

Once the user has selected the "components" they can then insert them into Word, the insertion is done via an array traversal (there is probably a better way to do this - but for now it does work),

wordBuild does the loading

function writeDocSync(){
  // run through nameXMLArray to find the right sequence
  var x = 0;
  var countXMLAdds = 0;
  //debugger;
  toggleWriteButton("disable");
  $('.progress-button').progressInitialize("Building Word");
  toggleProgressBar(true);
  // only run if we have data present
  if(nameXMLArray.length > 0){
    // increment through sequentially until we have all values
    while (countXMLAdds <= checkedList.length){
      // repeatedly traverse the array to get the next in sequence
      while (x < nameXMLArray.length){
        if (Number(nameXMLArray[x].position) === countXMLAdds && nameXMLArray[x].useStatus === true){
          progHold = countXMLAdds;
          wordBuild(nameXMLArray[x].xml, nameXMLArray[x].filename, countXMLAdds);
        }
        x++;
      }
      x=0;
      countXMLAdds ++;
    }
    document.getElementById("showCheck").className = "results";
    writeSelections("<b>You just built your proposal using<br/>the following components:</b><br/>");
    toggleWriteButton("enable");
  }
}

xxxxxxxxx

function wordBuild(xmlBody, nameDoc, progress){ 
  var aryLN = checkedList.length;
  var progPCT = (progress/aryLN)*100;
  progressMeter.progressSet(progPCT);
  Word.run(function (context) {
    var currentDoc = context.document; 
    var body = currentDoc.body;
    body.insertOoxml(xmlBody, Word.InsertLocation.end);
    body.insertBreak(Word.BreakType.page, Word.InsertLocation.end);
    return context.sync().then(function () {
      showNotification("Written " + nameDoc);
    });
  })
  .catch(function (error) {
    showNotification('Error: ' + nameDoc + ' :' + JSON.stringify(error));
    if (error instanceof OfficeExtension.Error) {
      showNotification('Debug info: ' + JSON.stringify(error.debugInfo));
    }
  });
}

All the documents will load singly, and all will load in batches of say 10 - 30 or more.

The problem comes when I load the entire set (I have a "check all" option). Sometimes 50 will build before I get an exception, sometimes 60, rarely more than 60, but very occasionally I get a gap where the exception doesn't occur, then it continues later.

The exception (which is repeated for each file) is:

Debug info: {} Error: componentABC.xml :{"name":"OfficeExtension.Error","code":"GeneralException","message":"An internal error has occurred.","traceMessages":[],"debugInfo":{},"stack":"GeneralException: An internal error has occurred.\n at Anonymous function (https://customerportal.sharepoint.com/sites/components/Shared%20Documents/componentAssembler/Scripts/Office/1/word-win32-16.00.js:19:150094)\n at yi (https://customerportal.sharepoint.com/sites/components/Shared%20Documents/componentAssembler/Scripts/Office/1/word-win32-16.00.js:19:163912)\n at st (https://customerportal.sharepoint.com/sites/components/Shared%20Documents/componentAssembler/Scripts/Office/1/word-win32-16.00.js:19:163999)\n at d (https://customerportal.sharepoint.com/sites/components/Shared%20Documents/componentAssembler/Scripts/Office/1/word-win32-16.00.js:19:163819)\n at c (https://customerportal.sharepoint.com/sites/components/Shared%20Documents/componentAssembler/Scripts/Office/1/word-win32-16.00.js:19:162405)"}

Any help with what might cause this would be hugely appreciated.

Oh I should also say, the files where the exception is raised don't get inserted into Word. But in smaller batches - they work without issue.

nateyolles
  • 1,851
  • 5
  • 17
  • 23
Jerry Weeks
  • 317
  • 2
  • 13
  • Is there any correlation between the execution time and how many it processes? Curious if the operation may be failing due to something timing out. – Marc LaFleur Jun 23 '16 at 14:46
  • I'll try and find out Marc. I have just added a semaphore type approach using code from http://www.developer.com/lang/jscript/article.php/3592016 (with the intent being to slow down the demand on the Word API.) Essentially I can see all 100 plus documents are released almost instantaneously to the API, so it is a panic of sorts somewhere outside of my code. Additionally, I cleaned and standardised all the styles in the documents, and that seemed to make it fail sooner. Almost as if the faster Word accepts the inserts, the sooner the error (I need to confirm this) – Jerry Weeks Jun 24 '16 at 09:15

2 Answers2

2

Word.run() is an asynchronous call, and there's a limit to the number of concurrent Word.run() calls you can make. Since you're executing Word.run() inside a while loop, all of them get kicked off at the same time and run simultaneously.

There are a few ways to work around this.

  1. Put everything inside one Word.run() call. This puts everything in one giant batch, avoiding multiple roundtrip calls to Word.

    if (nameXMLArray.length > 0 {
      Word.run(function(context) {
        //...
        while(...) {
          wordBuild(context, nameXMLArray[x].xml, nameXMLArray[x].filename, countXMLAdds);  
        //...
        }
        return context.sync();
      });
    }
    
    function wordBuild(context, xmlBoxy, nameDoc, progress) {
      //everything as it currently is, except without the Word.run and the context.sync
    }
    
  2. Implement wordBuild as a promise, and use AngularJS’s $q service to chain the promises, something vaguely like this:

    function wordBuild(...) {
      var deferred = $q.defer();
      Word.run( function(context) {
        // current code
        return context.sync().then(function() {
          deferred.resolve();
        });
      });
      return deferred.promise;
    }
    
    //Somewhere else
    for (var x…)
    {
      promises.add(wordBuild);
    }
    $q.all(promises);
    

    https://docs.angularjs.org/api/ng/service/$q

    Angularjs $q.all

  3. Chain the wordBuild calls yourself, as something like this:

    var x = 0;
    var context;
    
    function (wordBuild() {
      if (x >= nameXMLArray.length)
        return;
      else {
        context.document.body.insertOoxml(ooxml, Word.InsertLocation.end);
        x++;
        return context.sync().then(wordBuild);
      }
    });
    
    Word.run(function (ctx) {
      context = ctx;
      return wordBuild();
    }
    

    This sort of approach is difficult to maintain, but it could work.

Incidentally, the progress meter in your original code only updates when the call to Word starts, not when it actually returns. You might want to move the progress meter update code into the callback.

Community
  • 1
  • 1
Geoffrey
  • 46
  • 3
  • Thanks Geoffrey, really useful. I have tested your first option - the most simple / fast to add. But the sheer mass of files (100 or so) seem to cause Word to crash. Everything hangs almost immediately after I hit "build"! But 5 - 10 files work fine! There might be something smart to be done using batches of perhaps 10 or 15 files? I am now going to try your 3rd suggestion, I tried the "promise" and angular before, but got lost on route (all new territory for me!). – Jerry Weeks Jun 24 '16 at 16:33
  • 3rd options is tantalising - because the execution hangs, waiting for Word, the add-in times out and restarts! – Jerry Weeks Jun 27 '16 at 11:35
  • I have had some users testing, and in the original script we have more or less confirmed there is a 50 document limit on "Asynchronous calls" using "Word.run( function(context) {... Geoffrey, do you know if it is possible to receive a response back from Word body when it completes insertion? That way I could write to look for response back, before send next. At the moment the context.sync() seems to be confirmation that the asynchronous call has successfully taken the document, not Word has successfully inserted it. Thanks again for the help. – Jerry Weeks Jun 28 '16 at 07:49
  • Just a quick comment on: "Incidentally, the progress meter in your original code only updates when the call to Word starts, not when it actually returns. You might want to move the progress meter update code into the callback.". The handover is so fast, 100 docs go in less time than the progress bar can truly show, so in effect the progress bar is not worth much. Actually I have at start, and at end - in another part of the code. It briefly "blinks", if you are very observant! However it is the least of my concerns right now! – Jerry Weeks Jun 28 '16 at 07:53
  • Hi Jerry, I updated the third code option so that there's only one `Word.run()` - hopefully this works better! The `context.sync` callback is the only confirmation that Word gives, but usually this is pretty informative. – Geoffrey Jun 28 '16 at 17:33
0

I ended up using jQuery deferreds, I was already using jQuery for treeview and checkboxes etc. so it made sense.

This is a mix of Geoffrey's suggestions and my own! I cannot claim it to be good code, only that is does work. (If it is good code or not will take me more time to understand!)

I run batches of 49 xml doc inserts, at 51 the Async call "Word.run" failed in tests, and inserts of 80 or so documents in one Word.run caused Word to freeze, so although not proven 49 inserts within 1 Word.run seems like a good starter for 10! 50 inserts of 49 pieces allows for 2450 inserts, which is way beyond anything I can see being needed, and would probably break Word!

To get the deferreds and sent variables to keep their values once launched as asynch deferreds I had to create a variable to transfer both new deferreds, and values, so I could use the "bind" command. As Word async returns context.sync() I check the count of the batch, when the batch is completed, I then call the next batch - inside the context.sync()

A sort of recursive call, still a combination of Geoffrey's suggestion, and batches. This has a theoretical limit of 50 batches of 49 document sections. So far this has worked in all tests.

The progress meter exists in its own timed call, but as JavaScript prioritises code over UI it does hop. For example 120 documents it will hop just below half way fairly quickly, then a while later jump to almost complete, then complete (effectively 3 hops of a massively fast sequential percentage increases, various tricks suggested have zero effect (forceRepaint() is the latest experiment!).

        function startUILock(){
            // batch up in groups of 49 documents (51 and more were shown to fail, 49 gives manouvre room)
            toggleProgressBar(true);
            $('.progress-button').progressInitialize("Building Word");
            progressMeter.progressSet(1);   
            $.blockUI({message: "Building word..."});
            setTimeout(forceRepaint, 3000);
        }

        function forceRepaint(){
            var el = document.getElementById('progDiv');
            el.style.cssText += ';-webkit-transform:rotateZ(0deg)';
            el.offsetHeight;
            el.style.cssText += ';-webkit-transform:none';
        }

        function UIUnlock(insertedCount){
            debugger;
            var pct = (insertedCount/checkedList.length)*100
              //showNotification('Progress percent is: ' + pct);
            if (insertedCount !== checkedList.length ){  
              progressMeter.progressSet(pct);
              forceRepaint();
            } else {
            $.unblockUI();
            progressMeter.progressSet(100); 
            }
        }

        function writeDocDeffered(){
            insertedCounter = 0;
            var lastBatch = 0;
            var x = 49;
            var z = checkedList.length + 1;
            if(x > z){
                    x=z;
            }
            deferreds = buildDeferredBatch(x, lastBatch);
            $.when(deferreds).done(function () {    
                    return;
                })
                .fail(function () {
                   //showNotification('One of our promises failed');
                });
        }

        function buildDeferredBatch(batch, lastBatch) {
            // this ensures the variables remain as issued - allows use of "bind"
            var deferredsa = [];
                            var docSender = {
                                defr : $.Deferred(),                        
                                POSITION: batch,
                                LASTPOSITION: lastBatch,
                                runMe : function(){                                     
                                this.defr.resolve(writeDocBatchedDeferred(this.POSITION, this.LASTPOSITION, this.defr));
                                }
                            }
                            // small timeout might not be required
                            deferredsa.push(setTimeout(docSender.runMe.bind(docSender), 10));                                                               
            return deferredsa;
        }

        function writeDocBatchedDeferred(batch, lastBatch, defr){
            // write the batches using deferred and promises
            var x;
            var countXMLAdds = lastBatch; 
            x = 0;
            var fileName;
            debugger;   
            // only run if we have data present
            if(nameXMLArray.length > 0){
            var aryLN = checkedList.length;
            // increment through sequentially until we have all values  
                Word.run(function (context) {
                var currentDoc = context.document; 
                var body = currentDoc.body; 
                while (countXMLAdds <= batch){      
                    // repeatedly traverse the array to get the next in sequence
                    while (x < nameXMLArray.length){                
                        if (Number(nameXMLArray[x].position) === countXMLAdds && nameXMLArray[x].useStatus === true){  
                            fileName = nameXMLArray[x].filename;
                            body.insertOoxml(nameXMLArray[x].xml, Word.InsertLocation.end);
                            body.insertBreak(Word.BreakType.page, Word.InsertLocation.end);
                            insertedCounter = countXMLAdds;                     
                            var latest = insertedCounter;
                            var timerIt = {                                         
                                LATEST: latest,         
                                runMe : function(){ 
                                UIUnlock(this.LATEST);
                                }
                            }
                            setTimeout(timerIt.runMe.bind(timerIt),1000);                                   
                        }
                    x++;    
                    }
                    x=0;
                    countXMLAdds ++;
                }
                return context.sync().then(function () {                            
                    if(countXMLAdds = batch){
                        var lastBatch = batch + 1;
                        // set for next batch
                        var nextBatch = batch + 50;
                        var totalBatch = checkedList.length + 1;
                        // do not exceed the total batch
                        if(nextBatch > totalBatch){
                            nextBatch=totalBatch;
                        }
                        // any left to process keep going
                        if (nextBatch <= totalBatch && lastBatch < nextBatch){
                            deferreds =  deferreds.concat(buildDeferredBatch(nextBatch, lastBatch));                            
                        } 
                        // this batch done
                        defr.done();                
                    }
                });
                })
                .catch(function (error) {
                showNotification('Error: ' + nameXMLArray[x].filename + " " + JSON.stringify(error));
                if (error instanceof OfficeExtension.Error) {
                    showNotification('Debug info: ' + JSON.stringify(error.debugInfo));
                }
            }); 
                document.getElementById("showCheck").className = "results";
                writeSelections("<b>You just built your document using<br/>the following components:</b><br/>");
            }
            return defr.promise;
        }
Jerry Weeks
  • 317
  • 2
  • 13
  • The progress bar is almost instant due to the entire batch releasing almost instantly to asynch. Oddly enough $.blockUI is slightly better, but Word does allot of catching up post "insert" - even with Grammar and spelling switched off (pagination still runs). – Jerry Weeks Jul 05 '16 at 13:48