devxlogo

Deconstructing Add-In Architecture in Visual Studio .NET

Deconstructing Add-In Architecture in Visual Studio .NET

isual 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.

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

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

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.exeprocess, 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.

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

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.

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 IDTExtensibility2interface. In order to write a class to function as an add-in, you should first start by implementing IDTExtensibility2and 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.

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_USERSOFTWAREMicrosoftVisualStudio7.1AddIns. 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.

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 IDTExtensibility2interface 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 IDTCommandTargetinterface 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 AddControlmethod 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 IDTCommandTargetinterface 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.”

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist