A Beginner’s Guide to Making a Visual Studio Add-in
A client of mine has rather complicated requirements for releasing individual software tools. I thought it might be nice to have functionality within Visual Studio that would allow me to make sure all the release requirements are met after I compile a tool. I ended up writing a Visual Studio Add-in. In the process, I learned quite a bit, and the knowledge deserves to be gathered into one place so that others writing an Add-in for the first time might benefit.
My client requires us to develop using Visual Studio 2008 and the .NET 3.5 framework, therefore this article primarily pertains to them.
Extensibility Choices
Visual Studio has several models for extensibility.
- The most basic is the ability to launch an external tool from within Visual Studio. Developers can write their own stand-alone executable that can be included in the Visual Studio Tools menu (select External Tools… to add it). You can pass certain IDE variables to the external tool, thus allowing the tool to react to the IDE’s current state. Here is a list of the variables that can be passed to an external tool.
- Macros allow you to bundle a series of Visual Studio commands into a single command for the sake of convenience. Macros are recorded in Visual Basic, which gives you flexibility to customize how the commands work. Macros can be redistributed, though the code behind them must be included in the distribution.
- Add-ins allow you to work within the Visual Studio environment and make modifications or additions to a file, project, or solution. Developers can write their own Add-in in any language, and control what menu or toolbar item executes it. They are distributed as .dlls.
- The final option is the VSPackage, or Integration Package, which is built on the Visual Studio SDK and allows you to work within the Visual Studio environment at a lower level than Add-ins. This gives the developer the ability to do more things than an Add-in allows. VSPackages can be written in any language, though Microsoft provides templates only for C# and C++, and are distributed as .dlls, though they require a Package Load Key (PLK).
If you plan to extend Visual Studio, then the method you use will depend upon the language you plan to write in, the amount of integration with Visual Studio that you require, and how much effort you want to put into distributing your extension. In my case, the choice was fairly cut and dried. I want to write in C#, have a moderate amount of VS integration, and want to it to be simple to share my extension with other developers on my team. Add-in was the clear choice.
A note about Visual Studio 2010: Extensibility in VS2010 has been expanded somewhat to include several more models. For instance, because the text editor has been rewritten in WPF, if you wish to write an extension that works with it you will need to use the Managed Extension Framework (MEF). An Extension Manager has been added to allow the developer to manage what extensions are added and when. Several types of extension, including MEF components and VSPackages, can be packaged into VSIX files to make them compatible with the Extension Manager. Add-ins can still be written for VS2010, but they are not loaded via the Extension Manager.
Starting an Add-in
To start an Add-in, within Visual Studio select File->New Project. In the New Project wizard, select the Other Project Types->Extensibility option in the left-hand panel, then choose the Visual Studio Add-in template. Give your add-in a name and click OK.
Now the Add-in wizard comes up. Click Next to start. Choose the language you want to use for your add-in, and the Visual Studio versions you want to support. In this article I will be using C# and target VS 2008. Give your add-in a name and description, which will appear in the Visual Studio Add-in Manager.
At this point you must put some thought into how your add-in should be loaded and executed. There are three add-in options that you can specify.
1. Would you like to create command bar UI for your Add-in?
If you select this option, default code will be added to the project that will put a menu item in the Visual Studio Tool menu. You can modify this code to change the menu where your menu item appears, or add a button to a toolbar, or whatever UI you would like. The UI will be present every time you open Visual Studio after the Add-in is registered. If you do not check this item, but change your mind later, you can always add UI code to the appropriate method within the Add-in project.
Also if you select this option, a setting (called CommandPreload) will indicate that the first time that Visual Studio is started after the Add-in is registered, the Add-in will be pre-loaded so that the UI can be set up. After this, every other time that Visual Studio is launched the UI will be present.
Check this option if your Add-in will consist of a command(s) that the Visual Studio user will manually execute when needed. The command can be mapped to a toolbar button, a menu item, and/or a keyboard shortcut.
If instead your Add-in is something that will run automatically every time Visual Studio launches, or in short is not a command that the user can manually execute, then do not check this option.
2. I would like my Add-in to load when the host application starts
Selecting this option means that your Add-in’s dll will be loaded when Visual Studio starts up. This is a good option if your Add-in takes a noticeable amount of time to load, or it performs tasks that should occur when Visual Studio is starting. If you do not select this option, the add-in will only be loaded either when the user executes a command defined in the Add-in, or when the user indicates the Add-in should be loaded in the Add-in manager.
3. My Add-in will never put up modal UI, and can be used with command line builds
If you select this option, it means that your add-in can be safely called when running devenv (Visual Studio) from the command line.
Next you are asked if you would like to generate settings for the About dialog box. Selecting this option means that information about your Add-in will show up in the Help->About Microsoft Visual Studio.
Click Finish to complete the wizard.
A project is now set up and pre-populated with several files.
- <Add-in Name>.AddIn: This is an XML file with information and settings that are used to define and register the Add-in. This file will eventually end up in your <user path>/Documents/Visual Studio 2008/Addins directory.
- <Add-in Name> – For Testing.AddIn: This is an XML file with information and settings that are used to define and register the Add-in. This file is currently in your <user path>/Documents/Visual Studio 2008/Addins directory. It allows you to debug your Add-in.
- Connect.cs: This class implements the IDTExtensibility2 interface and contains two member fields and several methods that are called when certain Visual Studio events occur.
- _applicationObject: DTE2 object that has all the information available about Visual Studio, including the currently loaded solution and all of its projects, information about the Visual Studio user interface, etc.
- _addInInstance: an object that implements the AddIn interface, that contains information about the add-in instance.
- OnConnection: This is called every time the Add-in is loaded into Visual Studio (either when Visual Studio starts up or when the user selects the add-in from the Add-in Manager).
- OnDisconnection: This is called when the Add-in is unloaded from Visual Studio.
- OnAddinsUpdated: Called when the collection of Visual Studio Add-ins changes.
- OnStartupComplete: Called when Visual Studio completes its start-up process
- OnBeginShutdown: Called when Visual Studio starts to shut down.
- Exec: If you have UI associated with your add-in, this is the method that is called when the user clicks on the button associated with your add-in.
- QueryStatus: If you have UI associated with your add-in, this is the method is called when your Add-in command’s status is updated.
Add-in Structure
You can do quite a few interesting things with an Add-in, such as execute Visual Studio commands, open and close documents, modify documents, etc. What you want to do with your add-in, and whether or not it requires user interface, will dictate the load behavior for the add-in and which of the methods in Connect.cs you’ll need to modify. I will show two different scenarios here.
In the first scenario, I want to create an Add-in that runs whenever I start up and show down Visual Studio. My add-in will run automatically; I am not defining any commands in this Add-in, nor any buttons or menu items. In this case, I want it to load when Visual Studio starts up. Here is the Connect.cs file for SampleAddIn1:
1: using System;
2: using Extensibility;
3: using EnvDTE;
4: using EnvDTE80;
5: using System.Windows.Forms;
6:
7: namespace SampleAddIn1
8: {
9: /// <summary>The object for implementing an Add-in.</summary>
10: /// <seealso class='IDTExtensibility2' />
11: public class Connect : IDTExtensibility2
12: {
13: /// <summary>Implements the constructor for the Add-in object. Place your initialization code within this method.</summary>
14: public Connect()
15: {
16: }
17:
18: /// <summary>Implements the OnConnection method of the IDTExtensibility2 interface. Receives notification that the Add-in is being loaded.</summary>
19: /// <param term='application'>Root object of the host application.</param>
20: /// <param term='connectMode'>Describes how the Add-in is being loaded.</param>
21: /// <param term='addInInst'>Object representing this Add-in.</param>
22: /// <seealso class='IDTExtensibility2' />
23: public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
24: {
25: _applicationObject = (DTE2)application;
26: _addInInstance = (AddIn)addInInst;
27:
28: }
29:
30: /// <summary>Implements the OnDisconnection method of the IDTExtensibility2 interface. Receives notification that the Add-in is being unloaded.</summary>
31: /// <param term='disconnectMode'>Describes how the Add-in is being unloaded.</param>
32: /// <param term='custom'>Array of parameters that are host application specific.</param>
33: /// <seealso class='IDTExtensibility2' />
34: public void OnDisconnection(ext_DisconnectMode disconnectMode, ref Array custom)
35: {
36: }
37:
38: /// <summary>Implements the OnAddInsUpdate method of the IDTExtensibility2 interface. Receives notification when the collection of Add-ins has changed.</summary>
39: /// <param term='custom'>Array of parameters that are host application specific.</param>
40: /// <seealso class='IDTExtensibility2' />
41: public void OnAddInsUpdate(ref Array custom)
42: {
43: }
44:
45: /// <summary>Implements the OnStartupComplete method of the IDTExtensibility2 interface. Receives notification that the host application has completed loading.</summary>
46: /// <param term='custom'>Array of parameters that are host application specific.</param>
47: /// <seealso class='IDTExtensibility2' />
48: public void OnStartupComplete(ref Array custom)
49: {
50: MessageBox.Show("Welcome! Main Window: " + _applicationObject.MainWindow.Caption, _addInInstance.Name);
51: }
52:
53: /// <summary>Implements the OnBeginShutdown method of the IDTExtensibility2 interface. Receives notification that the host application is being unloaded.</summary>
54: /// <param term='custom'>Array of parameters that are host application specific.</param>
55: /// <seealso class='IDTExtensibility2' />
56: public void OnBeginShutdown(ref Array custom)
57: {
58: MessageBox.Show("Oh, Anne, so sad to see you go...", _addInInstance.Name);
59: }
60:
61: private DTE2 _applicationObject;
62: private AddIn _addInInstance;
63: }
64: }
To generate this example, I worked through the New Add-in wizard, opting to have no UI created, and to have the Add-in loaded at start-up. I then added the MessageBox code to the OnStartupComplete and OnBeginShutdown methods. The .AddIn file for this Add-in looks like this:
1: <?xml version="1.0" encoding="UTF-16" standalone="no"?>
2: <Extensibility xmlns="http://schemas.microsoft.com/AutomationExtensibility">
3: <HostApplication>
4: <Name>Microsoft Visual Studio</Name>
5: <Version>10.0</Version>
6: </HostApplication>
7: <Addin>
8: <FriendlyName>SampleAddIn1</FriendlyName>
9: <Description>SampleAddIn1 executes at VS start and shutdown</Description>
10: <AboutBoxDetails>Add-in written by Anne at SRT Solutions</AboutBoxDetails>
12: <Assembly>c:\users\amarsan\documents\visual studio 2010\Projects\SampleAddIn1\SampleAddIn1\bin\SampleAddIn1.dll</Assembly>
13: <FullClassName>SampleAddIn1.Connect</FullClassName>
14: <LoadBehavior>1</LoadBehavior>
15: <CommandPreload>0</CommandPreload>
16: <CommandLineSafe>0</CommandLineSafe>
17: </Addin>
18: </Extensibility>
In the .AddIn file, the <LoadBehavior> tag indicates when the Add-in should be loaded. A value of 1 indicates that it should always be loaded when Visual Studio starts-up. This ensures that my Add-in’s OnStartupComplete method will get called.
The <CommandPreload> tag indicates whether or not the Add-in should be pre-loaded the first time Visual Studio runs after the Add-in is registered (by putting the Add-in file in the AddIns directory) so that commands and UI can be set up. In our case, this Add-in does not have any commands or UI associated with it, so CommandPreload is set to 0.
In the second scenario, I want to set up an Add-in that creates a command and a menu item in the Visual Studio Tools menu that when clicked executes the command. The Add-in does not need to be loaded unless the user actually uses the command, so I will opt to defer Add-in loading. The Connect.cs file looks like this:
1: using System;
2: using Extensibility;
3: using EnvDTE;
4: using EnvDTE80;
5: using Microsoft.VisualStudio.CommandBars;
6: using System.Resources;
7: using System.Reflection;
8: using System.Globalization;
9: using System.Windows.Forms;
10:
11: namespace SampleAddin2
12: {
13: /// <summary>The object for implementing an Add-in.</summary>
14: /// <seealso class='IDTExtensibility2' />
15: public class Connect : IDTExtensibility2, IDTCommandTarget
16: {
17: /// <summary>Implements the constructor for the Add-in object. Place your initialization code within this method.</summary>
18: public Connect()
19: {
20: }
21:
22: /// <summary>Implements the OnConnection method of the IDTExtensibility2 interface. Receives notification that the Add-in is being loaded.</summary>
23: /// <param term='application'>Root object of the host application.</param>
24: /// <param term='connectMode'>Describes how the Add-in is being loaded.</param>
25: /// <param term='addInInst'>Object representing this Add-in.</param>
26: /// <seealso class='IDTExtensibility2' />
27: public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
28: {
29: _applicationObject = (DTE2)application;
30: _addInInstance = (AddIn)addInInst;
31: if(connectMode == ext_ConnectMode.ext_cm_UISetup)
32: {
33: object []contextGUIDS = new object[] { };
34: Commands2 commands = (Commands2)_applicationObject.Commands;
35: string toolsMenuName = "Tools";
36:
37: //Place the command on the tools menu.
38: //Find the MenuBar command bar, which is the top-level command bar holding all the main menu items:
39: Microsoft.VisualStudio.CommandBars.CommandBar menuBarCommandBar = ((Microsoft.VisualStudio.CommandBars.CommandBars)_applicationObject.CommandBars)["MenuBar"];
40:
41: //Find the Tools command bar on the MenuBar command bar:
42: CommandBarControl toolsControl = menuBarCommandBar.Controls[toolsMenuName];
43: CommandBarPopup toolsPopup = (CommandBarPopup)toolsControl;
44:
45: //This try/catch block can be duplicated if you wish to add multiple commands to be handled by your Add-in,
46: // just make sure you also update the QueryStatus/Exec method to include the new command names.
47: try
48: {
49: //Add a command to the Commands collection:
50: Command command = commands.AddNamedCommand2(_addInInstance, "SampleAddin2", "SampleAddin2", "Executes the command for SampleAddin2", true, 59, ref contextGUIDS, (int)vsCommandStatus.vsCommandStatusSupported+(int)vsCommandStatus.vsCommandStatusEnabled, (int)vsCommandStyle.vsCommandStylePictAndText, vsCommandControlType.vsCommandControlTypeButton);
51:
52: //Add a control for the command to the tools menu:
53: if((command != null) && (toolsPopup != null))
54: {
55: command.AddControl(toolsPopup.CommandBar, 1);
56: }
57: }
58: catch(System.ArgumentException)
59: {
60: //If we are here, then the exception is probably because a command with that name
61: // already exists. If so there is no need to recreate the command and we can
62: // safely ignore the exception.
63: }
64: }
65: }
66:
67: /// <summary>Implements the OnDisconnection method of the IDTExtensibility2 interface. Receives notification that the Add-in is being unloaded.</summary>
68: /// <param term='disconnectMode'>Describes how the Add-in is being unloaded.</param>
69: /// <param term='custom'>Array of parameters that are host application specific.</param>
70: /// <seealso class='IDTExtensibility2' />
71: public void OnDisconnection(ext_DisconnectMode disconnectMode, ref Array custom)
72: {
73: }
74:
75: /// <summary>Implements the OnAddInsUpdate method of the IDTExtensibility2 interface. Receives notification when the collection of Add-ins has changed.</summary>
76: /// <param term='custom'>Array of parameters that are host application specific.</param>
77: /// <seealso class='IDTExtensibility2' />
78: public void OnAddInsUpdate(ref Array custom)
79: {
80: }
81:
82: /// <summary>Implements the OnStartupComplete method of the IDTExtensibility2 interface. Receives notification that the host application has completed loading.</summary>
83: /// <param term='custom'>Array of parameters that are host application specific.</param>
84: /// <seealso class='IDTExtensibility2' />
85: public void OnStartupComplete(ref Array custom)
86: {
87: }
88:
89: /// <summary>Implements the OnBeginShutdown method of the IDTExtensibility2 interface. Receives notification that the host application is being unloaded.</summary>
90: /// <param term='custom'>Array of parameters that are host application specific.</param>
91: /// <seealso class='IDTExtensibility2' />
92: public void OnBeginShutdown(ref Array custom)
93: {
94: }
95:
96: /// <summary>Implements the QueryStatus method of the IDTCommandTarget interface. This is called when the command's availability is updated</summary>
97: /// <param term='commandName'>The name of the command to determine state for.</param>
98: /// <param term='neededText'>Text that is needed for the command.</param>
99: /// <param term='status'>The state of the command in the user interface.</param>
100: /// <param term='commandText'>Text requested by the neededText parameter.</param>
101: /// <seealso class='Exec' />
102: public void QueryStatus(string commandName, vsCommandStatusTextWanted neededText, ref vsCommandStatus status, ref object commandText)
103: {
104: if(neededText == vsCommandStatusTextWanted.vsCommandStatusTextWantedNone)
105: {
106: if(commandName == "SampleAddin2.Connect.SampleAddin2")
107: {
108: status = (vsCommandStatus)vsCommandStatus.vsCommandStatusSupported|vsCommandStatus.vsCommandStatusEnabled;
109: return;
110: }
111: }
112: }
113:
114: /// <summary>Implements the Exec method of the IDTCommandTarget interface. This is called when the command is invoked.</summary>
115: /// <param term='commandName'>The name of the command to execute.</param>
116: /// <param term='executeOption'>Describes how the command should be run.</param>
117: /// <param term='varIn'>Parameters passed from the caller to the command handler.</param>
118: /// <param term='varOut'>Parameters passed from the command handler to the caller.</param>
119: /// <param term='handled'>Informs the caller if the command was handled or not.</param>
120: /// <seealso class='Exec' />
121: public void Exec(string commandName, vsCommandExecOption executeOption, ref object varIn, ref object varOut, ref bool handled)
122: {
123: handled = false;
124: if(executeOption == vsCommandExecOption.vsCommandExecOptionDoDefault)
125: {
126: if(commandName == "SampleAddin2.Connect.SampleAddin2")
127: {
128: ExecuteMyCommand(_applicationObject, _addInInstance);
129: handled = true;
130: return;
131: }
132: }
133: }
134:
135: private void ExecuteMyCommand(DTE2 _applicationObject, AddIn _addInInstance)
136: {
137: MessageBox.Show("Active Window: " + _applicationObject.ActiveWindow.Caption , _addInInstance.Name);
138: }
139:
140: private DTE2 _applicationObject;
141: private AddIn _addInInstance;
142: }
143: }
I generated this file by working through the New Add-in wizard, only selecting the option to create command bar UI. Most of the code was automatically generated for me by the template. I added the ExecuteMyCommand method and the call to it from within the Exec method. The .AddIn file looks like this:
1: <?xml version="1.0" encoding="UTF-16" standalone="no"?>
2: <Extensibility xmlns="http://schemas.microsoft.com/AutomationExtensibility">
3: <HostApplication>
4: <Name>Microsoft Visual Studio</Name>
5: <Version>10.0</Version>
6: </HostApplication>
7: <Addin>
8: <FriendlyName>SampleAddin2</FriendlyName>
9: <Description>SampleAddin2 is a command that can be executed manually</Description>
10: <AboutBoxDetails>Developed by Anne at SRT Solutions.</AboutBoxDetails>
11: <Assembly>SampleAddin2.dll</Assembly>
12: <FullClassName>SampleAddin2.Connect</FullClassName>
13: <LoadBehavior>0</LoadBehavior>
14: <CommandPreload>1</CommandPreload>
15: <CommandLineSafe>0</CommandLineSafe>
16: </Addin>
17: </Extensibility>
The <LoadBehavior> value is 0 in this example because the Add-in will not be loaded at start-up.
The <CommandPreload> value is 1, indicating that Add-in will be pre-loaded the first time Visual Studio runs after it is registered so that the command and the Tool menu item can be set up. The code that generates the command and Tool menu item are located in the OnConnection method. The code is wrapped in an if statement that checks if the connect mode is ext_cm_UISetup. The OnConnection method is called every time that the Add-in is loaded. The first time that Visual Studio runs after this Add-in is registered, even though I have indicated that the Add-in should not be loaded at start-up, it will in fact be loaded with the connect mode set to ext_cm_UISetup. The code to create the command and Tool menu item will execute, and then the Add-in will be unloaded. It won’t be loaded again until the user executes the SampleAddin2 command (by selecting the menu item in the Tool menu). This time when the Add-in is loaded, it’s connect mode will be ext_cm_AfterStartup. If I had, in fact, set LoadBehavior to 1, the Add-in would still be preloaded and then unloaded, but would then immediately be reloaded, this time with the connect mode set to ext_cm_Startup.
Registering an Add-in
I’ve mentioned the notion of registering an Add-in several times in this article. While registration in older versions of Visual Studio involved setting registry keys, with versions 2005 and later it is actually quite straightforward. The .AddIn file that the Visual Studio template creates when starting a new Add-in project needs to be placed in the <user directory or all user directory>/Documents/Visual Studio 2008/AddIns directory. The .AddIn file <Assembly> tag specifies the path where the .dll for the Add-in is located. You can create an installer to deploy the Add-in on other developers’ machines that puts the .AddIn file in the correct directory and the Add-in dlls in, for instance, the Program Files directory. A description of the .AddIn file tags can be found here.
Debugging an Add-in
As straightforward as it is to get an Add-in up and running, there is one common snag you may run into. Initially you may get your Add-in working well and then use it every time you open up Visual Studio. After awhile, you decide you need to make some changes, so you open up an instance of Visual Studio and open up your add-in solution, make some code changes, then go to compile. You get an error like this:
Error 1 Unable to delete file “.\bin\SampleAddin2.dll”. Access to the path ‘C:\Users\amarsan\documents\visual studio 2008\Projects\SampleAddin2\SampleAddin2\bin\SampleAddin2.dll’ is denied.
The problem is that your add-in has been loaded into the instance of Visual Studio that you are currently running (or you have another VS instance open in which the add-in has been loaded), so when you try to recompile the add-in’s dll, you get this error. The trick is to edit your add-in’s .AddIn file, setting <LoadBehavior> to 0. Now close all instances of Visual Studio, and then reopen with your add-in solution. Recompile your add-in. This time you shouldn’t get the error.
No comments
Category:
