Michael Palm Michael Palm - 3 years ago 66
Javascript Question

How replace the text in a huge number of content controls via word-js?

I am trying to write a function which takes a list of Rich Text Content Controls and a single string as argument, and which replaces the content of all matching content controls with this string.

While this works with a smaller amount of content controls, it fails with documents with a huge amount of them. I have to work with documents with over 700 Content Controls with individual titles. In this case, the code just replaces the first 66X CCs and then aborts with a GeneralException. I assume this is just due to the huge amount of content controls. I am having similar problems, when I try to register bindings for all these CCs (GeneralException). But this is a different topic.

I tried to work around this problem, by limiting the amounts of changes per .sync() and looping through the CCs, performing as many loops as necessary. However, this is not that easy, due to the asynchronous nature of office-js. I am not very familiar with javascript-async-promise-programming so far. But this is what I have come up with:

function replaceCCtextWithSingleString (CCtitleList, string) {
var maxPerBatch = 100;

/*
* A first .then() block is executed to get proxy objects for all selected CCs
*
* Then we would replace all the text-contents in one single .then() block. BUT:
* Word throws a GeneralException if you try to replace the text in more then 6XX CCs in one .then() block.
* In consequence we only process maxPerBatch CCs per .then() block
*/
Word.run(function (context) {
var CCcList = [];

// load CCs
for(var i = 0; i < CCtitleList.length; i++) {
CCcList.push(context.document.contentControls.getByTitle(CCtitleList[i]).load('id'));
}

return context.sync().then(function () { // synchronous
var CClist = [];
// aggregate list of CCs
for(var i = 0; i < CCcList.length; i++) {
if(CCcList[i].items.length == 0) {
throw 'Could not find CC with title "'+CCtitleList[j]+'"';
}
else {
CClist = CClist.concat(CCcList[i].items);
}
}
$('#status').html('Found '+CClist.length+' CCs matching the criteria. Started replacing...');
console.log('Found '+CClist.length+' CCs matching the criteria. Started replacing...');

// start replacing
return context.sync().then((function loop (replaceCounter, CClist) {
// asynchronous recoursive loop
for(var i = 0; replaceCounter < CClist.length && i < maxPerBatch; i++) { // loop in loop (i does only appear in condition)
// do this maxPerBatch times and then .sync() as long as there are still unreplaced CCs
CClist[replaceCounter].insertText(string, 'Replace');
replaceCounter++;
}

if(replaceCounter < CClist.length) return context.sync() // continue loop
.then(function () {
$('#status').html('...replaced the content of '+replaceCounter+' CCs...');
return loop(replaceCounter, numCCs);
});
else return context.sync() // end loop
.then(function () {
$('#status').html('Replaced the content of all CCs');
});
})(0, CClist));
});
}).catch(function (error) {
$('#status').html('<pre>Error: ' + JSON.stringify(error, null, 4) + '</pre>');
console.log('Error: ' + JSON.stringify(error, null, 4));
if (error instanceof OfficeExtension.Error) {
console.log('Debug info: ' + JSON.stringify(error.debugInfo, null, 4));
}
throw error;
});
}


However... it is not working. It replaces the first 100 CCs and then stops. Without a failure, without an exception or anything. The
return loop(replaceCounter, CClist);
is just not executed and I don't know why. If I try to step into this line in the debugger it throws me somewhere in the office-js code.

Any suggestions?

Edit:

I updated my code based on the suggestions of Juan Balmori and it works as a charm:

function replaceCCtextWithSingleString_v1_1 (CCtitleList, string) {
Word.run(function (context) {
var time1 = Date.now();

// load the title of all content controls
var CCc = context.document.contentControls.load('title');

return context.sync().then(function () { // synchronous
// extract CC titles
var documentCCtitleList = [];
for(var i = 0; i < CCc.items.length; i++) { documentCCtitleList.push(CCc.items[i].title); }

// check for missing titles and replace
for(var i = 0; i < CCtitleList.length; i++) {
var index = documentCCtitleList.indexOf(CCtitleList[i]);
if(index == -1) { // title is missing
throw 'Could not find CC with title "'+CCtitleList[i]+'"';
}
else { // replace
CCc.items[index].insertText(string, 'Replace');
}
}

$('#status').html('...replacing...');

return context.sync().then(function () {
var time2 = Date.now();
var tdiff = time2-time1;
$('#status').html('Successfully replaced all selected CCs in '+tdiff+' ms');
});
});
}).catch(function (error) {
$('#status').html('<pre>Error: ' + JSON.stringify(error, null, 4) + '</pre>');
console.log('Error: ' + JSON.stringify(error, null, 4));
if (error instanceof OfficeExtension.Error) {
console.log('Debug info: ' + JSON.stringify(error.debugInfo, null, 4));
}
});
}


It still takes 13995 ms to complete, but at least it works :-)

Any ideas, what was provoking the GeneralException though? Or how to decrease the execution time?

Answer Source

Good Question.. I did some perf test long time ago and I was able to change more than 10k content controls in a document. with 700 you should be ok. Not sure why are you pre-filling a list, that is not needed, you are actually navigating 2 times the collection which is not good for perf. You can do the string comparison while traversing the collection!

Here is an example, I just did a quick test with a 700 content control document with a hypothetical tag of "test".

I was able to 1. Compare their text against whatever you want to compare it (its a string) 2. Change the value if the condition is true.

It took 5134 milliseconds to complete the operation and here is the code. which I think its quite acceptable.

Hope this helps!

 function perfContentControls() {
        var time1 = Date.now(); // lets see in how much time we complete the operation :)
        var CCs =0
        Word.run(function (context) {
            var myCCs = context.document.body.contentControls.getByTag("test");
            context.load(myCCs);
            return context.sync()
            .then(function () {
                CCs  = myCCs.items.length
                for (var i = 0; i < CCs; i++) {
                    if (myCCs.items[i].text == "new text 3") // you can check the cc content and if needed replace it....
                         myCCs.items[i].insertText("new text 4", "replace");
                
                }

                return context.sync()
                .then(function () {
                    var time2 = Date.now();
                  var diff = time2 - time1;
                  console.log("# of CCs:" + CCs + " time to change:" + diff + "ms");
                })
            })
           .catch(function (er) {
               console.log(er.message);

           })

        })

    }

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download