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 other 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 DLL that can be loaded into DevStudio 6, Visual Studio 2003, 2005, and 2008, and even Office 2003 and hosted as an add-in for each. Since 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 be referring to the sample add-in I wrote to illustrate my approach, SampleCAI
(see download). 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 our target hosts. This component will present itself to each host as an add-in in terms they recognize. For example, when DevStudio instantiates our component, it will ask for the interface IDSAddIn
. As long as we implement IDSAddIn
, DevStudio will treat us as a DevStudio-compliant add-in. We could implement a hundred other interfaces, present a UI, service HTTP requests, whatever -- DevStudio will neither know nor care.
Likewise, when Visual Studio 2003, 2005, 2008 or Office instantiate our component, they'll ask our component for the IDTEExtensibility2
and IDTECommandTarget
interfaces. Again, as long as we implement these two interfaces, the host application will treat it like an add-in, no matter what else we can do.
Put simply, if you whip up a COM component that implements these three interfaces, all our 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 & Visual Studio interfaces (e.g. ITextDocument
). I dealt with that by #include
'ing the DevStudio 6 header files (out of 'objmodel
') and left those identifiers in the global namespace
. I then #import
'ed the typelib
s 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 ;
}
The DLL can only export a single component due to the way DevStudio discovers new add-ins. When you point the DevStudio IDE at a DLL & ask it to load it as an add-in, DevStudio appears to call LoadLibrary
, then RegisterClassObjects
on that DLL. It seems to spy on the Registry so as 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 fails, DevStudio will notify the user and refuse to load the DLL as an add-in. Consequently, if we implemented, say, two COM objects, one for DevStudio & one for Visual Studio, DevStudio would find that its QI fails for the second component, decide the DLL wasn't a valid add-in, and 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); we have no IDL for it.
Now, let's take a look at CAddIn
, the C++ class that incarnates the COM component CoAddIn
:
class ATL_NO_VTABLE CAddIn :
{
public:
CAddIn();
HRESULT FinalConstruct();
void FinalRelease();
# ifndef DOXYGEN_INVOKED
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
public:
void Configure();
void SayHello();
private:
CComPtr m_pDSAddIn;
CComPtr 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, we 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 ;
}
Note the noncreatable
tag; we 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
, we'll delegate 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 & 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 we 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 us to return IDTEExtensibilty2
. Fortunately, IDSAddIn
is custom, so there's no conflict there.
The next point of interest are the public
methods Configure
and SayHello
. The sample can only do two things: configure itself and say hello. This functionality resides in CAddIn
. The idea is to concentrate the AddIn'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 AddIn
into multiple hosts, they just make the add-in a little nicer.
When you point the DevStudio IDE at a DLL & 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 AddIn
is loaded, the vfFirstTime
parameter to OnConnect
won't be set to VARIANT_TRUE
. This is a problem because that's typically how we figure out that our add-in is being loaded for the first time, and hence when to carry out one-time setup tasks, like creating toolbars.
Toolbar creation is complicated because:
- We need to keep track ourselves whether or not it's been created
- Calling
AddCommandBarButton
when vfFirstTime
is false
will fail
I've solved problem #1 by just writing down a bool
ean in the Registry. I've solved problem # 2 by posting a message to a hidden message window (it turns out that AddCommandBarButton
will succeed if we 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. Using the standard AddIn
APIs, your new toolbar will be named Toolbar<n>
, where <n>
is the number of un-named toolbars (which is irritating and ugly). The Sample AddIn
instead hooks the toolbar creation and changes the window name to something a little nicer. Note: the new name must be no longer than the intended one (usually eight characters).
With that, let's look at CDSAddIn
, the C++ class that incarnates the DevStudio AddIn
:
class ATL_NO_VTABLE CDSAddIn :
public CComObjectRootEx,
public CComCoClass,
public ISupportErrorInfo,
public IDSAddIn
{
public:
...
void SetParam(CAddIn *pParent);
...
DECLARE_NO_REGISTRY();
DECLARE_ONLY_AGGREGATABLE(CDSAddIn)
...
BEGIN_COM_MAP(CDSAddIn)
COM_INTERFACE_ENTRY(IDSAddIn)
COM_INTERFACE_ENTRY(ISupportErrorInfo)
END_COM_MAP()
...
private:
...
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 our 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, our add-in will provide implementations of IDTEExtensibility2
and IDTECommandTarget
through another COM aggregate, CoDTEAddIn
. This coclass is incarnated by CDTEAddIn
:
class ATL_NO_VTABLE CDTEAddIn :
{
public:
enum Host
{
Host_Unknown,
Host_VS2003,
Host_VS2005,
Host_Excel2003,
};
public:
...
void SetParam(CAddIn *pParent);
...
DECLARE_NO_REGISTRY();
DECLARE_ONLY_AGGREGATABLE(CDTEAddIn)
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:
...
CComPtr<:_dte> m_pApp;
CComPtr<:_application> m_pExcel;
Host m_nHost;
CAddIn *m_pParent;
...
};
The first thing that should jump out at you is that the class knows the host into which it's been loaded. While Visual Studio *and* Office 2003 use this add-in programming model, hosting applications themselves offer different interfaces to *us*. We need to take this into account when requesting services from our host. We guess the host type in the OnConnection
method of the IDTExtensibility2
interface:
HRESULT hr = S_OK;
try
{
if (NULL == pApplication) throw _com_error(E_INVALIDARG);
if (NULL == pAddInInst) throw _com_error(E_INVALIDARG);
m_pAddIn = com_cast<:addin>(pAddInInst);
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);
...<:addin>
After validating our parameters, the first real work we do is wrapped up in the call to GuessHostType
; here is where we figure out what sort of environment we'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 we were given in OnConnection
for various sorts of interfaces:
CDTEAddIn::Host CDTEAddIn::GuessHostType(IDispatch *pApp)
{
HRESULT hr = S_OK;
EnvDTE80::DTE2 *pDTE2Raw;
hr = pApp->QueryInterface(EnvDTE80::IID_DTE2, (void**)&pDTE2Raw);
if (SUCCEEDED(hr))
{
m_pApp = com_cast<:_dte>(pApp);
pDTE2Raw->Release();
return Host_VS2005;
}
...<:_dte>
Note that we make no distinction between Visual Studio 2005 and 2008. It turns out that Visual Studio 2008 implements interface EnvDTE80::IID_DTE2
, and implements in a manner close enough to Visual Studio 2005 that, for our purposes, we 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 we 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 it's parent CAddIn
, for the same reasons.
Conclusion
These are the broad strokes; as I mentioned at the start, most of the work was in the details. I've attached a fully functional sample add-in that will load into DevStudio, Visual Studio 2003, Visual Studio 2005, Visual Studio 2008, and Excel 2003. It's a Visual Studio 2005 solution that contains the add-in itself, as well as its associated satellite DLL. To install it, just build either the Debug or Release configuration; there's a post-build step that will automatically register the DLL appropriately.
There's certainly more work that could be done; see Appendix A.
Enjoy-- questions, feedback, and suggestions are welcome.
Cache COM Component Creation
The primary COM component, CoAddIn
, implements different hosting models in terms of aggregated components. Today, both those components are instantiated in FinalConstruct
; it would be nice to move to some sort of cached scheme to avoid instantiating an instance of, say, CoDTEAddIn
when we're loaded into DevStudio...
Property Pages
Visual Studio 2003 and 2005 allow their add-ins to add pages to the dialogs they display in response to Tools | Options. You can tell Visual Studio about your page, or pages, by adding some additional Registry entries (take a look at vs2003.rgs or vs2005.rgs in the sample, or see here).
I had thought it would be nice to add a new property page to DevStudio 6 and Excel 2003, but I wasn't able to figure out how. My scheme was to install a CBT hook, and catch the creation of the Tools | Options Property Sheet. There, I'd post a message back to a private, message-only window that would create *my* Property Page and send a PSM_ADDPAGE
message to the Property Sheet, along with my new page.
For whatever reason, I got that to work in a little test app, but not in either Dev Studio 6 or in Excel 2003. In both cases, the Tools | Options Property Sheet does not have a Windows
Class of "Dialog" (like this one does), so perhaps these apps have some kind of non-standard implementation.
If anyone has any thoughts on this, or more success than I did, I'd love to hear about it.
It would also be cool to have the sheets that Visual Studio displays set their selection to our page when Configure is invoked. Currently, they open to the last page viewed. I've got some thoughts in terms of again installing a CBT hook to catch the sheet's creation & sending a message to its child tree control, but I haven't done anything on it. Jeff Paquette tells me he's used this successfully in his VisEmacs add-in, however.
Other Applications
I don't have a copy of Visual Studio 2002, so I couldn't test that. I implemented support for Excel 2003, but that's it. It would be cool to build out support for the whole suite.
Project Template
A project template for generating code for a common add-in would be nice.
Appendix B - Visual Studio Commands and Command Bars
Getting the commands added, and command bars setup was probably the most irritating part of writing this sample. In wading through this mess, I relied heavily on the article "HOWTO: Adding buttons, commandbars and toolbars to Visual Studio .NET from an add-in", by Carlos J. Quintero [2].
Carlos describes two distinct flavors of Visual Studio Command Bar: permanent & temporary.
Permanent Command Bars:
- Remain visible in the IDE even if the add-in is unloaded through the add-in manager
- Are created once (when the
OnConnection
method receives the value ext_ConnectMode.ext_cm_UISetup
... which happens only once after an add-in is installed on a machine)
- Are added through
DTE.Commands.AddCommandBar()
- Are removed only when the add-in is uninstalled (not when it is unloaded) through a custom action in the uninstaller using the
DTE.Commands.RemoveCommandBar()
function
Temporary Command Bars:
- Are created each time the add-in is loaded using the
DTE.CommandBars.Add()
or CommandBar.Controls.Add()
functions (depending on the type of commandbar: Toolbar
or CommandBarPopup
)
- Are removed by the add-in each time it is unloaded, using the
CommandBar.Delete()
method
According to Carlos, the fact that Permanent Command Bars remain even when the user unloads the AddIn "will be confusing for many users" & consequently, "most add-ins don't use this approach". He lays out the following approach:
If ext_cm_AfterStartup or ext_cm_Startup
Check for the command's existence through Commands::Item
If not there, create it via Commands::AddNamedCommand for
both 2003 & 2005
Create a new (temporary) command bar by calling
pTempCmdBar = ICommandBars::Add() (both 2003 & 2005!)
Add a button:
pTempCmdBar->AddControl
pTempCmdBar->Visible = true;
Then call pTempCmdBar->Delete() in OnDisconnect
Note that he just ignores ext_cm_UIStartup
entirely.
Now, to me, temporary toolbars seem fine, except for two problems:
- The toolbar re-appears every time Visual Studio is started, no matter if the user turned it off.
- The commands are still there, even if you un-register the
AddIn
; that's not so bad... the IDE detects this and removes the commands the next time they're invoked.
Permanent toolbars respect the user's decision to turn them off, but if you un-register the AddIn
and delete the commands, the damn thing is *still* there and you can't delete it!
I still haven't settled on the "right" solution. The sample AddIn
can use a few different approaches, depending on the setting of the #define SAMPLECAI_COMMAND_BAR_STYLE
. It can take one of three values:
SAMPLECAI_COMMAND_BAR_TEMPORARY
Uses temporary command bars
SAMPLECAI_COMMAND_BAR_PERMANENT
Uses permanent command bars
SAMPLECAI_COMMAND_BAR_SEMIPERMANENT
Uses permanent command bars, but deletes the Command Bar programmatically on un-install
Appendix C - Resetting Visual Studio
During the course of developing your AddIn
, you'll likely have times where you want to just re-set all the UI changes your AddIn
's made to Visual Studio.
For Visual Studio 2005, you can run devenv /resetaddin <AddInNamespace.Connect>
. Unfortunately, Visual Studio 2003 only offers devenv.exe /setup
, which will re-set *all* your UI customizations (including your keybindings!). Since that's a bit draconian, I whipped up this little VBS script:
Dim objDTE
Dim objCommand
Dim objTb
On Error Resume Next
Set objDTE = CreateObject("VisualStudio.DTE.7.1")
If objDTE Is Nothing Then
MsgBox "Couldn't find VS 2003"
Else
Set objCommand = objDTE.Commands.Item("SampleCAI.CoAddIn.Configure")
If objCommand Is Nothing Then
MsgBox "The Configure command has already been deleted."
Else
objCommand.AddControl(objDTE.CommandBars.Item("Tools"))
objCommand.Delete
End If
Set objCommand = objDTE.Commands.Item("SampleCAI.CoAddIn.SayHello")
If objCommand Is Nothing Then
MsgBox "The SayHello command has already been deleted."
Else
objCommand.AddControl(objDTE.CommandBars.Item("Tools"))
objCommand.Delete
End If
Set objTb = objDTE.CommandBars.Item("SampleCAI")
If objTb Is Nothing Then
MsgBox "No (permanent) command bar named SampleCAI."
Else
objDTE.Commands.RemoveCommandBar(objTb)
objTb.Delete
set objTb = Nothing
End If
objDTE.Quit
set objDTE = Nothing
End If
Set objDTE = CreateObject("VisualStudio.DTE.8.0")
If objDTE Is Nothing Then
MsgBox "Couldn't find VS 2005"
Else
Set objCommand = objDTE.Commands.Item("SampleCAI.CoAddIn.Configure")
If objCommand Is Nothing Then
MsgBox "The Configure command has already been deleted."
Else
objCommand.Delete
End If
Set objCommand = objDTE.Commands.Item("SampleCAI.CoAddIn.SayHello")
If objCommand Is Nothing Then
MsgBox "The SayHello command has already been deleted."
Else
objCommand.Delete
End If
Set objTb = objDTE.CommandBars.Item("SampleCAI")
If objTb Is Nothing Then
MsgBox "No (permanent) command bar named SampleCAI."
Else
objDTE.Commands.RemoveCommandBar(objTb)
objTb.Delete
set objTb = Nothing
End If
objDTE.Quit
set objDTE = Nothing
End If
References
- Undocumented Visual C++ by Nick Hodapp
- Adding buttons, commandbars and toolbars to Visual Studio .NET from an add-in
- Building an Office2K COM addin with VC++/ATL
History
- October 1st, 2006: Initial post
- April 26th, 2008: Article updated