|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionThis 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. BackgroundI'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-InThe 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, 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 Likewise, when Visual Studio 2003, 2005, 2008 or Office instantiate our component, they'll ask our component for the 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. 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-InSampleCAI.cpp is boilerplate ATL code; it implements [
uuid(5f4e04a1-1a92-11db-89d7-00508d75f9f1),
helpstring("Common Add-In Sample Add-In Object")
]
coclass CoAddIn
{
[default] interface IUnknown /*IDSAddIn*/;
}
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 In any event, DevStudio determines what COM components are exported by your DLL and calls Therefore, SampleCAI.dll exports one and only one COM component, Now, let's take a look at 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
As you can see, [
uuid(5f4e04a2-1a92-11db-89d7-00508d75f9f1),
helpstring("Common AddIn Sample DevStudio 6 AddIn Object"),
noncreatable
]
coclass CoDSAddIn
{
[default] interface IUnknown /*IDSAddIn*/;
}
Note the The upshot is that when DevStudio calls
[
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 The next point of interest are the DevStudio 6 HostingI've done a few non-standard things with respect to hosting within DevStudio. These aren't, strictly speaking, necessary in terms of loading the When you point the DevStudio IDE at a DLL & ask to load it as an 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 Toolbar creation is complicated because:
I've solved problem #1 by just writing down a 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 With that, let's look at class ATL_NO_VTABLE CDSAddIn :
public CComObjectRootEx
For the most part, this is a straightforward ATL COM class implementing Notes:
Visual Studio and Office HostingAs described above, our add-in will provide implementations of 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<:_dte> m_pApp;
/// Reference to our host's Application object
CComPtr<:_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.<:_dte><:_application>
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 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 AddIn object representing us,
m_pAddIn = com_cast<:addin>(pAddInInst);
// & 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);
...<:addin>
After validating our parameters, the first real work we do is wrapped up in the call to
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<:_dte>(pApp);
pDTE2Raw->Release();
return Host_VS2005;
}
// Ok-- maybe it's Visual Studio 2003...
...<:_dte>
Note that we make no distinction between Visual Studio 2005 and 2008. It turns out that Visual Studio 2008 implements interface The goal is to fill in void CDTEAddIn::AddCommands(AddInDO::ext_ConnectMode nMode)
{
switch (m_nHost)
{
case Host_VS2003:
AddCommandsVS2003(nMode);
break;
case Host_VS2005:
AddCommandsVS2005(nMode);
break;
...
Notes:
ConclusionThese 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. Appendix A - Future WorkCache COM Component CreationThe primary COM component, Property PagesVisual 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 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 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 ApplicationsI 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 TemplateA project template for generating code for a common add-in would be nice. Appendix B - Visual Studio Commands and Command BarsGetting 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:
Temporary Command Bars:
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 Now, to me, temporary toolbars seem fine, except for two problems:
Permanent toolbars respect the user's decision to turn them off, but if you un-register the I still haven't settled on the "right" solution. The sample
Appendix C - Resetting Visual StudioDuring the course of developing your For Visual Studio 2005, you can run 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.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
References
History
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||