Introduction
This article describes a way to write add-ins such that a single binary can be hosted across multiple versions of DevStudio, Visual Studio, and Office. It uses C++ and ATL, but the principles should carry over to other languages and frameworks.
Background
I’ve been frustrated by the changes in the add-in programming model across versions of Microsoft’s IDEs over the years. I invested a fair amount of effort in writing some add-ins for DevStudio 6, and found I had to completely re-write them when I upgraded to Visual Studio 2003. Then, the object model changed again (albeit slightly) when I moved to Visual Studio 2005, requiring another update. I still use all three IDEs on different projects, so I can’t just abandon the older versions; I have to maintain all three versions of any given add-in I have.
This irritated me enough that I worked out a scheme for writing Add-Ins in such a way that the same binary that can be loaded into DevStudio 6, Visual Studio 2003 and 2005, and even Office 2003 and hosted as an add-in for each. Because I suspect I’m not the only one to go through this, I decided to write up what I’d done.
General Layout of the Add-In
The core idea is very simple; the bulk of the effort involved working out a lot of implementation details. For the rest of this article, I’ll refer to the sample add-in I wrote to illustrate this, SampleCAI (see below). Before digging into the details, however, let me lay out my general scheme.
I’m going to build an In-Process COM Server (that is, a .dll) that exports a single component that implements all the interfaces required by the target hosts. This component will present itself to each host as an add-in in terms it recognizes. For example, when DevStudio instantiates the component, it will ask for the interface IDSAddIn. As long as you implement IDSAddIn, DevStudio will treat you as a DevStudio-compliant add-in. You could implement a hundred other interfaces, present a UI, service HTTP requests, whatever—DevStudio will neither know nor care.
Likewise, when VisualStudio 2003, 2005, or Office instantiate your component, they’ll ask your component for the IDTEExtensibility2 and IDTECommandTarget interfaces. Again, as long as you implement these two interfaces, the host application will treat it like an add-in, no matter what else you can do.
Put simply, if you whip up a COM component that implements these three interfaces, all your hosts will happily load that component as an add-in that conforms to their respective models:
As an aside, there are name clashes between DevStudio 6 and Visual Studio interfaces (for example, ITextDocument). I dealt with that by #includeing the DevStudio 6 header files (out of ‘objmodel’) and left those identifiers in the global namespace. I then #imported the typelibs describing the Visual Studio extensibility models, taking advantage of import’s default behavior of creating a new namespace for all the entities defined in the imported typelib.
The sample is a standard Win32 DLL project, implemented in C++ on top of ATL. I’ll walk through the implementation step by step and try to explain what I did and why.
Class CAdd-In
SampleCAI.cpp is boilerplate ATL code; it implements DLLMain and the four exports required by all COM In-Process Servers. The story really starts at SampleCAI.dll’s sole COM component, CoAddIn. Here’s the IDL for CoAddIn:
[ uuid(5f4e04a1-1a92-11db-89d7-00508d75f9f1), helpstring("Common Add-In Sample Add-In Object") ] coclass CoAddIn { [default] interface IUnknown /*IDSAddIn*/; }
The DLL can export only a single component due to the way DevStudio discovers new add-ins. When you point the DevStudio IDE at a DLL and ask it to load it as an add-in, DevStudio appears to call LoadLibrary, and then RegisterClassObjects on that DLL. It seems to spy on the Registry to figure out what COM components are registered by that call. I say “seems to” and “appears to” because this mechanism is undocumented; various add-in authors have deduced it empirically (see [1], for instance).
In any event, DevStudio determines what COM components are exported by your DLL and calls CoCreateInstance on each. After it creates the component, it calls QueryInterface, looking for IDSAddIn. If QI failes, DevStudio will notify the user and refuse to load the DLL as an add-in. Consequently, if you implemented, say, two COM objects, one for DevStudio and one for Visual Studio 2003 and 2005, DevStudio would find that its QI fails for the second component and will refuse to load the entire DLL.
Therefore, SampleCAI.dll exports one and only one COM component, CoAddIn, and that component implements IDSAddIn. Note that I don’t actually advertise IDSAddIn in the IDL: That’s just because IDSAddIn is only defined in a C/C++ header file (ADDAUTO.H); you have no IDL for it.
Now, take a look at CAddIn, the C++ class that incarnates the COM component CoAddIn:
class ATL_NO_VTABLE CAddIn : // Standard ATL parent classes... { public: /// ATL requires a default ctor CAddIn(); /// ATL-defined initialization routine HRESULT FinalConstruct(); /// ATL-defined cleanup routine void FinalRelease(); # ifndef DOXYGEN_INVOKED // Shield the macros from doxygen... // Stock ATL macros... // Tell ATL which interfaces we support BEGIN_COM_MAP(CAddIn) COM_INTERFACE_ENTRY(ISupportErrorInfo) COM_INTERFACE_ENTRY_AGGREGATE(IID_IDSAddIn, m_pDSAddIn) COM_INTERFACE_ENTRY_AGGREGATE(EnvDTE::IID_IDTCommandTarget, m_pDTEAddIn) COM_INTERFACE_ENTRY_AGGREGATE(AddInDO::IID__IDTExtensibility2, m_pDTEAddIn) COM_INTERFACE_ENTRY_AGGREGATE(IID_IDispatch, m_pDTEAddIn) END_COM_MAP() # endif // not DOXYGEN_INVOKED // ... public: /// Display our configuration dialog void Configure(); /// Carry out our command void SayHello(); private: /// Reference on our aggregated instance of CoDSAddIn CComPtr<IUnknown> m_pDSAddIn; /// Reference on our aggregated instance of CDTEAddIn CComPtr<IUnknown> m_pDTEAddIn; };
As you can see, CAddIn is a plain-jane ATL class implementing a COM component. The first point of interest is the interface map. As explained above, class CAddIn exports IDSAddIn. However, you support it through an aggregate, m_pDSAddIn. This is a subordinate COM object:
[ uuid(5f4e04a2-1a92-11db-89d7-00508d75f9f1), helpstring("Common AddIn Sample DevStudio 6 AddIn Object"), noncreatable ] coclass CoDSAddIn { [default] interface IUnknown /*IDSAddIn*/; }
Note the “noncreatable” tag; you don’t register it, either. It’s not strictly necessary to do this; I created this class solely for purposes of code readability. Adding the code for all the object models (that is, DevStudio’s, Visual Studio’s, and Office’s) to CAddIn made for an overly large class. This is strictly a matter of my personal preference; externally, callers won’t know the difference.
The upshot is that when DevStudio calls QueryInterface looking for IDSAddIn, you’ll delegate to m_pDSAddIn for it.
m_pDTEAddIn will hold a reference on a CoDTEAddIn. This is another aggregate analagous to CoDSAddin, but supporting the interfaces needed for Visual Studio and Office 2003. Here’s the IDL for this component:
[ uuid(5f4e04a3-1a92-11db-89d7-00508d75f9f1), helpstring("Common AddIn Sample DTE-Compatible AddIn"), noncreatable ] coclass CoDTEAddIn { [default] interface IDispatch; }
The next touchy part is the fact that both IDTEExtensibility2 and IDTECommandTarget are duals; what should you do if it gets a QI for IDispatch? This is one of the many reasons I don’t like duals, but that’s another article! In this case, it turns out that the IDE expects you to return IDTEExtensibilty2. Fortunately, IDSAddIn is custom, so there’s no conflict there.
The next points of interest are the public methods Configure and SayHello. The sample can do only two things: configure itself and say hello. This functionality resides in CAddIn. The idea is to concentrate the add-in’s “core functionality” in CAddIn and delegate to aggregated helper classes for dealing with each host app. When they detect that the hosting application has invoked a command, they’ll just call on CAddIn to do the work. After all, the whole point of this undertaking is to eliminate the need for code duplication in multiple add-ins.
DevStudio 6 Hosting
I’ve done a few non-standard things with respect to hosting within DevStudio. These aren’t, strictly speaking, necessary in terms of loading the add-in into multiple hosts; they just make the add-in a little nicer.
When you point the DevStudio IDE at a .dll and ask to load it as an AddIn, DevStudio makes Registry entries for the new AddIn under:
HKEY_CURRENT_USER\Software\Microsoft\DevStudio\6.0\AddIns
However, a component can “self-register” under that key as part of its installation, sparing the user the hassle of doing so. However, this means that the first time the new add-in is loaded, the vfFirstTime parameter to OnConnect won’t be set to VARIANT_TRUE.
This complicates toolbar creation, because a) you need to keep track yourself whether or not it’s been created, and b) calling AddCommandBarButton when vfFirstTime is *em* true will fail.
I’ve solved Problem A by just writing down a boolean in the Registry. I’ve solved Problem B by posting a message to a hidden message window (it turns out that AddCommandBarButton will succeed if you call it outside the context of OnConnection).
I learned how to do this from Nick Hodapp’s article “Undocumented Visual C++” [1]. Here’s the Registrar script the sample uses:
HKCU { NoRemove Software { NoRemove Microsoft { NoRemove DevStudio { NoRemove '6.0' { NoRemove AddIns { ForceRemove 'SampleCAI.CoAddin.1' = s '1' { val Description = s 'Sample Common AddIn 'Developer Studio Add-in' val DisplayName = s 'SampleCAI' val Filename = s '%MODULE%' } } } } } } }
Another tip from the same article is a way to name your toolbar. When using the standard add-in APIs, your new toolbar will be named “Toolbar
Note: The new name must be no longer than the intended one (usually eight characters).
With that, look at CDSAddIn, the C++ class that incarnates the DevStudio AddIn:
class ATL_NO_VTABLE CDSAddIn : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CDSAddIn, &CLSID_CoDSAddIn>, public ISupportErrorInfo, public IDSAddIn { public: ... /// Private initialization routine void SetParam(CAddIn *pParent); ... /// Tell the ATL Registrar *not* to register us DECLARE_NO_REGISTRY(); /// This component may only be created as an aggregate DECLARE_ONLY_AGGREGATABLE(CDSAddIn) ... /// Tell ATL which interfaces we support BEGIN_COM_MAP(CDSAddIn) COM_INTERFACE_ENTRY(IDSAddIn) COM_INTERFACE_ENTRY(ISupportErrorInfo) END_COM_MAP() ... private: ... /// Non-owning reference to our parent CAddIn instance CAddIn *m_pParent; ... };
For the most part, this is a straightforward ATL COM class implementing IDSAddIn. In fact, the implementation should be familiar to DevStudio AddIn developers. Its implementation of IDSAddIn::OnConnection gives DevStudio a dispinterface exposing your commands, and sinks any DevStudio events it’s interested in. In IDSAddIn::OnDisconnection, it severs that connection.
Notes:
- The COM coclass CoDSAddIn is not directly creatable. It’s not even registered, and, in any event, creation will fail without an aggregator.
- CDSAddIn maintains a (non-owning) back-pointer to its parent CAddIn. This enables it to delegate to its parent’s command implementations. I suppose this isn’t the most generic design: having one COM object so intimately aware of another’s implementation. However, given that this component will never see use elsewhere, the convenience seemed worth it.
Visual Studio and Office Hosting
As described above, your add-in will provide implementations of IDTEExtensibility2 and IDTECommandTarget through another COM aggregate, CoDTEAddIn. This co-class is incarnated by CDTEAddIn:
class ATL_NO_VTABLE CDTEAddIn : // Stock ATL parent classes... { public: /// Application host flavors enum Host { /// Sentinel value Host_Unknown, /// Visual Studio 2003 Host_VS2003, /// Visual Studio 2005 Host_VS2005, /// Excel 2003 Host_Excel2003, // Add new hosts here... }; public: ... /// Private initialization routine void SetParam(CAddIn *pParent); ... /// Tell the ATL Registrar *not* to register us DECLARE_NO_REGISTRY(); /// This component may only be created as an aggregate DECLARE_ONLY_AGGREGATABLE(CDTEAddIn) /// Tell ATL which interfaces we support BEGIN_COM_MAP(CDTEAddIn) COM_INTERFACE_ENTRY(ISupportErrorInfo) COM_INTERFACE_ENTRY(EnvDTE::IDTCommandTarget) COM_INTERFACE_ENTRY(AddInDO::_IDTExtensibility2) COM_INTERFACE_ENTRY2(IDispatch, AddInDO::IDTExtensibility2) END_COM_MAP() ... private: ... /// Reference to our host's Application object CComPtr<EnvDTE::_DTE> m_pApp; /// Reference to our host's Application object CComPtr<Excel::_Application> m_pExcel; /// Which host are we loaded into Host m_nHost; /// Non-owning reference to our parent CAddIn instance CAddIn *m_pParent; ... }; // End CDTEAddIn.
The first thing that should jump out at you is that the class knows the host into which it’s been loaded. Although Visual Studio and Office 2003 use this Add-In programming model, hosting applications themselves offer different interfaces to you. You need to take this into account when requesting services from our host. You guess the host type in the OnConnection method of the IDTExtensibility2 interface:
HRESULT hr = S_OK; // Eventual return value try { // Validate our parameters... if (NULL == pApplication) throw _com_error(E_INVALIDARG); if (NULL == pAddInInst) throw _com_error(E_INVALIDARG); // take a reference on the add-in object representing us, m_pAddIn = com_cast<EnvDTE::AddIn>(pAddInInst); // and try to figure out what DTE-compatible host we're // currently loaded into: m_nHost = GuessHostType(pApplication); ATLTRACE2(atlTraceHosting, 2, "CoDTEAddIn has been loaded with a c" "onnect mode of %d (our host is %d).\n", nMode, m_nHost); ...
After validating your parameters, the first real work you do is wrapped up in the call to GuessHostType; here is where you figure out what sort of environment you’ve been loaded into. Now, I’ve seen some Add-Ins that call GetModuleFileName(NULL,…) to get the name of the executable they’ve been loaded into, but I took a different approach. My thinking was that as long as the host implements the interfaces I expect, I can talk to it. For example, an application suite like Open Office could host Microsoft Office add-ins by implementing the appropriate COM interfaces.
GuessHostType works by QI’ing the application pointer you were given in OnConnection for various sorts of interfaces:
CDTEAddIn::Host CDTEAddIn::GuessHostType(IDispatch *pApp) { HRESULT hr = S_OK; // Are we being hosted by Visual Studio 2005? I suspect this // will be the most common case. Check by asking for an // ENVDTE80::DTE2 interface... EnvDTE80::DTE2 *pDTE2Raw; hr = pApp->QueryInterface(EnvDTE80::IID_DTE2, (void**)&pDTE2Raw); if (SUCCEEDED(hr)) { m_pApp = com_cast<EnvDTE::_DTE>(pApp); pDTE2Raw->Release(); return Host_VS2005; } // Okay -- maybe it's Visual Studio 2003... ...
Note that we make no distinction between Visual Studio 2005 & 2008. It turns out that VS2008 implements interface EnvDTE80::IID_DTE2, and implements in a manner close enough to VS2005 that, for your purposes, you don’t need to distinguish between them.
The goal is to fill in m_nHost with a member of the Host enumeration so that the rest of the logic “knows” how to behave. For instance, the next thing you do in OnConnection is call AddCommands:
void CDTEAddIn::AddCommands(AddInDO::ext_ConnectMode nMode) { switch (m_nHost) { case Host_VS2003: AddCommandsVS2003(nMode); break; case Host_VS2005: AddCommandsVS2005(nMode); break; ...
Notes:
- The COM coclass CoDTEAddIn is not directly creatable; it’s not even registered, and, in any event, creation will fail without an aggregator.
- Like CDSAddIn, CDTEAddIn maintains a (non-owning) back-pointer to its parent CAddIn, for the same reasons.