Setting Up the Add-In
Practical code-generation solutions should seamlessly integrate with the developer's normal activities. I begin this project by creating the add-in that will trigger code generation whenever the config file is closed (or whenever the developer chooses to generate code by selecting a menu option).
Defining the Add-In
I start a new code-generation project either by extending an existing add-in with similar functionality or creating a new one. For this project, I start a new add-in. I always begin with the simplest possible interface for triggering the code generation: a single menu item that runs the solution. This simplifies testing and debugging. Near the end of the project, I convert this add-in to run when the project is built or when the configuration file is closed.
From the File menu, I select New Project and, in the New Project dialog, under Extensibility, I select the Visual Studio Add-In template. After giving my add-in a name (I used "ConnectionManager") and specifying a folder to keep it in, I click the OK button to start the wizard. In the wizard, I take the following actions:
- Select C# as the language.
- Deselect Microsoft Visual Studio Macros.
- Replace the default name and description for the add-in with my own text.
- Select all three choices on the fourth page:
- Add a command to the Tools menu.
- Have the add-in load with Visual Studio.
- Promise not to put up a modal dialog.
- Add some information for the About dialog.
Once the project is created, I modify the project's properties (as described in Chapter 2, "Integrating with Visual Studio"):
- Set the assembly name (and make the same change in the <Assembly> element of the two .addin files).
- In the Build Events tab (C#) or on the Compile tab after clicking the Build Events button (Visual Basic), I add these two lines to the Pre-build event command line. (This code is spread over two lines to fit on the page, but the second and third line should be entered as one line in the Pre-build text box.)
if exist "$(TargetPath).locked" del "$(TargetPath).locked" if exist "$(TargetPath)" if not exist "$(TargetPath).locked" move "$(TargetPath)" "$(TargetPath).locked"
Creating the Menu
Once the project is generated, my next step is to modify the code in the Connect.cs file. One of my goals when designing the Connect.cs file is to create a version that doesn't require many changes when setting up a new add-in. To support that, in the Connect.cs file I add four fields (named menuName, menuItemName, menuItemCaption, and menuTooltip) at the top of the class. If all that's required in an add-in is a single menu item on a menu (and that's always my start point for any solution), the only changes required are to the values of these four fields:
string menuName = "Tools"; string menuItemName = "ConStrGentr"; string menuItemCaption = "Generate Connection String Class"; string menuToolTip = "Create a class for managing connection strings";
I then replace all the code in the OnConnection method with the following code in Visual Studio 2005/2008, which uses my fields to find the menu specified in my four fields:
public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom) { _applicationObject = (DTE2)application; _addInInstance = (AddIn)addInInst; if (connectMode == ext_ConnectMode.ext_cm_UISetup) { object[] contextGUIDS = new object[] { }; Commands2 commands = (Commands2)_applicationObject.Commands; string FoundMenuName; try { System.Resources.ResourceManager resourceManager = new System.Resources.ResourceManager( _addInInstance.ProgID + ".CommandBar", System.Reflection.Assembly.GetExecutingAssembly()); System.Globalization.CultureInfo cultureInfo = new System.Globalization.CultureInfo(_applicationObject.LocaleID); if (cultureInfo.TwoLetterISOLanguageName == "zh") { System.Globalization.CultureInfo parentCultureInfo = cultureInfo.Parent; FoundMenuName = resourceManager.GetString( String.Concat(parentCultureInfo.Name, menuName)); } else { FoundMenuName = resourceManager.GetString(String.Concat( cultureInfo.TwoLetterISOLanguageName, menuName)); } } catch (Exception e) { FoundMenuName = menuName; } if (FoundMenuName == "") { FoundMenuName = menuName; }
The equivalent code in Visual Studio 2010 looks like this:
CommandBar cb; bool MainMenu = true; string MenuBarName = "Menubar"; if (MainMenu) { cb = ((CommandBars)_applicationObject.CommandBars)[MenuBarName]; cb = ((CommandBarPopup)cb.Controls[FoundMenuName]).CommandBar; } else { CommandBars cbs = (CommandBars)_applicationObject.CommandBars; cb = cbs[FoundMenuName]; }
In Visual Studio 2008/2010, the next code adds a new menu item (with the name specified in menuItemName) to the menu I just found, with the caption and tooltip specified in menuItemCaption and menuToolTip:
Commands2 cmds = (Commands2)_applicationObject.Commands; CommandBars cbs = (CommandBars)_applicationObject.CommandBars; CommandBar cb = cbs[FoundMenuName]; Command NamedCommand = null; try { NamedCommand = _applicationObject.Commands.Item( _addInInstance.ProgID + menuItemName, 1); } catch { try { NamedCommand = cmds.AddNamedCommand2(_addInInstance, menuItemName, menuItemCaption, menuToolTip, true,50, ref contextGUIDS (int) vsCommandStatus.vsCommandStatusSupported + (int) vsCommandStatus.vsCommandStatusEnabled, (int) vsCommandStyle.vsCommandStylePictAndText, vsCommandControlType.vsCommandControlTypeButton); } catch {} try { CommandBarControl cbc = cb.Controls[menuItemCaption]; } catch { NamedCommand.AddControl(cb, 1); } } } }
In Visual Studio 2005, I would need to replace the code that uses AddNamedCommand2 with code that uses AddNamedCommand:
Command command = cmds.AddNamedCommand(_addInInstance, menuItemName,menuItemCaption, menuToolTip, true,59,ref contextGUIDS, (int) vsCommandStatus.vsCommandStatusSupported) + (int) vsCommandStatus.vsCommandStatusEnabled);
In addition to replacing the code in the OnConnection method, I also need to modify the code in the QueryStatus method to allow me to use any menu item created by this add-in:
public void QueryStatus(string commandName, vsCommandStatusTextWanted neededText, ref vsCommandStatus status, ref object commandText) { if(neededText == vsCommandStatusTextWanted.vsCommandStatusTextWantedNone) { if (commandName.StartsWith(_addInInstance.Name + ".Connect")) { status = (vsCommandStatus)vsCommandStatus. vsCommandStatusSupported |vsCommandStatus.vsCommandStatusEnabled; return; } } }
Calling the Solution
To make the Connect.cs class as portable as possible, I put my code-generation solution in a separate class. This means that the only change required to the Exec method of Connect.cs is the name of the class and method that implements the solution.
For this solution, I have the Exec method instantiate a class called DatabaseUtilities and call a method named GenerateConnectionManager. I pass the DTE2 object that provides access to Visual Studio to the constructor for this code-generation class. As a result, the code in this Exec method to create the DatabaseUtilities class, pass the _applicationObject variable that holds the DTE2 object, and call the GenerateConnectionClass method looks like this:
public void Exec(string commandName, vsCommandExecOption executeOption, ref object varIn, ref object varOut, ref bool handled) { handled = false; if(executeOption == vsCommandExecOption. vsCommandExecOptionDoDefault) { if (commandName == _addInInstance.ProgID + "." + menuItemName) { DatabaseUtiltiies dbu = new DatabaseUtiltiies(_applicationObject); dbu.GenerateConnectionClass(); handled = true; return; } } }