ms-wordoffice-jsapps-for-office

How can a range be used across different Word.run contexts?


I have created a taskpane addin for word that runs a search and displays information about the results as a list to the user. When the user clicks on an item in the list I want to select the range in word to show the user the location of the item. The addin will then allow the user to perform additional tasks on the range, for example change the font colour.

I am able to run the search and get ranges for display using the function below:

function runSearch(textToFind) {
  var items = [];
  return Word.run(function(context) {
    var options = Word.SearchOptions.newObject(context);
    options.matchWildCards = false;

    var rangesFind = context.document.body.search(textToFind, options);
    context.load(rangesFind, 'text, font, style');
    return context.sync().then(function() {
      for (var i = 0; i < rangesFind.items.length; i++) {
        items.push(rangesFind.items[i]);
        context.trackedObjects.add(rangesFind.items[i]);
      }
      return context.sync();
    });
  })
  .then(function() {
    return items;
  });
}; 

However I am having difficulty selecting the range on user click. I have tried using the ranges context:

function selectRange(range){
  range.select();
  return range.context.sync();
}

Or using the range in a new Word.run context:

function selectRange(range){
  return Word.run(function(context) {
    context.load(range);
    return context.sync().then(function(){
      range.select();
      return context.sync();
    });
  });
}

I have come across a potential method that involves creating a content control for each search result and then reloading all the content controls in the selectRangefunction in the new context and finding the matching control, but that seems very inefficient when I have the range already.

What is the best method for reusing a range across different Word.run contexts?


Solution

  • You cannot use an object across Word.run invocations. Word.run creates a new context every time that it's invoked, whereas the original object is tied to its own context, creating a mismatch.

    That being said, you absolutely can, from within a Word.run, add the objects you desire to context.trackedObjects.add(obj), and they will remain as working objects even after Word.run finishes executing. By "working objects" I mean that their path will not get invalidated (think something similar to garbage collection, but for remote objects).

    Once you have such object (and it looks above like you do), you should be able to call

    range.select();
    range.context.sync().catch(...);
    

    If it's not working for you, can you provide an example of the error you're getting?

    For completeness sake, I should note that once you add objects to the trackedObjects collection, you're effectively taking memory management of those objects into your own hands. This means that if you don't properly release the memory, you will be slowing down Word by bogging down its memory / range-adjustment chain. So once you're done using the tracked object(s), you should call obj.context.trackedObjects.remove(obj), followed by obj.context.sync(). Don't forget the last part - if you don't do a sync, your request to remove the tracked objects will not be dispatched, and you'll continue to use up the memory.

    ======= Update 1 =======

    Tom, thanks for providing the error message. It looks like this might be a bug in the Word implementation of the APIs -- I'll follow up on that, and someone might reach out to you if there's more questions.

    From a conceptual standpoint, you are absolutely on the right path -- and the following does work in Excel, for example:

    var range;
    Excel.run(function (ctx) {
        var sheet = ctx.workbook.worksheets.getActiveWorksheet();
    
        range = sheet.getRange("A5");
        range.values = [[5]];
        ctx.trackedObjects.add(range);
    
        return ctx.sync();
    })
    .then(function(){
        setTimeout(function() {
            range.select();
            range.context.trackedObjects.remove(range);
            range.context.sync();
        }, 2000);
    })
    .catch(function (error) {
        showMessage("Error: " + error);        
    });
    

    ======= Update 2 =======

    It turns out there is indeed a bug in the product. However, the good news is that it's easy to fix with a JavaScript-only fix, and in fact we'll do so in the next couple of weeks, updating the CDN.

    With the fix, the following code works:

    var paragraph;
    Word.run(function (ctx) {
        var p = ctx.document.body.paragraphs.first;
        paragraph = p.next;
        ctx.trackedObjects.add(paragraph);
        return ctx.sync();
    })
    .then(function(){
        setTimeout(function() {
            paragraph.select();
            paragraph.context.trackedObjects.remove(paragraph);
            paragraph.context.sync()
                .then(function() {
                    console.log("Done");
                })
                .catch(handleError);
        }, 2000);
    })
    .catch(handleError);
    
    function handleError (error) {
        console.log('Error: ' + JSON.stringify(error));
        if (error instanceof OfficeExtension.Error) {
            console.log('Debug info: ' + JSON.stringify(error.debugInfo));
        }
    }
    

    Want even better news? Until the CDN is updated, you can use the code below to "patch" the JavaScript library and make the code above run. You should run this code some time after Office.js has already loaded (i.e., within your Office.initialize function), and before you do a Word.run.

    var TrackedObjects = (function () {
        function TrackedObjects(context) {
            this._autoCleanupList = {};
            this.m_context = context;
        }
        TrackedObjects.prototype.add = function (param) {
            var _this = this;
            if (Array.isArray(param)) {
                param.forEach(function (item) { return _this._addCommon(item, true); });
            }
            else {
                this._addCommon(param, true);
            }
        };
        TrackedObjects.prototype._autoAdd = function (object) {
            this._addCommon(object, false);
            this._autoCleanupList[object._objectPath.objectPathInfo.Id] = object;
        };
        TrackedObjects.prototype._addCommon = function (object, isExplicitlyAdded) {
            if (object[OfficeExtension.Constants.isTracked]) {
                if (isExplicitlyAdded && this.m_context._autoCleanup) {
                    delete this._autoCleanupList[object._objectPath.objectPathInfo.Id];
                }
                return;
            }
            var referenceId = object[OfficeExtension.Constants.referenceId];
            if (OfficeExtension.Utility.isNullOrEmptyString(referenceId) && object._KeepReference) {
                object._KeepReference();
                OfficeExtension.ActionFactory.createInstantiateAction(this.m_context, object);
                if (isExplicitlyAdded && this.m_context._autoCleanup) {
                    delete this._autoCleanupList[object._objectPath.objectPathInfo.Id];
                }
                object[OfficeExtension.Constants.isTracked] = true;
            }
        };
        TrackedObjects.prototype.remove = function (param) {
            var _this = this;
            if (Array.isArray(param)) {
                param.forEach(function (item) { return _this._removeCommon(item); });
            }
            else {
                this._removeCommon(param);
            }
        };
        TrackedObjects.prototype._removeCommon = function (object) {
            var referenceId = object[OfficeExtension.Constants.referenceId];
            if (!OfficeExtension.Utility.isNullOrEmptyString(referenceId)) {
                var rootObject = this.m_context._rootObject;
                if (rootObject._RemoveReference) {
                    rootObject._RemoveReference(referenceId);
                }
                delete object[OfficeExtension.Constants.isTracked];
            }
        };
        TrackedObjects.prototype._retrieveAndClearAutoCleanupList = function () {
            var list = this._autoCleanupList;
            this._autoCleanupList = {};
            return list;
        };
        return TrackedObjects;
    }());
    OfficeExtension.TrackedObjects = TrackedObjects;
    

    Hope this helps!

    ~ Michael Zlatkovsky, developer on Office Extensibility team, MSFT