Visual Studio Add-in Library
This library allows you to quickly and simply create add-ins for VS2008 and VS2010 (VS2005?).
Introduction
It has been a long time I wanted to natively integrate all our tools into Visual Studio, but due to lack of time and desire, I always thought: "OK, I'll see this later", and nothing was done... So this summer, I started to dig the Visual Studio plug-in APIs (EnvDTE), and while browsing CodeProject and more generally the Web, I discovered that in fact no one has a clear and simple solution to create add-ins for Visual Studio. For older versions, people where talking about packages and pure COM calls; for new ones (VS2005+), the Add-in Project template was used, and each article I read had its own implementation and limitation. Moreover, even the MS documentation wasn't clear, giving sometimes samples with different implementations for the same requirement. So, this library is a compilation of practices (that work!) that I picked around and reorganized to simplify the creation of add-ins and the associated menu items/commands.
Using the code
First of all .. create a new plug-in project like in figure 1 (click Next until the end)!
Then you finish with a structure like figure 2, with the file "Connect.cs" opened, if you choose C# for language.
|
Note:
|
If you look at the source code of "connect.cs", you'll see that it implements only one interface, "IDTExtensibility2
".
public class Connect : IDTExtensibility2
It seems enough to create a plug-in? Yes? No, it doesn't; if you search on the web, you'll see that some samples implement "IDTCommandTarget
" to gain more control on commands. Have a look at this article: How to: Expose an Add-In on the Tools Menu (Visual C#) ... This will show you all the code you need to just add one command to the "Tools" menu.
So the main idea is how to simplify all of this.
Here are the steps I followed:
- Create a class "CommandManager.cs" that will be the base class for the "Connect.cs" generated class.
- Move methods and Interfaces from "Connect.cs" to "CommandManager.cs".
- The class is not COM visible, so modify the class declaration ...
- Create a simple architecture for the library users.
...
public class CommandManager : IDTCommandTarget, IDTExtensibility2
{
...
}
...
Result: Compilation OK / Execution failed.
...
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public class CommandManager : IDispatch, IDTCommandTarget, IDTExtensibility2
{
...
}
...
Result: Compilation OK / Execution OK!
Now add the reference to the library (Figure 3).
|
Note:
|
Prepare the resource files for localization.
- This will allow your plug-in to select the correct translations based upon the VS language. From the Plugin Add New Item ...
- Add these strings to the resources files (they will be used later):
- Add a bitmap (16x16/32 bits/BMP format) to the resource files and rename it "2".
Warning: the resource file must follow this filename convention: Resources.[language].resx.
Add at least two resource language files (e.g.: en and fr).
...
<data name="MY_DESCRIPTION" xml:space="preserve">
<value>Desc EN</value>
</data>
<data name="MY_INFOS" xml:space="preserve">
<value>no infos EN</value>
</data>
<data name="MY_NAME" xml:space="preserve">
<value>Plugin EN</value>
</data>....
...
<data name="2" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Resources\2.bmp;System.Drawing.Bitmap, System.Drawing,
Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
Now, we'll use the library.
- Remove the body of the
Connect
class, derive it from "VisualStudio.AddInLibrary.CommandManager
", and remove the unnecessary "using
" clauses: - You must override the base "
CreateCommands
" method and implement the parameters initialization before creating your commands (here, "ToolsMenu
"):
using System;
using VisualStudio.AddInLibrary;
namespace TestAddin
{
public class Connect : CommandManager
{
/// <summary>Implements the constructor for the Add-in object.
/// Place your initialization code within this method.</summary>
public Connect()
{
}
}
}
// <summary>
/// creates plugin commands
/// </summary>
protected override void CreateCommands()
{
// we store a reference to the plugin assembly for use in the library
PluginAssembly = this.GetType().Assembly;
// parameters used to for each commands
BaseMenuParams _params = new BaseMenuParams();
_params.AddInInstance = _addInInstance;
_params.ResourceAssembly = PluginAssembly;
_params.VisualStudioInstance = _applicationObject;
// !!!!!!!!!!!!!!!!!! create your menus HERE !!!!!!!!!!!!!!!!!!!!!!
AddCommand(new ToolsMenu(_params));
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
}
Your Connect
class is now ready!
How to add commands to the "Tools" menu: ToolsMenu.cs
Add a new class named "ToolsMenu
" to the plug-in and derive it from BaseMenu
and implement the "IMenuImplementation
" interface:
using System;
using VisualStudio.AddInLibrary.Menus;
namespace TestAddin
{
class ToolsMenu : BaseMenu, IMenuImplementation
{
#region IMenuImplementation Members
public void CreateCommands()
{
throw new NotImplementedException();
}
public void Install()
{
throw new NotImplementedException();
}
public void UnInstall()
{
throw new NotImplementedException();
}
public MenuState GetState(string command,
EnvDTE.vsCommandStatusTextWanted NeededText,
ref object CommandText)
{
throw new NotImplementedException();
}
public bool IsItMycommand(string command)
{
throw new NotImplementedException();
}
#endregion
}
}
Add the constructor and call the base constructor.
public ToolsMenu(BaseMenuParams _params): base(_params)
{
}
Now we'll add two commands. We need to create them in the CreateCommands
method:
public void CreateCommands()
{
// first clear list
ClearCommands(this);
// add commands individually
AddCommand(new VSCommandInfo()
{
Name = "NEW",
Owner = this,
HaveSeparator = false,
Caption = "New",
Position = 1,
ToolTip = "",
BitmapResourceId = 2,
UseOfficeResources = false
Action = new PerformActionForVSCommandDelegate(RunCommand_New)
});
AddCommand(new VSCommandInfo()
{
Name = "EDIT",
Owner = this,
HaveSeparator = false,
Caption = "Edit",
Position = 2,
ToolTip = "",
BitmapResourceId = 59,
UseOfficeResources = true
});
}
Here's some explanation about the parameters:
Name
: the command name used by the system to identify a commandOwner
: instance of the class owning the commandHaveSeparator
: does this command need a separator?Caption
: menu title of the commandPosition
: order of the menu title in the menu listToolTip
: tooltip of the commandBitmapResourceId
: ID (only numbers) of the BMP that will be shown in the menuUseOfficeResources
: if yes, searchBitmapResourceId
in Office DLLs; otherwise search bitmap in plug-in resourcesAction
: method called when command is invoked (menu item clicked)
Implement the Install
method:
/// <summary>
/// Creates main Menu and subMenus
/// </summary>
public void Install()
{
// build and store commands
CreateCommands();
// create our menu with associated commands
try
{
AddMenu(this, // the class instance
"Tools", // the VS Menu / Command we want to attach too
// flag telling base class that
// we attach to main menu (File,Edit ...)
true,
// Our Main Menu name ...
// here extracted from plugin resources
ResourceMANAGER.GetString("MY_NAME"),
// Popup = create a menu with each commands
// as submenus under choosed VS menu
MenuType.Popup,
1, // position or our main menu
true); // true = add separator
}
catch (Exception ex)
{
// do what you want ...
}
}
Implement all the remaining methods:
/// <summary>
/// Remove menu and submenus
/// </summary>
public void UnInstall()
{
RemoveMenu(this);
}
/// <summary>
/// Returns information about a command to VS
/// </summary>
/// <param name="command">command name</param>
/// <param name="NeededText">VS query status</param>
/// <param name="CommandText">command caption</param>
/// <returns></returns>
public MenuState GetState(string command,
vsCommandStatusTextWanted NeededText, ref object CommandText)
{
MenuState m = MenuState.Enabled;
bool bsupport = VStudioHelpers.IsActiveProjectSupported(VStudioApplication);
/* --------------- INFO --------------------------
* bsupport will be set to true if when running the plugin
* - a csharp project is opened
* - a Web or Winform application project is opened
---------------------------------------------*/
// translate VS command in "readable" status
QueryMenuVSCommandStatus status = TranslateCommandState(NeededText);
// perform action ...
switch (status)
{
// 2: need to change command caption ?
case QueryMenuVSCommandStatus.Step2_GetText:
if (!bsupport)
{
VSCommandInfo cmd = GetCommand(command);
if (cmd != null)
CommandText = cmd.Caption + " - NOT SUPPORTED";
else
CommandText = "NOT SUPPORTED";
}
break;
// 3 : do we enable/disable/hide the command in the menu ?
case QueryMenuVSCommandStatus.Step3_FinalState:
if (!bsupport)
m = MenuState.Disabled;
else
m = MenuState.Enabled;
break;
}
return m;
}
/// <summary>
/// Does this command belongs to this instance ?
/// </summary>
/// <param name="command">command</param>
/// <returns></returns>
public bool IsItMycommand(string command)
{
return IsItMyCommand(this, command);
}
Final steps are to create the methods to be run individually by each command.
/// <summary>
/// Code associated with command "NEW"
/// see "Action = new PerformActionForVSCommandDelegate(RunCommand_New)"
/// </summary>
private void RunCommand_New()
{
// do what you want after running custom actions
bool bSolutionLoaded = VStudioHelpers.isSolutionLoaded(VStudioApplication);
if (bSolutionLoaded)
{
ProjectType pfound = null;
EnvDTE.Project p = VStudioHelpers.GetActiveProject(VStudioApplication);
if (p != null)
{
if (VStudioHelpers.IsSupportedProject(p, out pfound))
{
System.Windows.Forms.MessageBox.Show("NEW");
}
else
System.Windows.Forms.MessageBox.Show("Unsupported Project !");
}
else
System.Windows.Forms.MessageBox.Show("No project loaded !");
}
else
System.Windows.Forms.MessageBox.Show("No Solution loaded !");
}
Your code is now ready!
Editing the add-in config file
Here is the original file content:
<?xml version="1.0" encoding="UTF-16" standalone="no"?>
<Extensibility xmlns="http://schemas.microsoft.com/AutomationExtensibility">
<HostApplication>
<Name>Microsoft Visual Studio Macros</Name>
<Version>9.0</Version>
</HostApplication>
<HostApplication>
<Name>Microsoft Visual Studio</Name>
<Version>9.0</Version>
</HostApplication>
<HostApplication>
<Name>Microsoft Visual Studio Macros</Name>
<Version>8.0</Version>
</HostApplication>
<HostApplication>
<Name>Microsoft Visual Studio</Name>
<Version>8.0</Version>
</HostApplication>
<Addin>
<FriendlyName>TestAddin - No Name provided.</FriendlyName>
<Description>TestAddin - No Description provided.</Description>
<Assembly>D:\Documents and Settings\path\TestAddin\bin\TestAddin.dll</Assembly>
<FullClassName>TestAddin.Connect</FullClassName>
<LoadBehavior>0</LoadBehavior>
<CommandPreload>0</CommandPreload>
<CommandLineSafe>0</CommandLineSafe>
</Addin>
</Extensibility>
Adding support for Visual Studio 2010
Copy the file in the Visual Studio 2010 add-in folder and just add this:
<HostApplication>
<Name>Microsoft Visual Studio Macros</Name>
<Version>10.0</Version>
</HostApplication>
<HostApplication>
<Name>Microsoft Visual Studio</Name>
<Version>10.0</Version>
</HostApplication>
Getting strings from resources
Replace the code by this one below ... VS has a special syntax: @, that indicates to load from a resource ID. Note that these are the same IDs we used in our resource files.
<FriendlyName>@MY_NAME</FriendlyName>
<Description>@MY_DESCRIPTION</Description>
<AboutBoxDetails>@MY_INFOS</AboutBoxDetails>
How to test?
- Warning: the project name (Solution Explorer) must match the project assembly name (project properties) to allow the plug-in to find resources.
- Put the "TestAddin.AddIn" file into the VS add-in folder.
- Edit the file and replace:
- Run the project and view the results.
<Assembly>TestAddin.dll</Assembly>
with:
<Assembly>[full path to ]\TestAddin.dll</Assembly>
Results
The plug-in appears in the Tools menu, but is deactivated because no C# project was loaded (this is our rule defined in the GetState
method).
The plug-in menus are now active when a C# project is loaded:
And now if we want to change the way we want the menus to appear, replace:
AddMenu(this, // the class instance
"Tools", // the ENGLISH Visual Studio Menu / Command we want to attach too
true, // flag telling base class that we attach to main menu (File,Edit ...)
ResourceMANAGER.GetString("MY_NAME"), // Our Main Menu name ...
// here extracted from plugin resources
MenuType.Popup, // Popup = create a menu with each
// commands as submenus under choosed VS menu
1, // position or our main menu
true); // true = add separator
with:
AddMenu(this, // the class instance
"Tools", // the ENGLISH Visual Studio Menu / Command we want to attach too
true, // flag telling base class that we attach to main menu (File,Edit ...)
ResourceMANAGER.GetString("MY_NAME"), // Our Main Menu name ...
// here extracted from plugin resources
MenuType.Default, // <--- HERE
1, // position or our main menu
true); // true = add separator
We get the result below. You will notice that we do not have a sub-menu any more.
Conclusion
When I started, this project was just for "fun", and when I crawled the Web and saw that no one had a simple solution to create add-ins, I decided to try it myself... and finished by creating this library. It was very difficult to determine what were the best approaches, but this is mine... You may think that some pieces of code could be optimized, and you are welcome if you have any suggestions, advices ...to enhance them. Browse the source code, it will give you more ideas than me! I hope that this will help you a lot.
History
- 2010 September 10: Initial version.
- 2010 September 15: Updated article and source code.