I have a big Google Apps Script project working on Google Spreadsheets and I am trying to convert it to an office-js add-in that works on Excel Workbooks. I understand that it is good practice to put everything that will be calling excel specific functions (directly interacting with the workbook) into an Excel.run()
function so it does a proper clean-up and no memory leaks occur. I also understand I should do context.sync()
as little as possible to optimize performance.
Here are my questions, (I think some of them come from my incomplete grasp of how js works; these things GAS handled without me needing to question them) :
1a) When we put our block of code in
Excel.run(context => {
//code that does stuff with context;
context.sync();
});
where does the context come from? Is this equivalent to
Excel.run(()=> {
let context = new Excel.RequestContext;
//code that does stuff with context;
context.sync();
});
1b) Also, if context gets generated with every new function why would I ever return context.sync()
and not just context.sync()
?
function handle_error(e){//second context generated in case of error
let context=New Excel.RequestContext;
context.workbook.load('name');
await context.sync();
some_logging_function(context.workbook.name, e);
}
function data_func(some_data: Non-Excel-Interface): Other-Non-Excel-Interface{
//manipulate data
//in case of error
handle_error(e);
//continue with data massaging
return altered_data;
}
Excel.run(context=>{ //first context
context.workbook.worksheets.getItem('Sheet1').getUsedRange().load('values');
context.sync();
let values = context.workbook.worksheets.getItem('Sheet1').getUsedRange().values;
let some_data: Non-Excel-Interface = {sheetName: 'Sheet1', data: values};
let new_vals = data_func(some_data);
context.workbook.worksheets.getItem('Sheet1').getUsedRange().values = new_vals.new_data;
context.sync();
});
context: Excel.RequestContext
and range: Excel.Range
in other functions do I need Excel.run()
in those functions, too? In other words, should the code inside functions a()
and b()
be inside Excel.run()
?function a(rng: Excel.Range, values:string[][]):Excel.Range{
rng.values = values;
return rng;
}
function b(context: Excel.RequestContext): Excel.RequestContext{
context.workbook.load('name');//load name property, but don't context.sync()
return context;
}
Excel.run(async context=>{
context = b(context);
let rng = context.workbook.worksheets.getItem('Sheet1').getUsedRange();
rng.load('values');
await context.sync();//values property and workbook name property must be available now
rng = a(rng, [['aa', 'bb', 'cc']]);
await context.sync();//new values must be available now
console.log(context.workbook.name, rng.values);//should show the title of the workbook and the newly assigned values of the range
});
context.sync()
sparingly, that means I use it only when I desperately need it, so it must always come with await
. So why not make context.sync()
synchronous by default?I'll try to answer some of these questions and try to get some help for the others. I also recommend the book Building Office Add-ins for an understanding of the Office JavaScript library. See this too, if you haven't already: Application specific API model.
1a. Yes. That's essentially correct. Under the hood, Excel.run
creates an Office.RequestContext
object and passes it to the batch function parameter. (But your two code blocks are not literally equivalent. You would not call Excel.run
AND explicitly create a RequestContext
object.)
1b. From skimming the book I linked to, I think that you have to return
what the book calls the meta-promise so that the Excel.run
can resolve the Promise that it returns. Here's an example from the book:
Excel.run(function (context) {
var selectionRange = context.workbook.getSelectedRange();
selectionRange.format.fill.clear();
selectionRange.load("values");
return context.sync()
.then(function () {
var rowCount = selectionRange.values.length;
var columnCount = selectionRange.values[0].length;
for (var row = 0; row < rowCount; row++) {
for (var column = 0; column < columnCount; column ++) {
if (selectionRange.values[row][column] > 50) {
selectionRange.getCell(row, column)
.format.fill.color = "yellow";
}
}
}
})
.then(context.sync);
}).catch(OfficeHelpers.Utilities.log);
From skimming the book I linked to, I think that the answer is yes; the Excel.run
always creates a new context
object and passes it to the batch function. There are techniques and an override of Excel.run
that enable you to pass an object created in one context to another call of Excel.run
, but these are intended for use with independent calls of Excel.run
, not nested calls, as in your case.
No. You should not call Excel.run
inside a
or b
.
I think there are scenarios in which you would not need to await context.sync
. For example, when all the code in the parent function that comes after the context.sync
only affects the UI of a task pane and does not depend on reading any data from the current Office document. The good practice of minimizing calls of context.sync
is because it requires a round-trip between the document and JavaScript runtime in which the add-in code is running (on the user's computer). This would be true regardless of whether context.sync
is synchronous or not.