Integrating Doc-level and Add-in Solutions

Everyone knows you can build document-level Office solutions and you can build application-level Office add-ins. Suppose your requirements dictate that you build a solution that uses both techniques – can this be done? First, let’s pause and consider whether this is a good idea in the first place, and whether you could re-architect to use only one or the other.

In most cases, the requirements for a solution can be met either with doc-level solutions or app-level add-ins. The Office application’s COM object model is entirely available to both types of solution. In some cases, there will be clear advantages to one or the other approach. For example, the VSTO-enhanced doc-level host-item controls (eg, XmlNodes and Content Controls in Word – or, ListObjects and NamedRanges in Excel) are only available in doc-level solutions. In other cases, you could do it either way. For example, suppose you have a need for some control UI in a custom task pane – you could use the doc-level Actions Pane, or you could use the app-level Custom Task Pane. Suppose you need Ribbon customization – again, you could do this either with a doc-level solution or with an app-level add-in. The same is true for custom CommandBars in older versions of Office.

On the other hand, there will be some contexts where the only possible solution requires the use of both techniques. Consider this scenario, for example:

· Your solution needs to work with users who have Word 2003 and with other users who have Word 2007.

· A user must be able to work with the solution in one version, then hand the document to another user who might be using the other version, who then hands it back for further work. In other words, the same document must be editable in both versions, it must be round-trippable, and the VSTO customization must work in both versions.

· The solution requires custom CommandBars when running in Word 2003, and custom Ribbon when running in Word 2007.

· The solution requires some form of Custom Task Pane or Actions Pane.

These requirements impose significant constraints on the solution design, specifically:

· The solution must use the Word97-2003 binary file format, because the VSTO v3 runtime that works with the new Open XML file format does not work with Word 2003.

· You cannot use doc-level Ribbon customization, because this only works with the new file format. So, you must use app-level Ribbon customization.

· You cannot use an app-level Custom Task Pane, because it must work the same in Word 2003 which does not support Custom Task Panes. So, you must use the doc-level Actions Pane.

To meet these seeminly conflicting requirements, you’d have to design the solution to use both doc-level and app-level components:

· Build an app-level add-in, which will work in both Word 2003 and 2007. This would include conditional code to determine which version of Word the solution is running in, and set up either a Ribbon customization or custom CommandBars accordingly.

· Build a doc-level customization, targeting the old binary file format in Word 2003 – which will also work in Word 2007, so you can have a doc-level ActionsPane.

· Remaining business logic could be put in either place: either in the doc-level customization or in the app-level add-in. If you need closely integrated processing of VSTO doc-level host-item control wrappers, this obviously goes in the doc-level customization. Any other work could go in either place.

· Connect the doc-level customization with the app-level add-in through shared logical interfaces. That is, the doc code will include an object that implements some interface, say IDocumentCommands, that is known to the add-in; and the add-in will include an object that implements some interface, say IAddInCommands, that is known to the doc code. These interfaces should be defined in a separate assembly that is then re-used by both the doc customization and the add-in.

· The runtime hook-up can use the fact that all app-level add-ins are exposed through the host application’s COMAddIns collection. Each add-in can expose any arbitrary functionality by setting the COMAddIn.Object property to some arbitrary object. In a sensible system, you would set this property to an object that implements an interface that is known to both the doc-level customization and the app-level add-in, say IAddInCommands.

· Calls from the doc-level customization to the app-level add-in will therefore go through this object, via standard CLR COM interop.

· The Office COM object models do not expose any COMAddIns equivalent for doc-level customizations. That means that the doc-level code must make itself available in a custom manner, and without the benefit of CLR COM interop.

· Therefore, the solution requires that the doc-level code gets hold of the add-in’s exposed object via the COMAddIns collection, and hands off to the add-in some exposed doc-level object. From this point, calls in the other direction – that is, from the app-level add-in to the doc-level customization will go through remoting, via the shared (IDocumentCommands) interface.

Here’s an overview of the design:

Let’s break this down. First, we define the two interfaces in a separate class library. Note that the IDocumentCommands interface will be implemented in the doc code, and the implementing object will be used via remoting – so this does not need to be COM-visible. On the other hand, the IAddInCommands will be implemented by the object that the add-in sets into the COMAddIn.Object property, and will be used via COM interop – and therefore needs to be a COM-visible dual interface:

namespace DocumentAddInInterfaces

{

    public interface IDocumentCommands

    {

        void InsertText(string s);

    }

    [ComVisible(true)]

    [Guid("158CF0BC-16F3-49be-A4DD-E3A81283C7A0")]

    [InterfaceType(ComInterfaceType.InterfaceIsDual)]

    public interface IAddInCommands

    {

        void RegisterDocument(IDocumentCommands d);

        void InsertText(string s);

    }

}

 

When the add-in loads, it constructs some arbitrary object that it wishes to expose. This object implements an interface (IAddInCommands) that is known to both the add-in and the doc customization. This interface offers two methods: RegisterDocument, which allows the doc-level code to register itself with the add-in; and InsertText, which inserts some text in the document:

[ComVisible(true)]

[ClassInterface(ClassInterfaceType.None)]

public class AddInCommands : IAddInCommands

{

    private Word.Application wordApplication;

    public AddInCommands(Word.Application w)

    {

        wordApplication = w;

    }

    public void RegisterDocument(IDocumentCommands d)

    {

        Globals.ThisAddIn.documentCommands = d;

    }

    public void InsertText(string s)

    {

        object missing = Type.Missing;

        Word.Range r = wordApplication.ActiveDocument.Range(
ref missing, ref missing);

        r.InsertAfter(

            String.Format("Add-in.InsertText: {0}{1}",
s, Environment.NewLine));

    }

}

The add-in sets this object as the value of the Object property in the COMAddIn object that represents this add-in in the COMAddIns collection of the host application. In VSTO add-ins, this is done by overriding the RequestComAddInAutomationService method:

public partial class ThisAddIn

{

    internal IDocumentCommands documentCommands;

    public IAddInCommands addInCommands;

    protected override object RequestComAddInAutomationService()

    {

        if (addInCommands == null)

        {

            addInCommands = new AddInCommands(this.Application);

        }

        return addInCommands;

    }

}

When the document loads (at some point after the add-in is loaded), it gets hold of the application’s COMAddIns collection, and the specific COMAddIn object that represents the add-in it wants. From this, it gets the add-in’s exposed object that implements IAddInCommands. The first thing it does with this object is to call back on its RegisterDocument method. This is the method that allows the doc code to pass an arbitrary object to the add-in code, thereby establishing the two-way communication:

public partial class ThisDocument

{

    public IDocumentCommands documentCommands;

    internal IAddInCommands addInCommands;

    private void ThisDocument_Startup(object sender, System.EventArgs e)

    {

        SimpleControl simpleControl = new SimpleControl();

        this.ActionsPane.Controls.Add(simpleControl);

        try

        {

            documentCommands = new DocumentCommands(this.Application);

            object addInProgId = "IntegrationTestAddIn";

            addInCommands =

                (IAddInCommands)

                this.Application.COMAddIns.Item(ref addInProgId).Object;

            addInCommands.RegisterDocument(documentCommands);

            simpleControl.btnCallAddIn.Enabled = true;

        }

        catch (Exception ex)

        {

            MessageBox.Show(ex.ToString());

        }

    }

}

Note that the code above assumes we have a SimpleControl custom UserControl that we’re placing in the doc-level Actions Pane. This SimpleControl offers a button that the user can click.

The doc object implements another known interface (IDocumentCommands) which has one method, InsertText. This object will be used via remoting, so we’ll make it MBRO:

internal class DocumentCommands :

    System.MarshalByRefObject, IDocumentCommands

{

    private Word.Application wordApplication;

    public DocumentCommands(Word.Application w)

    {

        wordApplication = w;

    }

    public void InsertText(string s)

    {

        object missing = Type.Missing;

        Word.Range r = wordApplication.ActiveDocument.Range(
ref missing, ref missing);

        r.InsertAfter(

            String.Format("Document.InsertText: {0}{1}",
s, Environment.NewLine));

    }

}

At some point later on, the user interacts with the doc customization code and also with the app-level add-in code, in no particular order. Here’s what happens when they interact with the custom CommandBars or Ribbon (which are implemented in the add-in)… They click a button (in the Ribbon/CommandBar) to insert some text in the document. This button handler uses the cached IDocumentCommands object to invoke the InsertText method implemented in the doc code:

public void btnCallDocument_Click(Office.IRibbonControl control)

{

    Globals.ThisAddIn.documentCommands.InsertText("Add-in invokes Doc");

}

Conversely, when the user interacts with the doc-level custom ActionsPane, the Click handler for the button on the UserControl uses the cached IAddInCommands object to invoke the InsertText method implemented in the add-in code:

private void btnCallAddIn_Click(object sender, EventArgs e)

{

    Globals.ThisDocument.addInCommands.InsertText("Doc invokes Add-in");

}

 

To sum up: most of the time, you can simply pick one or other of the two main Office client customization technologies to meet your solution requirements – and you should strive to keep things simple wherever possible. On the other hand, sometimes you need to build a more complex solution that uses both technologies. This is particularly true if you need to span multiple Office versions, each of which supports different extensibility mechanisms. If you take this approach, you’ll want to connect the components together in some sensible fashion. Going from add-in to doc customization, you can use the CLR’s standard COM interop. Going from doc customization to add-in, you can use remoting. In both directions, you should use a defined set of interfaces.