Office add-ins typically feature a specific type that is truly the star of the show, with its responsibilities including things such as facilitating the connection to the target product, extending the Office ribbon, and more. We typically refer to this type as the Connect class, and given the amount of responsibilities it can potentially acquire, it can easily turn into a very large, bulky, and perhaps slightly Lovecraftian entity.

When such a thing becomes reality for your add-in, you may start to wonder if it is at all possible to break up these responsibilities into separate types; indeed, separation is possible, and this article will go into how. Understanding the solution however, of course requires understanding the actual problem at hand.

Before we start, however, I would note that if you use VSTO to develop your add-in, this article most likely does not apply to you. I can’t be sure, as I stay very far away from the thing. If interfaces such as IDTExtensibility2 and IRibbonExtensibility appear alien to you, then I can assure you that reading any more after this point would only serve to consume your limited time.

The Problem

The Parties Involved

An add-in’s implementation of the IDTExtensibility2 interface serves as its connection to the Office product it targets. This is why we typically use Connect as the name for the implementing class. Without it, our add-in has no way of being loaded/unloaded by the host Office application.

If we want our add-in to improve upon Office’s ribbon, then we must implement the IRibbonExtensibility automation interface. This is used by Office in order to load the custom ribbon XML markup from the add-in and execute callbacks via the use of IDispatch::GetIDsOfNames and IDispatch::Invoke. Because it is Office that makes use of these methods, then it is manifest that Office be able to locate its implementation, and the method in which it does so may be unclear to those coming from a strictly .NET/managed background.

In order to allow for our add-in to extend the ribbon, the usual course of action for those of us developing in a managed environment is to simply add a IRibbonExtensibility implementation to our Connect class. Office will pick up on this just fine (although many of us at the time may not be very sure as to how it does so) resulting in our envisioned custom ribbon making it onto the live application instance.

Let’s not forget that Office has a few other (albeit less common) extensibility points we may be interested in, such as the ability to provide custom take panes through the use of the ICustomTaskPaneConsumer interface. The prime candidate for implementation of such interfaces will yet again most likely need to be our trusty Connect class.

A Growing Manifestation

We like to keep our code clean and compartmentalized, with each module or class responsible for as few things as possible. This desire will quickly become an impossible dream to many when we start dabbling in areas such as extension of the Office ribbon.

If we’re dealing with anything more than the simplest of add-ins, we’re going to start to see a very large number of callbacks being added to our Connect class. In the end, our Connect class will be very large, with potentially lots of responsibilities.

So it may come to be that you want to separate your add-in entry point code (IDTExtensibility2) from your ribbon extension code (IRibbonExtensibility) into two separate entities. If you know little of the mechanisms in use by Office in order to tap into your IRibbonExtensibility implementation, this may seem nigh impossible at first.

If we do some blind digging around how Outlook seems to be finding and interacting with our add-in, we can see that the scant registration data pertaining to our add-in makes no mention of its ribbon extending capabilities; in most cases, the only thing related to our add-in that is registered with the system is our Connect class.

Office is a strictly unmanaged beast. Consequently, it certainly doesn’t make use of .NET Reflection APIs in order to figure out if your add-in’s entry point implements IRibbonExtensibility. So, how does Office know if your add-in extends its ribbon?

COM-mon Knowledge, Baby

Hopefully you wouldn’t be surprised if I told you that the add-in you’ve been developing is a COM add-in. Indeed, your add-in is an in-process COM server that implements the IDTExtensibility2 interface as described in Msaddndr.dll (which just rolls off the tongue, I know).

So, hopefully you don’t feel it to be a very large leap of faith to go a bit further here and extrapolate that Office is making use of the basic set of tools native to COM in order to do all of its digging (I’d think the mention of the IDispatch interface earlier might have been a bit of a giveaway).

Your Connect class (well, actually the IDTExtensibility2 implementation found in your unmanaged shim, you are using a shim right?) is actually registered as an in-process server for your add-in via an InProcServer32 registry key. Office will get an interface pointer to the IUnknown of the object that implements IDTExtensibility2, and then use that pointer to QueryInterface for any subsequent interfaces (such as IRibbonExtensibility, etc.).

That’s all we need to know.

The Solution

If we want the responsibilities of extending Office’s ribbon delegated to another class, we need to return a pointer to that class when queried. Before we get into how we do that, let’s put what we’re working with on the table first.

We’ll be working with two managed classes: one which provides the point of connection to Office (Connect) and one which extends the Office ribbon (Ribbon).

Connect.cs
[ComVisible(true)]
[ProgId(CONNECT_PROG_ID)]
[Guid("5842D85C-3E55-423F-A114-9D3368EBC64F")]
[ClassInterface(ClassInterfaceType.None)]
public sealed class Connect: IDTExtensibility2
{
.
.
.
}
Ribbon.cs
[ComVisible(true)]
public sealed class Ribbon : IRibbonExtensibility
{
.
.
.
}

With these two classes, we can separate our connection logic from our ribbon logic. The tough part is getting Office to make use of our Ribbon class; as was stated previously, by default, the COM shim wizards require the Connect class to implement c>IRibbonExtensibility in addition to IDTExtensibility2 in order for us to be able to influence the Office ribbon.

So, naturally, the changes we’ll need to make will be to our unmanaged COM shim. The various file/class names I’ll be referencing here should be the ones generated by the Office COM shim wizard; if I’m making references to files that appear wholly foreign to you, let me know in the comments, as there is a chance that I’ve renamed them.

The first file we’ll be modifying is the header file for our outer aggregator (IOuterAggregator). This is the interface implemented by our ConnectProxy class and provided to a managed component so that the managed component can supply the shim with references to the various managed instances it either needs to use in response to queries or forward those queries to.

By default, the outer aggregator is used to supply a reference to only a single managed component, that being our Connect class. We need to be able to supply an additional reference to our Ribbon class, so we modify its only method’s signature to accept an additional IUnknown.

IOuterAggregator.h
__interface __declspec(uuid("7b70c487-b741-4973-b915-c812a91bdf63"))
IOuterAggregator : public IUnknown
{
	HRESULT __stdcall SetInnerPointers(IUnknown *pUnkRibbon, IUnknown *pUnkBlind);
};

Don’t forget that there is also a managed COM import declaration of this type in either your add-in or (if you didn’t combine the two) your external “managed aggregator”. This will also need to be updated.

IOuterAggregator.cs
[ComImport]
[Guid("7B70C487-B741-4973-B915-C812A91BDF63")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IOuterAggregator
{
    void SetInnerPointers(IntPtr pUnkRibbon, IntPtr pUnkBlind);
}

The next file we’ll be modifying is the header file for the class that acts as the proxy between Office and our managed components (ConnectProxy). The first modification we need to make is the addition of a separate pointer that will hold (indirectly of course) a reference to our managed Ribbon instance. We can simply add a new IUnknown declaration to the private section of the header file. In the end, we should have something like the following:

ConnectProxy.h
.
.
.
private:
    IDTExtensibility2 *_pConnect;
    CLRLoader *_pCLRLoader;
    IUnknown *_pUnkBlind;
    IUnknown *_pUnkRibbon;
};

OBJECT_ENTRY_AUTO(__uuidof(ConnectProxy), ConnectProxy)

Above, we can see the new IUnknown declaration for our ribbon. And before you yell at me about using Hungarian notation, know that the code generated by the wizard uses COM notation, and I continue to do so as well so as to remain consistent.

Next we need to make some changes to the COM map declared in this header file. Normally, there is no mention of IRibbonExtensibility2 in our COM map, even if the COM shim wizard has been configured to support IRibbonExtensibility2, and that’s because of the following line:

COM_INTERFACE_ENTRY_AGGREGATE_BLIND(_pUnkBlind)

This macro essentially causes queries for IIDs that do not match previous COM map entries to be forwarded to the provided pointer. So any queries for IRibbonExtensibility2 would hit up _pUnkBlind, which essentially points to the same thing pointed to by _pConnect.

Since we want our _pUnkRibbon instance to be forwarded ribbon related queries, we’ll need to it to the COM map. However, just adding an entry for IRibbonExtensibility2 is not enough; an entry for the IDispatch interface is also required. This is because the IDispatch interface provides the mechanic used for when we need to add callbacks to the various elements of a ribbon. Office sees that it needs to call a method named GetButtonLabel in order to get a button’s label, so it will query for IDispatch and use that to find a method named GetButtonLabel. Remember, we’re in unmanaged land, there’s no Reflection API to help us here.

Here’s what our COM map will look like when we’re done:

ConnectProxy.h
.
.
.
public:
.
.
.
BEGIN_COM_MAP(ConnectProxy)
    COM_INTERFACE_ENTRY(IDTExtensibility2)
    COM_INTERFACE_ENTRY(IOuterAggregator)
	COM_INTERFACE_ENTRY_AGGREGATE(__uuidof(IRibbonExtensibility), _pUnkRibbon)
	COM_INTERFACE_ENTRY_AGGREGATE(__uuidof(IDispatch), _pUnkRibbon)
    COM_INTERFACE_ENTRY_AGGREGATE_BLIND(_pUnkBlind)
END_COM_MAP()

The final change we need to make to this header file is related to the changes we made to the IOuterAggregator interface. Because ConnectProxy implements said interface, we need to propagate the changes we made to it to the proxy’s header file.

ConnectProxy.h
.
.
.
public:
.
.
.
STDMETHOD(SetInnerPointers)(IUnknown* pUnkRibbon, IUnknown* pUnkBlind);
.
.
.

That’s it as far as the header file is concerned. Next, because we made changes to the header file, we’re going to make some changes to the actual class file. We added a new member to the class, so be sure to initialize the _pUnkRibbon pointer to null in the constructor’s initializer list, as well as add the necessary clean up logic for it in the FinalRelease method.

Other than those routine concerns, the main changes will be to the SetInnerPointers method. It needs to reflect the updated declaration we committed to the IOuterAggregator.h file, as well as store the additional ribbon pointer being provided to us.

ConnectProxy.cpp
HRESULT __stdcall ConnectProxy::SetInnerPointers(IUnknown* pUnkRibbon, IUnknown* pUnkBlind)
{
	if (pUnkRibbon == NULL || pUnkBlind == NULL)
	{
        return E_POINTER;
    }

    if (_pUnkRibbon != NULL || _pUnkBlind != NULL)
    {
        return E_UNEXPECTED;
    }

	_pUnkRibbon = pUnkRibbon;
	_pUnkRibbon->AddRef();

    _pUnkBlind = pUnkBlind;
    _pUnkBlind->AddRef();

    return S_OK;
}

The last class we need to change is our managed implementation of the IInnerAggregator class. This is where the pointer to our now separate ribbon type will get provided to the shim, so we need to add the work required in order to do this to it.

InnerAggregator.cs
[ClassInterface(ClassInterfaceType.None)]
[ProgId("Your.ProgId")]
internal sealed class InnerAggregator : IInnerAggregator
{
    /// <inheritdoc/>
    public void CreateAggregatedInstance(IOuterAggregator outerObject)
    {
        IntPtr pOuter = IntPtr.Zero;
        IntPtr pBlindInner = IntPtr.Zero;
        IntPtr pRibbonInner = IntPtr.Zero;

        try
        {
            pOuter = Marshal.GetIUnknownForObject(outerObject);

            Connect connect = new Connect();
            Ribbon ribbon = new Ribbon();

            pBlindInner = Marshal.CreateAggregatedObject(pOuter, connect);
            pRibbonInner = Marshal.CreateAggregatedObject(pOuter, ribbon);

            outerObject.SetInnerPointers(pRibbonInner, pBlindInner);
        }
        finally
        {
            if (pOuter != IntPtr.Zero)
                Marshal.Release(pOuter);
            if (pBlindInner != IntPtr.Zero)
                Marshal.Release(pBlindInner);
            if (pRibbonInner != IntPtr.Zero)
                Marshal.Release(pRibbonInner);

            Marshal.ReleaseComObject(outerObject);
        }
    }
}

And that does it. Any ribbon related queries made by Office should be correctly redirected to your managed Ribbon class, and connection related activities will continue to be covered by the managed Connect class. No additional registry entries need to be made relating to all of this, everything will be handled by the additional code we added. Enjoy slightly more neat and compartmentalized Office add-in code.

Matt Weber

I'm the the Senior Software Architect at Emergingsoft where I lead the software development team. I am also the owner of this website. I enjoy well-designed code, independent thought, and the application of rationality in general. You can reach me at matt@badecho.com.

 Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title="" rel=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

 
© 2012-2013 Matt Weber. All Rights Reserved. Terms of Use.