Creating Visual Studio .Net Add-Ins
Microsoft has taken excellent strides in making it more convenient for third party developers to customize and extend Visual Studio .Net. Developers and extenders can write Macros to automate repetitive tasks, create Wizards to serialize complex tasks, create Class Library Add-Ins that plug into Visual Studio .Net, or participate in the VSIP (Visual Studio Integration Program), for a fee and get access to Visual Studio for Applications and other resources. The VSIP program is to support, in part, integrators that may be implementing languages for Visual Studio .Net.
In this article I will demonstrate just one kind of customization, the Add-In. Add-Ins are class library projects that implement the Extensibility2 interface. There are about a half-dozen interface methods that you have to implement to make the class library work correctly with Visual Studio .Net. Fortunately there is a wizard that makes it easy to get started. Following that line of thinking that is where we will start. We’ll create an Add-In using the Visual Studio .Net Add-In wizard.
Using the Add-In Wizard
Add-Ins are applications that extend or customize Visual Studio .Net in some way. Add-Ins do not have to be complex to be useful; if the concept is useful to you and saves you sufficient enough effort in the future to warrant development then I encourage you to create the Add-In. (Keep in mind you can always create a macro first and convert the macro code into an Add-In later.)
We will briefly review the Add-In wizard steps because most of you are probably familiar with how to use wizards. Add-Ins are class libraries. You don’t have to use the wizard to create an Add-In. If you create a class library and implement IDTExtensibility2 and IDTCommandTarget for convenience and register the Add-In with regasm.exe then you can achieve the same result. The wizard is just easier. In fact, the preceding two sentences adequately summarize what the wizard does for us.
To create an Add-In using the wizard:
- Select File|New|Project|Other Projects|Extensibility Projects|Visual Studio.Net Add-In. Provide a name for your Add-In and click OK to start the wizard.
- The wizard has a welcome screen and six steps before it generates code. Click Next on the welcome screen.
- Select Visual Basic on the page 1 of 6 screen, and click Next.
- On page 2 of 6 use the default host applications: VSMacros IDE and Visual Studio.Net. Click Next.
- On page 3 of 6 enter a name for your add-in and a brief description. Click Next.
- Step asks among other things if you would like to create to interact with your Add-In. Check this box. This will add the IDTCommandTarget interface to your class library, allowing you to run your Add-In from VS.NETs Tools menu and directly invoke it through the Command Window. Also check that you want the Add-In to load when the hostVS.NET and VSMacrosload, and check that the Add-In should be available to all users. Click Next.
- On Page 5 of 6 fill out the Help About information if you want your information available in a help box. Click Next.
- Step 6 of 6 allows you to review selections. Click Finish when you are satisfied with your selections.
The end result will be a Class Library project with your Add-In as the assembly name and the root namespace. The class will be named Connect. The output from the wizard follows.
Listing 1: Output from the Visual Studio .Net Add-In wizard.
1: Imports Microsoft.Office.Core2: Imports EnvDTE3: imports System.Runtime.InteropServices4: 5: [ Read me for Add-in installation and setup information. ]6: 7: <GuidAttribute("921DE547-32FA-40BB-961A-EA390B7AE27D"), _8: ProgIdAttribute("MyAddin.Connect")> _9: Public Class Connect10: 11: Implements IDTExtensibility212: Implements IDTCommandTarget13: 14: Dim applicationObject As EnvDTE.DTE15: Dim addInInstance As EnvDTE.AddIn16: 17: Public Sub OnBeginShutdown(ByRef custom() As Object) _18: Implements IDTExtensibility2.OnBeginShutdown19: End Sub20: 21: Public Sub OnAddInsUpdate(ByRef custom() As Object) _22: Implements IDTExtensibility2.OnAddInsUpdate23: End Sub24: 25: Public Sub OnStartupComplete(ByRef custom() As Object) _26: Implements IDTExtensibility2.OnStartupComplete27: End Sub28: 29: Public Sub OnDisconnection( _30: ByVal RemoveMode As ext_DisconnectMode, ByRef custom() As Object) _31: Implements IDTExtensibility2.OnDisconnection32: End Sub33: 34: Public Sub OnConnection(ByVal application As Object, _35: ByVal connectMode As ext_ConnectMode, _36: ByVal addInInst As Object, _37: ByRef custom() As Object) _38: Implements IDTExtensibility2.OnConnection39: 40: applicationObject = CType(application, EnvDTE.DTE)41: addInInstance = CType(addInInst, EnvDTE.AddIn)42: If connectMode = ext_ConnectMode.ext_cm_UISetup Then43: Dim objAddIn As AddIn = CType(addInInst, AddIn)44: Dim CommandObj As Command45: 46: 'IMPORTANT!47: 'If your command no longer appears on the 48: ' appropriate command bar, you add a new or 49: ' modify an existing command, or if you would like 50: ' to re-create the command, close all 51: ' instances of Visual Studio .NET and double 52: ' click the file 'ReCreateCommands.reg' in the folder 53: ' holding the source code to your Add-in.54: 'IMPORTANT!55: Try56: CommandObj = applicationObject.Commands.AddNamedCommand( _57: objAddIn, "MyAddin", "MyAddin", _58: "Executes the command for MyAddin", _59: True, 59, Nothing, 1 + 2)60: '1+2 == vsCommandStatusSupported+vsCommandStatusEnabled61: 62: CommandObj.AddControl( _63: applicationObject.CommandBars.Item("Tools"))64: Catch e As System.Exception65: End Try66: End If67: End Sub68: 69: Public Sub Exec(ByVal cmdName As String, _70: ByVal executeOption As vsCommandExecOption, _71: ByRef varIn As Object, ByRef varOut As Object, _72: ByRef handled As Boolean) Implements IDTCommandTarget.Exec73: handled = False74: If (executeOption = _75: vsCommandExecOption.vsCommandExecOptionDoDefault) Then76: If cmdName = "MyAddin.Connect.MyAddin" Then77: handled = True78: Exit Sub79: End If80: End If81: End Sub82: 83: Public Sub QueryStatus(ByVal cmdName As String, _84: ByVal neededText As vsCommandStatusTextWanted, _85: ByRef statusOption As vsCommandStatus, _86: ByRef commandText As Object) _87: Implements IDTCommandTarget.QueryStatus88: 89: If neededText = _90: EnvDTE.vsCommandStatusTextWanted.vsCommandStatusTextWantedNone Then91: If cmdName = "MyAddin.Connect.MyAddin" Then92: statusOption = _93: CType(vsCommandStatus.vsCommandStatusEnabled + _94: vsCommandStatus.vsCommandStatusSupported, vsCommandStatus)95: Else96: statusOption = vsCommandStatus.vsCommandStatusUnsupported97: End If98: End If99: End Sub100: End Class
From the listing we can determine that the class name is Connect and that Connect implements IDTCommandTarget and IDTExtensibility2 (lines 11 and 12). IDTExtensibility requires an implementation for OnBeginShutdown, OnAddInsUpdate, OnStartupComplete, OnDisconnection, OnConnection, and IDTCommandTarget requires that we implement Exec and QueryStatus. A required implementation includes simply an empty procedure.
OnBeginShutdown is called when the IDE shutting down. OnAddInsUpdate is called when the list of Add-ins changes. OnStartupComplete occurs when the IDE has finished loading. OnDisconnection occurs when an Add-In is unloaded from the host, and OnConnection occurs when an Add-In is loaded into a host. Exec is called when a user invokes the Add-In behavior and QueryStatus is called when the user drops down the Tools menu or when Intellisense is displaying available Add-ins from the Command Window or somewhere else.
Implementing the IDTExtensibility2 Interface
The Add-In wizard provides an implementation for OnConnection automatically. OnConnection initializes the applicationObject and the AddInInstance references. These objects are used to create and insert a NamedCommand and add a menu item to the Tools menu on lines 34 to 67. The applicationObject references refers to the Development Tools Environment (DTE), which is the root object representing the host IDE. The addInInstance object is a reference to the specific instance of the Add-In, ensuring that an invocation refers to a specific object instance.
The other four IDTExtensibility2 interface methods are implemented as empty procedures. Add code to these procedures if you need additional code for initialization, startup, or de-initialization.
Implementing the IDTCommandTarget Interface
The IDTCommandTarget interfaces are implemented too. QueryStatus determines if the command is available, returning this state in the statusOption parameter, and Exec is represents the point of invocation.
Choose to implement those interface methods that you need to support your Add-In and leave the rest as empty procedures. Insert your response code between lines 76 and 77; for example, you might simply insert DoExecute method and implement your custom behavior beginning in the DoExecute method.
|Tip: Other things the Add-In wizard does for you include creating the setup project, creating strong name key file, configuring debug properties, indicating that the devenv.exe (VS.NET IDE) is a host application for your Add-In. (Remember class libraries must be executed by a host application.)|
Insert the statement MsgBox(“MyAddIn”) on line 77 and press F5 to run and test the Add-In. Pressing F5 will run a second copy of Visual Studio.Net with the Add-In available on the tools menu. Click the new Add-In menu item, and you will see the message box with the text MyAddIn displayed. (Keep in mind that the setup target created by the wizard will be compiled too, so be patient when you press F5. Building both the Add-In and setup project may take a couple of minutes.)
Debugging Add-Ins in Visual Studio.Net
Before you register your Add-In and put it into general purpose use you can debug it from VS .NET. The wizard sets debug properties indicating that VS .NET the host application. When you press F5 VS .NET will run another instance of the IDE and allow you test your Add-In. Set a break point in the source code of the Add-In in the first instance of the IDE. When you run your Add-In from the second instance it will halt when you breakpoint is hit. At that point you can debug your Add-In as you would any other application.
Add-Ins are assemblies. You can register Add-Ins as private assemblies for personal use or in the Global Assembly Cache (GAC) for shared used. Both types of registration are covered here.
Private Assembly Registration
Applications, like an Add-In DLL, have application settings that are stored in the Registry. You may have heard that .Net assemblies support xcopy deployment. This is true of .Net assemblies but not of .Net assemblies that are used by COM-based applications. Add-Ins use the system.Runtime.InteropServices which suggests that Add-Ins are used by COM-based applications, specifically the Add-In manager. For this reason you will need to register your Add-Ins. Additionally you will need to add registry settings allowing the Add-In Manager to display the Add-In in the Add-In Manager.
|Caution: Anytime you modify the registry ensure that you export and maintain a backup of your master registry file.|
There are several steps that you must perform to register your Add-In assembly. The first thing you need to do after you have tested your assembly is to run regasm.exe. The regasm.exe utility is found by default in the winntMicrosoft.NetFramework. The command to register an assembly is
regasm <path>myaddin.dll /codebase
where myaddin is the name of your Add-In, including the path information. The second step is to add information, instructing the Add-In manager how to make your Add-In accessible. You can create a registry script by copying the structure of the following listing in a text file with a .reg extension.
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINESOFTWAREMicrosoftVisualStudio7.0AddInsMyAddIn.Connect]"FriendlyName"="MyAddIn""Description"="My First AddIn""LoadBehavior"=dword:00000001"CommandLineSafe"=dword:00000001"CommandPreload"=dword:00000001
Figure 1: Registry entries describe the Add-In in the Add-in Manager.
|Note: You can construct the keys necessary from the registry while you are debugging the Add-In because the keys are added during debugging. Unfortunately those keys are removed intentionally or accidentally after debugging is complete. Store the keys in an external registry file by exporting the Add-In keys from the registry editor. (Additionally, there is a reminder that you may have to load the ReCreateCommands.reg keys into the registry if the Add-In stops showing up. Unfortunately, during early versions of beta 2 none of these remedies seem to be reliable.)|
The preceding registry script adds a key to the registry. Replace MyAddIn.Connect with the namespace and class of your Add-In. (Connect is the class name created by the Add-In wizard by default.) FriendlyName is the name of the Add-In that is displayed in the Available Add-Ins list of the Add-in Manager (see figure 1). Description indicates the text shown in the Description field, and the three remaining keys indicate the load behavior of the Add-In.
Shared Assembly Registration
Shared assemblies are stored in the Global Assembly Cache, called the GAC. The GAC can be viewed by navigating Windows Explorer to the winntassembly folder. When you navigate to this folder the GAC snap-in is automatically loaded by Windows Explorer (see figure 2).
Figure 2: The Global Assembly Name cache folder: a plug-in used by Windows Explorer automatically when you navigate to the winntassembly folder.
Global assemblies can be shared. Global assemblies are distinguished by the string name, public key rather than the file name. Hence you may have more than one file with an identical name in the GAC, but you will need to generate a strong name for shared assemblies. Strong names are generated by the Add-In wizard, or you can explicitly use the sn.exe utility to generate a string name file.
When you have run the Add-In wizard, added your custom code, and tested the Add-In then you are ready to register it. The gacutil.exe program can be used to add an assembly to the GAC. The command is gacutil /i <path>myaddin.dll where myaddin is the name of your Add-In. (By default the gacutil.exe utility is located in the “C:Program FilesMicrosoft.NETFrameworkSDKBingacutil.exe” directory.) Include the complete path information for gacutil.exe and your Add-In. If you browse to the shared assembly directory (see figure 2) then you will be able to determine that your registry has been added to the GAC. Finally you will need to add the registry entries necessary for the Add-in Manager to manage the Add-In.
In early versions of beta 2 this process seems to be a little unreliable. This is to be expected from beta software. Expect refinements in the creation and testing of Add-Ins in release versions of Visual Studio.Net. Return to this column for more information on shared assemblies and building Add-Ins as revisions to VS.NET are made available.
About the Author
Paul Kimmel is a freelance writer for Developer.com and CodeGuru.com. Look for cool Visual Basic.Net topics in his upcoming book “Sams Visual Basic .Net Unleashed”.
Paul founded Software Conceptions, Inc. in 1990. Contact Paul Kimmel at [email protected] for help building VB .NET applications or migrating VB6 applications to .NET.
# # #