Visual Studio .NET provides an incredible leap forward from its predecessor in terms of functionality, but eventually, every developer finds a sought-after feature that just seems overlooked.

VS .NET provides great features and capabilities, from intelligent wizards to very useful drag-and-drop functionality such as that provided by the Server Explorer tool window. If there's a problem, it could be one of too much success. A simple stroll through the IDE reveals feature after productivity-inducing feature, providing the developer with a high set of expectations. Eventually, you may find that an expected feature is missing. Fortunately, Microsoft included a very powerful extensibility model, allowing integration of new, custom features directly into the IDE.

Microsoft has provided extensibility and automation object models for most of their products for as long as one can remember. For example, the automation model exposed by Microsoft Office provides the foundation of the entire VBA community. When you work within the world of the developer, the strength and power of the developer's tools will dictate productivity. The ceiling on productivity achievable by a developer bears a high-correlation with the number and usefulness of the features provided by the IDE. If you make the IDE better, developers will rejoice. Herein lays a major problem. For all the R&D money and general efforts placed into the design and development of it's products, Microsoft understands that it can never anticipate all the features the entire developer community might need or want. If they attempted to achieve that goal, the natural result would invariably be to occasionally actually ship an IDE?maybe once a decade or so.

Compiling add-ins into assemblies highlights a fundamental difference: macros run out-of-process with the IDE, while a DLL containing an add-in class achieves better performance in running within the IDE.

This makes me think of the analogy of the fisherman. Give a man a fish, and he will eat for a day. Teach him to fish, and he will eat for a lifetime. In similar fashion, if you trade fish for features, the moral of the story stands. It's better to give the developer the tools to create new, custom features, than to try and anticipate all the features that developers need. This is where the extensibility model of Visual Studio .NET, found in the EnvDTE and Extensibility namespaces, plays a role. Using these namespaces as a primary toolkit, the power developer will create extended capability and features, in the form of add-ins, that evolve the IDE into the customized power tool that can address all of the developer's needs. In essence, if the developer plays the role of the fisherman, than the extensibility model is the net in .NET.

A Brief Comparison to Macros

Before jumping into the guts of what an add-in entails, or how the architecture of the IDE interacts with these special classes, let me explain the relationship that macros and add-ins have to one another. Macros play a special role by providing the developer with a quick and simple mechanism for defining custom behavior and functionality. Macros allow the development of custom functionality, but carry significant limits in terms of deployment, performance, and functionality constraints. Both fundamentally use the same extensibility API, EnvDTE, to perform all of their fancy footwork. Similarities tend to end there. For example, the language used to write macros within the IDE is limited to Visual Basic .NET. Since macros tend to exist to perform simpler, more straight-forward tasks of an automation nature within the IDE, using Visual Basic .NET as the macro language provides a more natural fit. This approach mirrors the “Great for simplicity” strategy, but consequently falls short of providing an ideal situation for the predominantly C# developer.

VS .NET saves macros in files with a .vsmacros extension, while add-ins are .NET classes compiled into DLL assemblies. Compiling add-ins into assemblies highlights a fundamental difference: macros run out-of-process with the IDE, while a DLL containing an add-in class achieves better performance by running within the IDE. Macros run within a vsmsvr.exe process, in comparison to the IDE, or devenv.exe. You can see this in Figure 1 where I recorded and then ran a macro while I had the Task Manager open. Compiling add-ins into assemblies also provides a layer of intellectual property protection (security) that is not afforded to macros, which are essentially distributed along with their source code.

Figure 1: The Visual Studio macro server process, vsmsvr.exe, handles the execution of macros outside of the IDE process, devenv.exe
Figure 1: The Visual Studio macro server process, vsmsvr.exe, handles the execution of macros outside of the IDE process, devenv.exe

Macros and add-ins also leverage completely separate design environments. You write macros within their own macros IDE, or vsaenv.exe. Macro development most commonly begins by initiating a macro recording session. You can tweak the resulting code auto-generated by the macro recorder into what best suites your current needs. Contrasting this with add-in development, the Add-In wizard will be the normal starting point for most developers developing an Add-In. When you select the Visual Studio .NET Add-In project type shown in Figure 3 to add a new project to a solution, the Add-In wizard will be invoked.

Figure 2: You can use the MakeAddin macro sample, found in the Macros IDE, to convert macros into add-ins.
Figure 2: You can use the MakeAddin macro sample, found in the Macros IDE, to convert macros into add-ins.
Figure 3: You'll find the Visual Studio .NET Add-in project type in the Extensibility Projects folder of the Add New Project dialog box.
Figure 3: You'll find the Visual Studio .NET Add-in project type in the Extensibility Projects folder of the Add New Project dialog box.

While the Add-In wizard provides an adequate starting point, it basically creates a minimal shell?it doesn't generate any significant implementation code. Additionally, while you might write macros completely from scratch, without aid of the macro recorder, it is rare that you would create an add-in without using the Add-In wizard as a starting point. Although this is the case, there is no technical reason why you must use the Add-In wizard. Later I'll discuss the tasks performed for you by the Add-In wizard in more detail, but first let's explore the basic architecture of a Visual Studio .NET add-in.

Basic Add-In Architecture

The team responsible for designing the extensibility API in Visual Studio .NET stipulated few requirements for a .NET class to have the ability to function as an add-in. The Visual Studio .NET Add-In Project type provides a wizard to generate a great deal of basic coding elements that have the tendency to hide the bare essence of what is actually required. Later I'll show you the results of using this Project type as a starting point for your add-in, but for now, let's stick just to the bare facts.

Common Requirements

The immediate requirement is that you should with a Class Library project. You should add a single class to this project. You will need to reference three primary assemblies that provide interfaces and UI elements used by most add-ins. With the Add Reference wizard, you should add references for:

  • extensibility.dll
  • envdte.dll
  • office.dll

These assemblies will provide the core interfaces you will need to implement and the access to the primary interop assembly for the Office menu controls, allowing use by your add-in of the core set of UI controls used by Visual Studio .NET for the command bars, context menus, and menu items.

Visual Studio .NET requires an add-in compiled as a .NET assembly to be registered as a COM object.

Most add-in projects will greatly benefit from the use of a core set of imports statements at the top of the code file.

Imports Microsoft.Office.Core
imports Extensibility
imports System.Runtime.InteropServices
Imports EnvDTE

Meeting the Three Primary Requirements

The public assembly extensibility.dll, shown in Figure 4, contains a single element, the IDTExtensibility2 interface. In order to write a class to function as an add-in, you should first start by implementing IDTExtensibility2 and its five methods: OnConnection, OnDisconnection, OnAddInsUpdate, OnStartupComplete, and OnBeginShutdown. As far as coding goes, this is the only requirement. Two more requirements exist in order to allow Visual Studio .NET to be aware of and load your type as an add-in.

Figure 4: The assembly Extensibility.dll contains a single interface, IDTExtensibility2, that you must implement with an add-in.
Figure 4: The assembly Extensibility.dll contains a single interface, IDTExtensibility2, that you must implement with an add-in.

The most surprising requirement for a .NET class to function as an add-in seems to be a contradiction to everything that is managed in nature. Visual Studio .NET still contains a great deal of functionality that is based on unmanaged code. This extends to the ability to load .NET add-ins. Visual Studio .NET requires an add-in compiled as a .NET assembly to be registered as a COM object. The IDE expects to instantiate a COM object for each add-in. Since .NET provides a very robust COM interoperability set of technologies built-in, you will need to do nothing beyond ensuring that you've registered your .NET class in the registry as a COM object.

You can create the required registry keys needed for a .NET class to register as a COM class by using the .NET Framework SDK utility regasm.exe.

Regasm /codebase SnippetLoader.dll   

The Regasm utility will ensure the correct registry entries exist. Using Regasm is a manual process and not suitable for all situations. The most common and best way to ensure this is accomplished is to create a new Class Library project and set the project properties to register for COM interoperability.

Visual Studio .NET is made aware of add-ins by the presence of a registry entry similar to the following: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\7.1\AddIns<ProgID*>.* The ProgID used in this registry key name is the value used when the .NET class was registered for COM interoperability.

The team responsible for designing the extensibility API in Visual Studio .NET stipulated few requirements for a .NET class to have the ability to function as an add-in.

You should place several values (shown in Figure 5) in this registry entry to control the behavior of an add-in. Placing this key in HKEY_LOCAL_MACHINE classifies the add-in as an administrator add-in. To make an add-in available to only the current user as a user add-in, HKEY_LOCAL_MACHINE should be replaced with HKEY_CURRENT_USER. Table 1 lists the optional name/value pairs that you can use in this registry entry.

Figure 5: The registry key used for an add-in contains several values which dictate add-in behavior and provide add-in details.
Figure 5: The registry key used for an add-in contains several values which dictate add-in behavior and provide add-in details.

You need to understand some of the idiosyncrasies of the values you can assign to these named-value pairs. Failing to provide a FriendlyName named-value, Visual Studio .NET will substitute the <ProgID> used for the .NET class for COM Interoperability. Two values, AboutBoxDetails and AboutBoxIcon, are required in order to receive recognition in the Installed Products listing on the About dialog box for Visual Studio .NET shown in Figure 6. If you didn't provide a value for FriendlyName, .NET will use the <ProgId> used for COM interoperability as the FriendlyName in both the Installed Products list shown on the About dialog and in the Add-in Manager dialog box.

Figure 6: The AboutBoxDetails and AboutBoxIcon allow you to include add-ins in the list of Installed Products shown in the Visual Studio .NET About dialog box.
Figure 6: The AboutBoxDetails and AboutBoxIcon allow you to include add-ins in the list of Installed Products shown in the Visual Studio .NET About dialog box.

Understanding the Preload Process

The behavior involved in preloading add-ins will not seem intuitive at first. If you follow the SDK documentation regarding the use and behavior of CommandPreload, you will likely be left scratching your head. Two key registry entries work together to provide both Administrator and User add-ins to accomplish a single preload event for any add-in requiring UI or general add-in initialization upon first load.

The entire decision logic tree followed by Visual Studio .NET is shown in Figure 7. The preload logic requires the use of the user-specific registry key PreloadAddinState. The path for this key is HKEY_CURRENT_USER\SOFTWARE\Microsoft\VisualStudio\7.1\AddIns. Within this key you should place DWORD values with names matching the ProgID of your add-in. The value will be used as a flag to track whether an add-in requiring preloading has been initialized.

Figure 7: The CommandPreload and PreloadAddinState named-values work together to allow both Administrator and User add-ins to configure for a single preload.
Figure 7: The CommandPreload and PreloadAddinState named-values work together to allow both Administrator and User add-ins to configure for a single preload.

If you create an administrator add-in available to all users and registered in HKEY_LOCAL_MACHINE, the value of CommandPreload should generally be set to 0x1. Setting this value to 0x0 has the effect of doing nothing and results in the same behavior as if the named-value was completely missing. For an administrator, Visual Studio .NET will never change this value. Changing to 0x0 or 0x2, from 0x0, would result in future users loading the IDE failing to have the add-in initialized for them. This would not be good. Instead, the state of preloading for each user is tracked in the PreloadAddinState key.

Visual Studio .NET will treat add-ins installed as user add-ins differently. It will be made aware of the available add-in directly by a dedicated subkey in the \Addins key under the HKEY_CURRENT_USER tree. For user add-ins, the state of previous preloading will be tracked by changing the value of CommandPreload directly from 0x1 to 0x2.

Useful Attributes to Use

COM interoperability plays a big role in the IDE's add-in architecture. Instead of relying on the ProgID and GUID chosen for you by the IDE, you should use a few attributes to take control of that process. Apply the ProgIdAttribute and GuidAttribute classes to your add-in type to set the values used.

&lt;GuidAttribute("3D2A136E-...-5A1B22C90627"), _
 ProgIdAttribute("FromScratch.MyAddin")&gt; _
Public Class MyAddIn
    Implements Extensibility.IDTExtensibility2

The ProgIdAttribute value you supply will naturally be used for the custom subkey under \Addins and, in the case of a user add-in, the name of the DWORD value added automatically by Visual Studio .NET under the PreloadAddinState key in the HKEY_CURRENT_USER tree. You should also leverage the GuidAttribute to maintain better control over the GUID used when registering your .NET class as a COM class.

Remember to change the \AddIns subkey for your add-in any time you modify the ProgIDAttribute that you apply. Changing this attribute and solely rebuilding will not automatically change the name of the subkey you created to notify Visual Studio .NET of the existence of your add-in.

Breaking Down the Interfaces

Writing add-ins for Visual Studio .NET will involve two interfaces: IDTExtensibility2, contained with the Extensibility namespace provided by Extensibility.dll, and IDTCommandTarget, contained with the EnvDTE namespace provided by EnvDTE.dll. The members exposed by these interfaces will expose your add-in's primary communication channel with the IDE during setup, and if you are declaring named commands, during runtime upon invocation of your named commands.

Providing Core IDE Communication

The IDTExtensibility2 interface implemented by an add-in is the same interface you used to write COM add-ins for Office XP. For some readers this will provide a welcome feeling of familiarity. You use this interface to provide a communication mechanism between the IDE and your code. This interface contains five method members, with two primary methods used to signal the loading and unloading of your add-in code.

You'll start this communication with the OnConnection method. Within this method, Visual Studio .NET will provide an object reference to the IDE itself. You should cast the initial object parameter passed into the method into the core type provided by the EnvDTE namespace, DTE.

Dim IDE As EnvDTE.DTE

Public Sub OnConnection( _ 
         ByVal Application As Object, _ 
         ByVal ConnectMode As ext_ConnectMode, _
         ByVal AddInInst As Object, _ 
         ByRef custom As System.Array) _
     Implements IDTExtensibility2.OnConnection
     
   IDE = CType(Application, EnvDTE.DTE)

The primary actions performed by most add-ins involve access and manipulation of objects contained within the EnvDTE namespace. The instance of the DTE object passed into OnConnection will provide you this ability. The second parameter will provide instruction to your add-in regarding the reason the add-in was loaded. The second parameter uses the enumeration type ext_ConnectMode found in the Extensibility namespace. You should use either an If..Then or Select statement to provide behavior appropriate for each add-in loading scenario. For a simple add-in, you will most likely limit the code in OnConnection to setting up your initial UI. The third parameter provides an object reference to your add-in's own object instance, of type EnvDTE.AddIn, and the final parameter will generally be of limited value to you. Currently, the IDE passes an empty array for the final parameter of the OnConnection method.

The second most important method is the OnDisconnection method. This method indicates the end of an add-in's life. The add-in architecture in Visual Studio .NET will result in the add-in class being loaded at least twice. The removeMode parameter uses the enumeration type ext_DisconnectMode found in the Extensibility namespace. You should use the removeMode parameter to respond appropriately for the scenario in which your add-in was unloaded. You should use this method for any required clean up proceedings.

The final three methods of the IDTExtensibility2 interface will normally be of limited use. Use OnStartupComplete when your initialization code, normally placed in the OnConnection method, needs to access IDE components. The IDE automation model objects you access, found in the EnvDTE namespace, may not yet be available; therefore you should place initialization code in OnStartupComplete to compensate for IDE component dependencies in your add-in initialization routines.

The OnAddInsUpdate method will interrupt your add-in to inform your code that another add-in has either been loaded or unloaded. Use this method to respond to a changing add-in landscape. Do not get your hopes up about the direct usability of this method to adjust to add-in dependency scenarios; only one parameter is provided and this parameter provides no information as to the source or nature of the change in add-in landscape.

The OnBeginShutdown is called only if Visual Studio .NET attempts to shutdown while an add-in is running. Shutdown at this point is irreversible and this method should be used only to clean up items prior to the inevitable IDE shutdown.

Implementing Named Commands

Use the IDTCommandTarget interface only if you intend to provide one or more custom named commands. A named command with Visual Studio .NET is merely a piece of functional code that is assigned a well-known name within the context of the entire collection of commands (or macros) programmatically accessible, or also invoked directly through the IDE's Command window. Almost every action a developer can initiate within the IDE is made available in the form of a named command. To see this in action, open the Command window and type View.FullScreen. This will change your IDE configuration to the Full Screen mode.

The benefit of exposing add-in functionality as a named command will be two-fold. As a convenience, or for programmatic accessibility of your add-in's functionality, a named command is the standard mechanism the developer will use. The second benefit will stem from the ease of adding menu items to context menus or command bars through use of the AddControl method of the EnvDTE.Command object.

Dim oAI As AddIn = CType(addInInst, AddIn)
Dim oCmd As Command
Try
    oCmd = IDE.Commands.AddNamedCommand( _ 
               oAI, "CmdName", "ButtonText", _    
               "Tooltip", True, 59, Nothing, _
               1 + 2)
                     
    oCmd.AddControl( _      
                IDE.CommandBars.Item("Tools"))
Catch e As System.Exception
End Try

The IDTCommandTarget interface exposes two method members, Exec and QueryStatus, which you will implement. You will need to implement both so that you can respond properly to the IDE when Visual Studio .NET invokes your named commands. The QueryStatus method implementation provided by your add-in will be called in three scenarios:

  • Your named command is manually entered into the Command window
  • The ExecuteCommand method of the core EnvDTE.DTE object is used to request your named command
  • The IDE needs to draw a menu item associated with your named command through the original call to AddControl.

You should assign a value from the vsCommandStatus enumeration to the statusOption parameter being passed by reference. Your value should indicate the command is both available (vsCommandStatusEnabled) and supported (vsCommandStatusSupported). This will instruct the IDE to invoke the Exec method, providing your add-in the chance to handle the command request.


StatusOption = CType( _ 
    vsCommandStatus.vsCommandStatusEnabled + _
    vsCommandStatus.vsCommandStatusSupported, _
    vsCommandStatus)

You should place the necessary named command implementation code within the Exec method. You need a logic check to guarantee that the proper code is executed for the command being requested.

If cmdName = "FromScratch.MyAddIn.CmdName" Then
    handled = True
    Exit Sub
End If

The actual name of your command will be a combination of the ProgID set with the ProgIdAttribute class applied to your add-in class and the second parameter passed into the AddNamedCommand.

Using the Add-in Wizard

Developing your first add-in from scratch will be a very useful exercise. Visual Studio .NET provides a Visual Studio .NET Add-In Project type shown in Figure 3. This project type will invoke a wizard that will ask you several common questions about the nature of the add-in you are about to develop. The questions are basic and will be used to customize the most common elements an add-in will need. The nicest thing about this wizard is that it will also create a Visual Studio .NET Setup Project that includes all the necessary registry entries shown in Figure 5.

Several pages of the wizard will collect simple data points that map directly to assorted registry entries. Page 1 of this wizard will ask which language you need to use, impacting the final output of this wizard. Page 2 will ask if your add-in should be available to Visual Studio .NET and/or the Macros IDE. Your answer on this page will impact only the registry path the \AddIns subkey modifications will impact. You should modify these settings post-wizard by changing the registry settings the generated Setup Project will add to the target system upon installation. Page 3 will ask for a name and description for your add-in. Visual Studio will use these values to populate the FriendlyName and Description values for your add-in. Page 5 will provide you an option to include an About box information. Choosing yes will result in AboutBoxIcon and AboutBoxDetails values being included in your add-in's registry subkey.

The only slightly interesting options will be presented on Page 4 of the Add-In wizard shown in Figure 8. You will be given four options on Page 4. Check the option, “Yes, create a Tool's menu item,” if you want the wizard to provide base implementation code for the IDTCommandTarget interface and have a named command declared by your add-in by code placed in the IDTExtensibility2.OnConnection method. You should check this option, as it will provide you a good starting point that you can tweak to modify command bars other than “Tools.”

Figure 8: The slightly interesting portion of the Visual Studio .NET Add-In wizard is presented on Page 4, where you are given the only option that impacts the initial code placed into your add-in.
Figure 8: The slightly interesting portion of the Visual Studio .NET Add-In wizard is presented on Page 4, where you are given the only option that impacts the initial code placed into your add-in.

Check “My Add-in will never put up a modal UI….” if you want to add the CommandLineSafe value to the registry settings for your add-in. The option, “I would like my add-in to load when the host application starts,” maps directly to the value used for the registry value LoadBehavior. The final option on Page 4, under the section called “Setting up access privileges,” will determine whether the registry entries for your add-in will be placed under the HKEY_LOCAL_MACHINE or HKEY_CURRENT_USER trees.

The wizard sets up the most common and simplest of elements for your add-in, but the result of the add-in provides you with the bare essence of a starting point.

The implementation code generated by the wizard will go to extremes to check on conditions, settings, and enumeration values. Many of these checks are unnecessary, but no harm will result if you leave them unchanged. In general, you should leave them alone.

Summary

The extensibility model provided by Visual Studio .NET provides a powerful way to build custom value-add features that can integrate seamlessly into the IDE. You can develop an add-in class by hand, with little effort, but you will need to develop a little hands-on experience to learn the idiosyncrasies of add-in architecture. A few elements of the architecture work differently in Visual Studio .NET 2002 and do not coincide with the documentation. Most of these quirks were corrected in Visual Studio .NET 2003. Independent of the minor quirks you will find here and there, the model is accessible, usable, powerful, and opens the door to development of that crucial piece of functionality that lands at the top of your wish list, but currently does not exist “out-of-the-box.”

This article covered the purpose, usage, and members of the IDTExtensibility2 interface comprising the sole contents of the extensibility.dll assembly. The primary methods OnConnection and OnDisconnection were covered. The concept of named commands was covered, including the usage and implementation details of the IDTCommandTarget interface, provided by the core IDE extensibility namespace EnvDTE contained within the EnvDTE.dll assembly.

In the space afforded a magazine article, you can barely scratch the surface of what you can code an add-in to accomplish. The EnvDTE automation object model for Visual Studio .NET provides over 140 different objects for working with and manipulating the IDE and close to anything within it. I have mentioned many of the common changes an add-in might create to impact the IDE, such as multi-level menu item hierarchies, modifying the UI without using named commands, interaction directly with project files or individual tool windows, or even the ability to hook IDE events and respond to user interactions with the IDE itself. All together, an enormous world of IDE customization awaits you. In the process of deconstructing the basic add-in architecture, you have been handed the keys….time to take the IDE out for that 200 mph test drive all developers deserve.

Michael Lane Thomas - The .NET Cowboy

Listing 1: Sample add-in with minimized code listings that defines and handles a single named command

Imports EnvDTE
Imports Microsoft.Office.Core
Imports System.Runtime.InteropServices
Imports System.Reflection
Imports Extensibility

&lt;GuidAttribute("3D2A136E-AE04-49d0-80B1-5A1B22C90627"), _
 ProgIdAttribute("MLT.SnippetLoader")&gt; _
Public Class MyAddIn
    Implements IDTExtensibility2, IDTCommandTarget
    Dim IDE As EnvDTE.DTE
    Dim oAI As EnvDTE.AddIn

    Dim oCmd As Command
    Shared Snippets As Hashtable

    Public Sub OnConnection(ByVal Application As Object, _ 
                      ByVal ConnectMode As ext_ConnectMode, _ 
                      ByVal AddInInst As Object, _ 
                      ByRef custom As System.Array) _
       Implements IDTExtensibility2.OnConnection
       
        IDE = CType(Application, EnvDTE.DTE)
        If ConnectMode = ext_ConnectMode.ext_cm_UISetup Then
            oAI = CType(AddInInst, AddIn)
            Try
                oCmd = IDE.Commands.AddNamedCommand( _
                    oAI, "Toggle", "Toggle Snippets", _
                    "Toggles loading of Custom Snippets", _
                    True, 59, Nothing, 1 + 2)
                oCmd.AddControl(IDE.CommandBars.Item("Tools"))
                CacheSnippets()
            Catch e As System.Exception
            End Try
        End If
    End Sub

    Private Sub CacheSnippets()
        Snippets = New Hashtable
        Dim cfgFile As String
        cfgFile = [Assembly].GetExecutingAssembly.Location + _
                   ".tabs"
        Try
            If System.IO.File.Exists(cfgFile) Then
                Dim tabs As New Xml.XmlDocument
                tabs.Load(cfgFile)
                Dim snip As Xml.XmlNode
                For Each snip In _ 
                        tabs.SelectNodes("/snippets/snippet")
                    Snippets.Add(snip.Attributes("name").Value, _
                                 snip.InnerText)
                Next
            End If
        Catch ex As Exception
        End Try
    End Sub

    Public Sub OnDisconnection( _ 
                     ByVal RemoveMode As ext_DisconnectMode, _ 
                     ByRef custom As System.Array) _
            Implements IDTExtensibility2.OnDisconnection
    End Sub

    Public Sub OnStartupComplete(ByRef custom As System.Array) _
            Implements IDTExtensibility2.OnStartupComplete
    End Sub

    Public Sub OnAddInsUpdate(ByRef custom As System.Array) _
            Implements IDTExtensibility2.OnAddInsUpdate
    End Sub

    Public Sub OnBeginShutdown(ByRef custom As System.Array) _
            Implements IDTExtensibility2.OnBeginShutdown
    End Sub

    Public Sub Exec(ByVal CmdName As String, _ 
                    ByVal ExecuteOption As vsCommandExecOption, _ 
                    ByRef VariantIn As Object, _ 
                    ByRef VariantOut As Object, _ 
                    ByRef handled As Boolean) _
            Implements IDTCommandTarget.Exec
        Dim wTbx As Window = _
               IDE.Windows.Item(Constants.vsWindowKindToolbox)
        Dim oTbx As ToolBox = wTbx.Object
        Dim tab, myTab As ToolBoxTab
        For Each tab In oTbx.ToolBoxTabs
            If tab.Name = "SnippetLoader" Then
                myTab = oTbx.ToolBoxTabs.Item("SnippetLoader")
                Exit For
            End If
        Next
        Dim snip As Object
        If myTab Is Nothing Then
            myTab = oTbx.ToolBoxTabs.Add("SnippetLoader")
            For Each snip In Snippets.Keys
                myTab.ToolBoxItems.Add( _ 
                     snip.ToString, _
                     Snippets(snip).ToString, _
                     vsToolBoxItemFormat.vsToolBoxItemFormatText)
            Next
        Else
            myTab.Delete()
        End If
        handled = True
    End Sub

    Public Sub QueryStatus( _ 
              ByVal CmdName As String, _ 
              ByVal NeededText As vsCommandStatusTextWanted, _ 
              ByRef StatusOption As vsCommandStatus, _ 
              ByRef CommandText As Object) _
            Implements IDTCommandTarget.QueryStatus
        StatusOption = _  
              CType(vsCommandStatus.vsCommandStatusEnabled + _
                    vsCommandStatus.vsCommandStatusSupported, _
                    vsCommandStatus)
    End Sub
End Class

Listing 2: Sample XML configuration file (.tabs) used by add-in

&lt;?xml version="1.0" encoding="utf-8" ?&gt;
&lt;snippets&gt;
&lt;snippet name="Copyright"&gt;
' Michael Lane Thomas Copyright 2003
&lt;/snippet&gt;
&lt;snippet name=".NET Cowboy URL"&gt;
' <a href="http://blogs.gotdotnet.com/mlthomas";>http://blogs.gotdotnet.com/mlthomas<;/a>
&lt;/snippet&gt;
&lt;/snippets&gt;

Table 1: Optional name/value pairs used in the add-in registry key.

Registry ValuesDescription
AboutBoxDetailsA description string displayed in the Visual Studio .NET About dialog box.
AboutBoxIconA flexibility format value used as the Icon data. This named-value must be present, but can be essentially blank. Allowed formats for the icon data include:String presenting the path to an icon file.A comma-separated string of a path to an executable file and the resource ID of an icon file in the executable file.A string file representing the resource ID of an icon found in the add-ins satellite DLL. Binary data representing the icon file.
CommandLineSafeAn optional value for informational purposes only that indicates whether the add-in was designed to be safe for command-line usage.
CommandPreloadA type DWORD bit field. Value is optional. Used to direct Visual Studio .NET to attempt a preload of an add-in for the purpose of setting up initial UI or configuration elements needed by an add-in. Possible values include:0x0 ? No preloading of add-in needed. Same impact as if value did not exist in registry.0x1 ? If a user add-in (HKEY_CURRENT_USER), this indicates need to preload add-in once to allow UI or add-in initialization. If administrative add-in (HKEY_LOCAL_MACHINE), this is the standard value used.0x2 ? Indicates a user add-in (HKEY_CURRENT_USER), that has been preloaded once.
DescriptionThe description string displayed at the bottom of the Add-Ins Manager when the add-in is selected. Value is optional. If not provided, no description will be available in the Add-Ins Manager.
FriendlyNameThe name that will appear in the Add-Ins Manager list of available add-ins. Value is optional.
LoadBehaviorA type DWORD bit field. Value is optional, but failing to provide a value will result in the name/value pair being added with a value of 0 upon first enumeration of available add-ins by Visual Studio .NET on next load. Possible values include the following:0x0 ? IDE is not loaded, and will not be loaded.0x1 ? Add-in should be loaded whenever the IDE is loaded.0x2 ? Add-in is currently loaded.0x4 ? Add-in should load when the IDE is started via command-line with a build switch being used.
SatelliteDllPathUsed to specify the path, ending in a backslash (\), for a satellite DLL to be used when localizing the FriendlyName or Description values.
SatelliteDllNameSpecifies the name of the satellite DLL found at the location specified in the SatelliteDllPath value.