Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Common Add-Ins

0.00/5 (No votes)
28 Apr 2008 1  
Writing add-ins for DevStudio, Visual Studio, and Office

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:

Class diagram

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 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 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 :
    // 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 m_pDSAddIn;
    /// Reference on our aggregated instance of CDTEAddIn
    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 /*IDSAddIn*/;
  }

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:

  1. We need to keep track ourselves whether or not it's been created
  2. Calling AddCommandBarButton when vfFirstTime is false will fail

I've solved problem #1 by just writing down a boolean 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:
    ...
    /// 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 our commands, and sinks any DevStudio events it's interested in. In IDSAddIn::OnDisconnection, it severs that connection.

Notes:

  1. The COM coclass CoDSAddIn is not directly creatable; it's not even registered, and in any event creation will fail without an aggregator.
  2. 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 :
    // 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&lt;:_dte&gt; m_pApp;
    /// Reference to our host's Application object
    CComPtr&lt;:_application&gt; 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&gt;<:_application&gt;

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;            // 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&lt;:addin&gt;(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&gt;

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;

    // 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&lt;:_dte&gt;(pApp);
      pDTE2Raw->Release();

      return Host_VS2005;
    }

    // Ok-- maybe it's Visual Studio 2003...
    ...<:_dte&gt;

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:

  1. The COM coclass CoDTEAddIn is *not* directly creatable; it's not even registered, and in any event creation will fail without an aggregator
  2. 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.

Appendix A - Future Work

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:

  1. Remain visible in the IDE even if the add-in is unloaded through the add-in manager
  2. 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)
  3. Are added through DTE.Commands.AddCommandBar()
  4. 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:

  1. 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)
  2. 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:

  1. The toolbar re-appears every time Visual Studio is started, no matter if the user turned it off.
  2. 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:

  1. SAMPLECAI_COMMAND_BAR_TEMPORARY Uses temporary command bars
  2. SAMPLECAI_COMMAND_BAR_PERMANENT Uses permanent command bars
  3. 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&gt;. 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.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

  1. Undocumented Visual C++ by Nick Hodapp
  2. Adding buttons, commandbars and toolbars to Visual Studio .NET from an add-in
  3. Building an Office2K COM addin with VC++/ATL

History

  • October 1st, 2006: Initial post
  • April 26th, 2008: Article updated

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here