Creating Visual Studio .NET Add-Ins
As you might have gathered by now, a lot of information is available in the general and project-neutral extensibility models. What are all of these automation objects for? They are there to help language extenderspeople like youcreate Add-Ins among other things.
Because the object model is so vast, you will have to rely heavily on the help files to begin scratching the surface of extensibility; more than likely, several tool vendors will develop in-house expertise, and a developer from one of these shops will write an entire book on this subject.
In this section we will look at the necessary, basic considerations for creating VS .NET add-ins and will create some simple add-ins. Of course any add-in could potentially be a complex application by itselfconsider Visual SourceSafe.
Creating an Add-In Project
Macros extend the IDE by providing automated solutions that can be run in the IDE. The biggest difference between an add-in and a macro is that add-ins are compiled into a separate .DLL and macros are not. The tasks you automate with macros or add-ins can be identical. The reason you would choose an add-in over a macro is one of distribution. If you write an extension that you want to sell, and you want to prevent customers from pillaging your source code, you will create an add-in project; otherwise a macro project will suffice.
The essential ingredients necessary to create add-ins are a DLL or class library, a project, and a class that implements the IDTExtensibility2 interface. There are other interfaces you can implement that will make your add-in more useful, but IDTExtensibility2 is the primary interface that an add-in must implement.
Fortunately, there is an add-in wizard that automates the basic steps necessary to create an add-in. Granted, the add-in created by the wizard is a do-nothing add-in, but it will get you jump started. Follow the numbered steps listed following to create an add-in project that will compile and run.
Start a new project by choosing File, New Project, Other Projects Extensibility Projects, Visual Studio.Net Add-In. Click OK. Click Next at the Welcome screen.
On Page 1 of 6, select Create an Add-In Using Visual Basic. Click Next.
Page 2 of 6 uses the defaults, which define the Macros IDE and Visual Studio .NET as hosts for the add-in. Click Next.
On Page 3 of 6 name the add-in MyAddin and enter a description; for example, My First Add-In. Click Next.
On Page 4 of 6 we want to indicate that the add-in can be invoked from the Tools menu, the add-in should load when the host loads, and the add-in should be available to all users. (Check all check boxes except My Add-In Will Never Put Up a Modal UI.) Click Next.
On Page 5 of 6, check the check box if you want to add About box information. (If you do, modify the About box information accordingly.) Click Next.
On Page 6 of 6, review the Summary information, and if it is correct, click Finish.
The wizard will create an add-in projectin our example named MyAddInand a Windows Installer project named MyAddInSetup. (Using the AddInNameSetup naming convention.) The add-in project will have references, an assemblyinfo.vb module, and a module named connect.vb that contains the add-in class, Connect. Connect implements the IDTExtensibility2 interface and, because we indicated that we wanted integration with the IDE's Tools menu, the IDTCommandTarget interface.
The add-in will compile and run as is, but it performs no real task at the present time. If you run the add-in from the IDE, it will compile and package the setup file MyAddInSetup.msi. (Creating the setup file takes a while, so be patient.) When the add-in project runs, it starts a new instance of the IDE within our casethe add-in added as a menu operation in the Tools menu. Go ahead and try it.
CAUTION
The add-in menu item may stop appearing in the Tools menu after you test the add-in to debug it. If this happens, close all instances of the IDE and double-click ReCreateCommands.reg in your Add-In directory. This step reloads the registry settings and the menu should reappear when you restart VS .NET.
Our vanilla add-in implements the IDTExtensibility2 and IDTCommandTarget interfaces resulting in the code in Listing 4.9.
Listing 4.9 The add-in module created by the Visual Studio .NET Add-In Wizard (repaginated to fit this page).
1: Imports Microsoft.Office.Core 2: Imports EnvDTE 3: Imports System.Runtime.InteropServices 4: 5: #Region " Read me for Add-in installation and setup information. " 6: ' When run, the Add-in wizard prepared the registry for the Add-in. 7: ' At a later time, if the Add-in becomes unavailable for reasons such as: 8: ' 1) You moved this project to a computer other than which it was originally created on. 9: ' 2) You chose 'Yes' when presented with a message asking if you wish to remove the Add-in. 10: ' 3) Registry corruption. 11: ' you will need to re-register the Add-in by building the MyAddinSetup project 12: ' by right clicking the project in the Solution Explorer, then [ic:ccc]choosing install. 13: #End Region 14: 15: <GuidAttribute("F61C62A7-A0F2-4660-87C7-67BCE5C0BF96"), _ 16: ProgIdAttribute("MyAddin.Connect")> _ 17: Public Class Connect 18: 19: Implements IDTExtensibility2 20: Implements IDTCommandTarget 21: 22: Dim applicationObject As EnvDTE.DTE 23: Dim addInInstance As EnvDTE.AddIn 24: 25: Public Sub OnBeginShutdown(ByRef custom() As Object) _ 26: Implements IDTExtensibility2.OnBeginShutdown 27: 28: End Sub 29: 30: Public Sub OnAddInsUpdate(ByRef custom() As Object) _ 31: Implements IDTExtensibility2.OnAddInsUpdate 32: 33: End Sub 34: 35: Public Sub OnStartupComplete(ByRef custom() As Object) _ 36: Implements IDTExtensibility2.OnStartupComplete 37: 38: End Sub 39: 40: Public Sub OnDisconnection( _ 41: ByVal RemoveMode As ext_DisconnectMode, _ 42: ByRef custom() As Object) _ 43: Implements IDTExtensibility2.OnDisconnection 44: 45: End Sub 46: 47: Public Sub OnConnection(ByVal application As Object, _ 48: ByVal connectMode As ext_ConnectMode, _ 49: ByVal addInInst As Object, ByRef custom() As Object) _ 50: Implements IDTExtensibility2.OnConnection 51: 52: applicationObject = CType(application, EnvDTE.DTE) 53: addInInstance = CType(addInInst, EnvDTE.AddIn) 54: If connectMode = ext_ConnectMode.ext_cm_UISetup Then 55: Dim objAddIn As AddIn = CType(addInInst, AddIn) 56: Dim CommandObj As Command 57: 58: 'IMPORTANT! 59: 'If your command no longer appears on the appropriate 60: ' command bar, you add a new or modify an existing command, 61: ' or if you would like to re-create the command, close 62: ' all instances of Visual Studio .NET and double click 63: ' the(file) 'ReCreateCommands.reg' in the folder 64: ' holding the source code to your Add-in. 65: 'IMPORTANT! 66: Try 67: CommandObj = _ 68: applicationObject.Commands.AddNamedCommand(objAddIn, _ 69: "MyAddin", "MyAddin", "Executes the command for MyAddin", True, 59, Nothing, _ 70: 1 + 2) 71: '1+2 == vsCommandStatusSupported+vsCommandStatusEnabled 72: 73: CommandObj.AddControl( _ 74: applicationObject.CommandBars.Item("Tools")) 75: Catch e As System.Exception 76: End Try 77: End If 78: End Sub 79: 80: Public Sub Exec(ByVal cmdName As String, _ 81: ByVal executeOption As vsCommandExecOption, _ 82: ByRef varIn As Object, ByRef varOut As Object, _ 83: ByRef handled As Boolean) Implements IDTCommandTarget.Exec 84: 85: handled = False 86: If (executeOption = _ 87: vsCommandExecOption.vsCommandExecOptionDoDefault) Then 88: 89: If cmdName = "MyAddin.Connect.MyAddin" Then 90: handled = True 91: Exit Sub 92: End If 93: End If 94: End Sub 95: 96: Public Sub QueryStatus(ByVal cmdName As String, _ 97: ByVal neededText As vsCommandStatusTextWanted, _ 98: ByRef statusOption As vsCommandStatus, _ 99: ByRef commandText As Object) _ 100: Implements IDTCommandTarget.QueryStatus 101: 102: If neededText = _ 103: EnvDTE.vsCommandStatusTextWanted.vsCommandStatusTextWantedNone _ 104: Then 105: 106: If cmdName = "MyAddin.Connect.MyAddin" Then 107: statusOption = _ 108: CType(vsCommandStatus.vsCommandStatusEnabled + _ 109: vsCommandStatus.vsCommandStatusSupported, _ 110: vsCommandStatus) 111: Else 112: statusOption = vsCommandStatus.vsCommandStatusUnsupported 113: End If 114: End If 115: End Sub 116: End Class
NOTE
Keep in mind that GUIDs will always be unique. Thus, the value of GuidAttribute on line 15 on your computer will vary.
As is immediately apparent from the listing, a few of these procedures are a little murky and all of the statements are long, but when you evaluate each one they are pretty straightforward. All of the code implements the two interfaces previously mentioned. The IDTExtensibility2 methods are implemented as empty methods, and the IDTCommandTarget methods have the necessary code needed to place a menu item in the Tools menu and respond when it is clicked. The next two subsections discuss the IDTCommandTarget and IDTExtensibility2 interfaces.
Implementing the IDTExtensibility2 Interface
IDTExtensibility2 is the basic add-in interface. There are five methods that you must implementalthough they can be empty methodsto define an add-in. These are described in Table 4.1.
Table 4.1 IDTExtensibility2 Interface Methods That Have To Be Implemented To Create an Add-In
Interface Method |
Description |
OnAddInsUpdate |
Called when the Add-In Manager list changes |
OnBeginShutDown |
Called when the IDE is shut down before OnDisconnection |
OnConnection |
Called when an add-in is loaded |
OnDisconnection |
Called when an add-in is unloaded |
OnStartupComplete |
Called when the IDE has completed startup |
From the descriptions, it is apparent that these methods are provided to allow you an opportunity to initialize and release resources at opportunistic points during your add-in's lifecycle. If you need some processing to occur before your add-in is run, implement OnConnection or OnStartupComplete. If you need processing to occur before your add-in or the IDE is shut down, implement OnDisconnection or OnBeginShutDown. Otherwise, leave the implementation of these interface methods blank, as demonstrated in Listing 4.9.
OnConnection is implemented to create (named) Command object on lines 67 through 70, allowing the command to be invoked from the Command window, and adds the command to the Tools menuin the example, on lines 73 and 74.
Implementing the IDTCommandTarget Interface
The IDTCommandTarget interface implements the menu item for the add-in. The Exec method is called when the menu item is clicked and QueryStatus returns the status of the command specified in the arguments, indicating whether the command is available.
The code implementing the IDTCommandTarget is very specific, suggesting that it is easiest if you let the wizard generate the add-in initially. You could, however, use the wizard-generated code as a template for creating add-ins manually.
Registering Add-Ins
To register your VB .NET add-in, open a command prompt window and run the Regasm.exe application with the /codebase switch, passing it the name of your add-in DLL.
Regasm.exe is installed in the \Winnt\Microsoft.Net\Framework\ directory by default. Because we indicated that the add-in should be available to all users in step 5 in the section "Creating an Add-In Project," the add-in keys are added to the registry in HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\7.0\AddIns\MyAddIn.Connect.
The help topic ms-help://MS.VSCC/MS.MSDNVS/vsintro7/html/vxconAdd-InRegistration.htm titled "Add-In Registration" provides additional details on the keys that are added to the registry and appropriate values for those keys.
Here is an example demonstrating the proper registration of an add-in named MyAddIn.dll. (The example assumes that the current directory contains the add-in .DLL.)
C:\WINNT\Microsoft.NET\Framework\v1.0.3215\Regasm.exe myaddin.dll /codebase
(The specific location of Regasm.exe may vary by computer and version of the .NET Framework.)
TIP
The best way to ensure that add-ins are properly registered and installed is to run the Add-In setup project when you are finished testing and debugging your add-in.
Regasm.exe assigns a .NET assembly a ClassID and adds a registry entry. The reason for all of this has to do with the Add-Ins Manager. The Add-Ins Manager is an Automation server. Yes, that's rightold COM technology still lingers in a few places.
The /codebase switch puts the full path to the Add-In into the registry.
Implementing the IDTToolsOptionsPage Interface
Implement the IDTToolsOptionsPage interface when you want to define a custom Tools Options Page for your add-in. You will have to implement the following interface methods to create the Options page: GetProperties, OnAfterCreated, OnCancel, OnHelp, and OnOK.
GetProperties needs to return a DTE.Properties collection containing all of the properties on the custom page. OnAfterCreated is called after the page is created. OnCancel, OnHelp, and OnOK are called in response to the user clicking those buttons, respectively.
Adding Behaviors to the Add-In
Now that we have created an empty add-in, we need to add some code to define a behavior. Our add-in behavior is invoked when the user clicks the Tools, MyAddIn menu item or when the user types MyAddin.Connect.MyAddIn in the Command window. Either of these methods yields control to the Exec method we implemented, hence our new behavior needs to be initiated in the Exec method.
Building on the behavior we contrived in an earlier section, we can insert code to run the copyright-insertion behavior from the Add-In menu. That is, our add-in will insert a copyright tag.
To modify the MyAddIn add-in to use the InsertCopyrightTag macro, we will need to ensure that the macro project containing that macro is loaded. Alternatively, we can copy the macro code into the MyAddIn project and avoid a dependency on the macro. Listing 4.10 demonstrates the former approach; it is assumed that the macro project is loaded and we will execute the macro to actually perform the add-in behavior for us.
Listing 4.10 Excerpt from Listing 4.9 showing the modifications to the add-in created by the wizard.
80: Public Sub Exec(ByVal cmdName As String, _ 81: ByVal executeOption As vsCommandExecOption, _ 82: ByRef varIn As Object, ByRef varOut As Object, _ 83: ByRef handled As Boolean) Implements IDTCommandTarget.Exec 84: 85: handled = False 86: If (executeOption = _ 87: vsCommandExecOption.vsCommandExecOptionDoDefault) Then 88: 89: If cmdName = "MyAddIn.Connect.MyAddIn" Then 90: 91: DoExecute() 92: 93: handled = True 94: Exit Sub 95: End If 96: End If 97: End Sub ... 120: Private Sub DoExecute() 121: ' Need to ensure the Copyright macro project is defined. 122: applicationObject.ExecuteCommand( _ 123: "Macros.MyMacros.Copyright.InsertCopyrightTag") 124: End Sub
Listing 4.10 is excerpted from Listing 4.9. Replace the Exec implementation and add the DoExecute method to complete the add-in MyAddIn. Line 91 invokes the DoExecute behavior when the Exec method is called. DoExecute is implemented in terms of the InsertCopyrightTag defined earlier in this chapter. Line 122 of DoExecute uses the EnvDTE.DTE.ExecuteCommand method to run the macro. ExecuteCommand works as if you had typed the statement in the Command window.
Because macros are implemented with VB .NET, you could literally cut and paste the macro code into the add-in as an alternative to invoking the macro.