8.3 Add-insAn 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 ChoicesAn 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 use—VB.NET, C#, or C++. (J# is not supported by this wizard.) Our example will use C#.
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 project—it builds a Microsoft Installer (.msi file) for the add-in. This makes it easy to distribute your add-in to other developers—the 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.)
The OnConnection method is particularly important—it 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-inpublic 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 menuif(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 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-inpublic 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 Exec—it 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 is—this 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 handlingpublic 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 work—we 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 eventpublic 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-insObviously, 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 hierarchy—the 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 configurationFigure 8-7. A custom Options page
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.
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 colorHWND 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 collection—remember 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 this—you may return a null reference—but you are advised to return a collection, in order that your settings may be controlled through automation.
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 dialogThe 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-in—this 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 initializationprivate _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 solutionprivate 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 OnBuildDoneprivate 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 InstallationFor 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.
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 dialogThis 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 systemwide—they do not apply just to the currently loaded solution. 8.3.3 DebuggingBy 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-in—if the add-in gets disabled, you will have to reenable it before you can test it again. |