8.3 Add-ins

An add-in is a COM component that implements certain interfaces. These interfaces are used to connect the IDE and the add-in. It allows the IDE to notify the add-in about user input and other potentially interesting events, and it also allows the component to communicate with the IDE's object model.

8.3.1 Design Choices

An add-in can be developed in any language that can implement a COM class. It is easy enough to write an add-in from scratch, but you don't need to do that as VS.NET has a special project template for creating an add-in. The project is found in the New Project dialog under Other Projects Extensibility Projects.

When you create a new Visual Studio .NET add-in project, you will be presented with a wizard that lets you choose how to build your add-in. It will first ask you which language you wish to useVB.NET, C#, or C++. (J# is not supported by this wizard.) Our example will use C#.

If you want to supply a custom tool window or a custom property page in the Options dialog, you are required to write an ActiveX control. Since C# and VB.NET do not support the authoring of ActiveX controls, you may want to choose C++ if you plan to use these features in your add-in. Alternatively, there is nothing stopping you writing the control in a separate component and using C# or VB.NET for the rest of the add-in.

Next you choose the IDEs in which you would like your add-in to be able to work. (You can choose VS.NET, the macro IDE, or both.) Then you will be asked to enter a name and description for your add-in. We will call our example the TaskList Data Gen Add-in.

The wizard presents many more options in the dialogs that follow, most of which are straightforward. For our example, the most important option is that we want our add-in to add an entry to the VS.NET Tools menu so that we can display a configuration user interface. (The wizard provides a checkbox to enable this.)

The Add-in Wizard creates two projects. One builds the actual add-in. The other is a Setup projectit builds a Microsoft Installer (.msi file) for the add-in. This makes it easy to distribute your add-in to other developersthe setup installer will put the component in a suitable folder and add all the necessary registry entries (more on that later).

In the main add-in project, the wizard creates a source file containing a class that implements IDTExtensibility2. IDTExtensibility2 has five methods, which are described in Table 8-2. (Older Microsoft development environments defined an interface called IDTExtensibility. This has been replaced entirely by IDTExtensibility2. Unlike certain IXxx2 interfaces in COM, implementing IDTExtensibility2 does not require you to implement IDTExtensibility as well.)

Table 8-2. IDTExtensibility2

Method

Description

OnAddInsUpdate

Called when the add-in is loaded or unloaded in the environment.

OnBeginShutdown

Called when VS.NET is being shut down.

OnConnection

Called when the add-in is loaded by VS.NET.

OnDisconnection

Called when the add-in is unloaded by VS.NET.

OnStartupComplete

Called when VS.NET has completed starting.

The OnConnection method is particularly importantit is called by VS.NET when our add-in is first loaded. Among other things, VS.NET passes in references to a couple of objects in the automation object model. In the wizard-generated implementation of this method, the first thing this code does is to store these references in a couple of fields, so that they will be available later, as Example 8-14 shows.

Example 8-14. Storing the automation objects in an add-in
public void OnConnection(object application, ext_ConnectMode connectMode,

    object addInInst, ref System.Array custom)

{

    applicationObject = (_DTE)application;

    addInInstance = (AddIn)addInInst;

   

    . . .

   

}

   

. . .

   

private _DTE applicationObject;

private AddIn addInInstance;

If you told VS.NET to add an item to the Tools menu for your add-in, the OnConnection method will check to see if this is the first time the add-in has been called since being installed. (VS.NET will pass the value ext_cm_UISetup in as the connectMode parameter the very first time the add-in is loaded.) If this is the first time, the code creates a command object and a new entry for that command on the Tools menu.

Example 8-15 shows code that adds a command and a menu item. This has been modified slightly from the code generated by the wizard. By default, the wizard names the command after the add-in project name. However, as this menu item will be providing access to a configuration dialog, we have changed the command's name to Configure.

Example 8-15. Adding an item to the Tools menu
if(connectMode =  = ext_ConnectMode.ext_cm_UISetup)

{

    object[  ] contextGUIDS = new object[  ] { };

    Commands commands = applicationObject.Commands;

    _CommandBars commandBars = applicationObject.CommandBars;

   

    try

    {

        // Add the command object. (This is a persistent

        // operation, so we only need to do this the first

        // time we run.)

   

        Command command = commands.AddNamedCommand(addInInstance,

            "Configure",

            "Configure TaskList DataSet Generator...",

            "Configures the TaskList DataSet Generator",

            true, 59,

            ref contextGUIDS,

            (int)vsCommandStatus.vsCommandStatusSupported +

              (int)vsCommandStatus.vsCommandStatusEnabled);

   

        // Add an item to the Tools menu for the new

        // command object. (This is also a persistent

        // operation.)

   

        CommandBar commandBar = (CommandBar)commandBars["Tools"];

        CommandBarControl commandBarControl =

            command.AddControl(commandBar, 1);

    }

    catch(System.Exception /*e*/)

    {

    }

}

The full name of the command will be AddInProgID.Configure, where AddInProgID is the ProgID of the AddIn class. By default, the wizard sets the ProgID to ProjectName.Connect. So if we called our add-in project TaskListAddin, the full name of the command would be:

TaskListAddin.Connect.Configure

If you want to change the prefix from ProjectName.Connect, you must change the ProgID of your add-in. This is set with the ProgID attribute on the class that the wizard generates. If you change this, you must also modify the Setup and Deployment project to matchit refers to the ProgID in its registry configuration. See Section 8.3.2 for details on how add-ins are configured in the registry. See Chapter 6 for information about how to modify Setup and Deployment projects.

The strings that follow in the AddNamedCommand parameter list determine the button/menu item text and the tooltip text, so these have also been modified to be more appropriate than the generic defaults that the wizard provides.

Add-ins that add themselves to VS.NET menus or toolbars must implement the IDTCommand interface. This defines an Exec method, which VS.NET will call when the user clicks on the relevant items. Again, if you asked the wizard to add an entry to the toolbar, it helpfully provides an implementation that does the basic command handling. All you need to do is provide the functionality. In Example 8-16, we simply display a configuration dialog. (The configuration dialog is a Windows Forms form class called TaskListDataGenConfigDialog, which will be discussed later. Its constructor, which is shown in Example 8-20, takes a reference to the DTE object, so that it can store any configuration changes.)

Example 8-16. Handling commands in an add-in
public void Exec(string commandName,

    vsCommandExecOption executeOption,

    ref object varIn, ref object varOut, ref bool handled)

{

    handled = false;

    if(executeOption = = vsCommandExecOption.vsCommandExecOptionDoDefault)

    {

        if(commandName = = "TaskListAddin.Connect.Configure")

        {

            handled = true;

   

            using (TaskListDataGenConfigDialog dlg =

                      new TaskListDataGenConfigDialog(applicationObject))

            {

                dlg.ShowDialog(  );

            }

            return;

        }

    }

}

IDTCommandTarget defines a second method, QueryStatus, which VS.NET calls to determine whether a particular command is available. This allows add-ins to gray out menu items or buttons. VS.NET will call Exec for a command only after it has checked its availability with QueryStatus. The Add-in Wizard provides an implementation of QueryStatus that looks very similar to Execit checks the command name and then sets the status. In our add-in, we never disable the command, so we can use a much simpler implementation, shown in Example 8-17. (We check the neededText parameter to see what kind of status query this isthis method also allows us to change the text dynamically. In this example we only care about making sure the command is enabled to ensure that we respond to only the appropriate kind of query.)

Example 8-17. Command status query handling
public void QueryStatus(string commandName,

    vsCommandStatusTextWanted neededText,

    ref vsCommandStatus status, ref object commandText)

{

    if(neededText = =

          vsCommandStatusTextWanted.vsCommandStatusTextWantedNone)

    {

        status = (vsCommandStatus)

            vsCommandStatus.vsCommandStatusSupported |

            vsCommandStatus.vsCommandStatusEnabled;

    }

}

We have not yet managed to implement our add-in's primary purpose: to generate a serialized DataSet containing the TaskList output. To do this, we need to port the VB.NET macro (from Example 8-12) to C#. However, since we want to be able to generate the DataSet automatically every time a build occurs, we need to do a little extra workwe cannot simply hook the ported code into the command handling that we have seen so far. Fortunately, the object model can notify us of build events through its BuildEvents object Example 8-18 shows the code that adds a suitable event handler.

Example 8-18. Handling the OnBuildDone event
public void OnConnection(object application,

    ext_ConnectMode connectMode,

    object addInInst, ref System.Array custom)

{

   

    . . . as from Example 8-14 . . .

   

   

    // Handle OnBuildDone.

    // We don't want to do this the very first time

    // VS.NET loads us--it actually calls

    // OnConnection twice, once passing in

    // ext_ConnectMode.ext_cm_UISetup, then it calls

    // OnDisconnection, and then it calls OnConnection

    // again, passing ext_ConnectMode.ext_cm_Startup.

    // We ignore the exceptional first call.

    // (The buildEventConnected flag is used to make

    // sure we don't attach two event handlers -- if the

    // user unloads and reloads the add-in using the

    // Add-in Manager, again we might see multiple

    // calls to OnConnection.)

   

    if ((connectMode != ext_ConnectMode.ext_cm_UISetup) &&

        !buildEventConnected)

    {

        applicationObject.Events.BuildEvents.OnBuildDone +=

            new _dispBuildEvents_OnBuildDoneEventHandler(

                                       BuildEvents_OnBuildDone);

        buildEventConnected = true;

    }

    

}

   

private bool buildEventConnected = false;

   

public void OnDisconnection(ext_DisconnectMode disconnectMode,

    ref System.Array custom)

{

    // Disconnect the OnBuildDone event handler.

    if (buildEventConnected)

    {

        applicationObject.Events.BuildEvents.OnBuildDone -=

            new _dispBuildEvents_OnBuildDoneEventHandler(

                                       BuildEvents_OnBuildDone);

        buildEventConnected = false;

    }

}

   

private void BuildEvents_OnBuildDone(vsBuildScope Scope,

    vsBuildAction Action)

{

    TaskListGenerator.Build(applicationObject,

                 @"c:\inetpub\wwwroot\tasklist.xml");

}

The OnConnection method is notified whenever the add-in is loaded, and in here we use the DTE object's Events property to locate the BuildEvents object. We hook up a handler for the OnBuildDone event called BuildEvents_OnBuildDone. This calls the code that generates the TaskList. (That code is just a C# version of the code shown in Example 8-12 and is not shown here.) The environment also notifies the add-in when it is about to be unloaded by calling OnDisconnection. In this function, we detach the event handler.

8.3.1.1 Configuring add-ins

Obviously, the user may not want the add-in to run every time any solution is built, so it would be prudent to add a way for the user to configure the add-in.

Add-ins have three ways of persisting configuration options. They can provide per-user settings, per-solution settings, or per-project settings. For per-user settings, an add-in can add an extra page to the Visual Studio .NET Options dialog (Tools Options). To insert pages into the Options dialog, you must add some items to VS.NET's registry settings. The relevant registry key will be:

HKCU\SOFTWARE\Microsoft\VisualStudio\7.1\AddIns\<Addin ProgID>

where <Addin ProgID> is the COM ProgID of your add-in. If you are installing your add-in for all users in the machine instead of just the installing user, you will want to use the HKLM hive, not the HKCU hive. (For VS.NET 2002, you will require 7.0 instead of 7.1.) If you add an Options key underneath this key, you can add extra pages in the Options dialog.

The Options dialog presents option pages as a hierarchythe pane on the lefthand side of the dialog presents a tree of folders and configuration pages. You can therefore add pages of your own in a hierarchical fashion. You do this by adding keys under your Options key in a hierarchy that reflects the structure you wish to see in the Options dialog. For example, if you create a Reporting key under your Options key and a Tasks DataSet key under Reporting, as illustrated in Figure 8-6, the Options dialog will show a Reporting folder containing a Tasks DataSet item, as illustrated in Figure 8-7.

Figure 8-6. Options dialog registry configuration
figs/mvs_0806.gif
Figure 8-7. A custom Options page
figs/mvs_0807.gif

You are allowed only two levels in this hierarchy. Add-ins cannot display a folder within a folder in the Options dialog.

Of course, you will need to provide a user interface to appear in the righthand side of the Options dialog box when the user clicks on your add-in's item on the left. VS.NET requires you to supply this user interface as an ActiveX control. Underneath the key for each page you must supply a text value called Control, containing either the GUID or the ProgID for the control.

Because VS.NET requires you to provide an ActiveX control, you cannot use a Windows Forms control. This means you cannot use VB.NET, C#, or J# to write a custom property page for the Options dialog.

The site that hosts your ActiveX control in the Options dialog always seems to return an ambient background color property of black. This means you should ignore the ambient background color; otherwise, your property page's background will be black. If you are using the ATL to build the ActiveX control, it automatically retrieves the ambient background property in its Create method. Example 8-19 shows a suitable replacement Create method that you can add to your control class to disable this behavior.

Example 8-19. Ignoring the ambient background color
HWND Create(HWND hWndParent, RECT& rcPos, LPARAM dwInitParam = NULL)

{

    CComCompositeControl<COptionsDialog>::Create(hWndParent, rcPos,

                                                 dwInitParam);

   

    // The base class sets m_hbrBackground to be

    // whatever the container specifies as an

    // ambient property. Unfortunately, VS.NET

    // sets this to black, so we overrule that

    // here, selecting the normal dialog background

    // color.

    if (m_hbrBackground != NULL)

    {

        DeleteObject(m_hbrBackground);

        m_hbrBackground = NULL;

    }

    m_hbrBackground = ::GetSysColorBrush(COLOR_BTNFACE);

   

    return m_hWnd;

}

In order to be loaded into the Options dialog, the ActiveX control should implement the IDTToolsOptionsPage interface as well as the standard ActiveX control interfaces. The IDTToolsOptionsPage interface allows VS.NET to integrate your properties page into the Options dialog correctly. The interface has five methods. VS.NET will call OnAfterCreated after the options page is loaded, passing a reference to the DTE object. It calls either OnOK or OnCancel to indicate when and how the Options dialog is dismissed. It calls OnHelp if the user clicks the Help button. Finally, there is the GetProperties method. This should return a Properties collectionremember that global property collections are exposed through the DTE object's Properties property. The object you return through this method will also be available through the DTE.Properties collection. You are not obliged to support thisyou may return a null referencebut you are advised to return a collection, in order that your settings may be controlled through automation.

The IDTToolsOptionsPage interface is defined in the Microsoft Development Environment type library, dte.olb. When you use the wizard to create an add-in project, this type library (or its equivalent primary interop assembly) will be referenced automatically. However, if you decided to write your add-in using C# or VB.NET and then added an extra ATL project to supply an ActiveX control for an Options page, the ATL project will not have a reference to this type library.

Fortunately, you can add a reference to this type library and also add a skeleton implementation of IDTToolsOptionsPage in one step using the Implement Interface Wizard. Open the class view, right-click on the control class and select Add Implement Interface... In the dialog that appears, choose to implement an interface from the registry. The Microsoft Development Environment type library will be one of those offered in the Available Type Libraries list. If you select this library and then choose the IDTToolsOptionsPage interface from the list, the wizard will add most of the necessary settings to your project. However, you may find it necessary to add an auto_rename flag to the generated #import directive in the stdafx.h file, as the type library defines some symbols that clash with certain common include files.

The VS.NET Options dialog is intended for setting global options, not per-solution or per-project options. (These settings cannot be stored in a solution or a project file because the Options dialog is always available, even when no solution is loaded.) The Options dialog is therefore not a good choice for configuring which solutions our add-in will work for. So instead, we will use our add-in's entry on the Tools menu, to display a dialog for configuring whether the add-in should run when the currently loaded solution is built. Also, rather than hardcoding the path of the XML file to which the DataSet will be persisted, we will also allow this to be configured in the dialog. This dialog is shown in Figure 8-8, and it stores all of its settings in the loaded solution's .sln file, allowing per-solution configuration.

Figure 8-8. Add-in configuration dialog
figs/mvs_0808.gif

The dialog is just a normal Windows Forms dialog. The two main interesting parts of the dialog's code are the initialization, where it reads settings out of the solution file, and the OK button click handler, where it writes them back in to the solution file.

Example 8-20 shows the form's constructor. It takes a reference to the DTE object as a parameter and stores it in a private field. It then uses the loaded Solution object's Globals property to see if the solution already has settings for this add-inthis is the mechanism by which VS.NET lets add-ins store configuration information in an .sln file (see Example 8-21). If settings are found, they are used to initialize the form. Otherwise, the form's fields are left in their default (blank) state.

Example 8-20. Add-in configuration dialog initialization
private _DTE dte;

public TaskListDataGenConfigDialog(_DTE dteObject)

{

     InitializeComponent(  );

    dte = dteObject;

   

    Globals g = dte.Solution.Globals;

    if (g.get_VariableExists("TaskDataSetAddinPath"))

    {

        txtOutputPath.Text = g["TaskDataSetAddinPath"].ToString(  );

        checkBoxEnable.Checked =

            bool.Parse(g["TaskDataSetAddinCmdBuild"].ToString(  ));

    }

}
Example 8-21. Saving add-in settings in a solution
private void btnOK_Click(object sender, System.EventArgs e)

{

    Globals g = dte.Solution.Globals;

   

    bool save = checkBoxEnable.Checked;

    g["TaskDataSetAddinPath"] = save ? txtOutputPath.Text : "";

    g["TaskDataSetAddinCmdBuild"] = save.ToString(  );

    g.set_VariablePersists("TaskDataSetAddinPath", true);

    g.set_VariablePersists("TaskDataSetAddinCmdBuild", true);

}

This retrieves the user's settings from the controls on the configuration dialog and writes them into the Solution object's Globals collection. Then it tells the Globals object to persist the variables we are using, ensuring that they will be saved in the ExtensibilityGlobals section of the .sln file:

GlobalSection(ExtensibilityGlobals) = postSolution

        TaskDataSetAddinCmdBuild = True

        TaskDataSetAddinPath = C:\inetpub\wwwroot\taskdata.xml

EndGlobalSection

Finally, for these settings to be of any use, we need to modify our OnBuildDone event handler from Example 8-18. This now needs to check the solution's settings to see if the TaskList DataSet generation facility is required for this particular project. A suitably modified handler is shown in Example 8-22.

Example 8-22. Checking the solution settings in OnBuildDone
private void BuildEvents_OnBuildDone(vsBuildScope Scope,

    vsBuildAction Action)

{

    seenBuildDoneEvent = true;

    Solution soln = applicationObject.Solution;

    Globals g = soln.Globals;

   

    string xmlPath = "";

    bool save = false;

    if (g.get_VariableExists("TaskDataSetAddinPath"))

    {

        xmlPath = g["TaskDataSetAddinPath"].ToString(  );

        save = bool.Parse(g["TaskDataSetAddinCmdBuild"].ToString(  ));

    }

    if (save)

    {

        TaskListGenerator.Build(applicationObject, xmlPath);

    }

}

8.3.2 Installation

For your add-in to be loaded by VS.NET, you will need to add certain entries in the registry. The relevant registry key will be:

HKCU\SOFTWARE\Microsoft\VisualStudio\7.1\AddIns\<Addin ProgID>

where <Addin ProgID> is the COM ProgID of your add-in. And, of course, your add-in also needs to be properly registered as a COM object in the normal way. Fortunately, both of these requirements will be taken care of by the setup project that is created by the Add-in Wizard.

The registry keys are specific to the version of VS.NET in which you wish to install the add-in. The key shown here is for VS.NET 2003, but for VS.NET 2002, you would need to change the 7.1 to 7.0. The simplest way of supporting both versions is to provide two installers.

In VS.NET you can select which of the currently installed add-ins is in use by selecting Tools Add-in Manager. This brings up the dialog box shown in Figure 8-9.

Figure 8-9. Add-in Manager dialog
figs/mvs_0809.gif

This dialog lets you enable or disable add-ins. It also lets you control which add-ins are loaded at startup and whether they are available when VS.NET is invoked from the command line. Note that these settings are systemwidethey do not apply just to the currently loaded solution.

8.3.3 Debugging

By default, VS.NET add-in projects are set up to launch another instance of VS.NET (devenv.exe) when you start debugging. Debugging is generally straightforward, but there is a minor complication when unhandled exceptions occur in your add-in. When this happens, VS.NET displays a dialog asking you whether you'd like to keep the add-in available. You should normally choose to keep the add-inif the add-in gets disabled, you will have to reenable it before you can test it again.