Up to this point, we have confined our Visual Studio automation discussions to macros. As you have seen, macros are an ideal way to control a variety of items in the IDE. Within a macro, you have access to the entire automation object model, macros are easy to write, and they come complete with their own development environment. Even with all of these positives, however, there are limitations to what a macro can do:
Visual Studio add-ins can do all of the preceding and more. Put simply, add-ins present deeper IDE integration options to developers. So, what exactly is an add-in? An add-in is a compiled DLL containing a COM object that implements a specific interface, IDTExtensibility2, which provides the add-in with a direct connection to the IDE. As we have mentioned previously, you can write add-ins in your managed language of choice. Because we have spent so much time working with Visual Basic syntax in the macro world in this chapter, all of the add-in examples will be done in C#. Probably the simplest way to get started with add-ins is to run the Add-in Wizard. As with the macro recorder, the wizard will give you a starting point for implementing your own add-ins, and by examining the code that the Add-in Wizard creates, you can learn a great deal about the makeup of an add-in. Managing Add-insVisual Studio add-ins are controlled with the Visual Studio Add-in Manager. It allows you to do two things: load and unload any registered add-in and specify how an add-in can be loaded. To access the Add-in Manager, select Tools, Add-in Manager (see Figure 11.13). Figure 11.13. Managing add-ins.This dialog box will always display a list of any available add-ins on the local machine. Checking or unchecking the box next to an add-in's name will cause the add-in to immediately load or unload. The Startup check box determines whether the add-in will load automatically when Visual Studio is started. The Command Line check box performs the same action if Visual Studio is started via the command line (such as when you are launching Visual Studio as part of an automated build scenario). Add-in Automation ObjectsTo programmatically manage add-ins, you use the DTE.AddIns collection, which contains an AddIn instance for every currently registered add-in (whether or not it is loaded). You can directly reference add-ins from the DTE.AddIns collection by using their name like this: AddIn addIn = this.DTE.AddIns.Item("MyFirstAddIn") With a valid add-in object, you can use its properties to determine whether it is loaded, query its name, or retrieve the add-in's ProgID: bool isLoaded = addIn.Connected; string name = addIn.Name; string id = addIn.ProgId; Note We use the term registered to denote an add-in that has been installed on the local machine and registered with Visual Studio. In versions prior to Visual Studio 2005, this meant that a Registry entry was created for the add-in. This concept has now been replaced with XML files: Visual Studio looks for XML files with an .addin extension to determine the list of add-ins available to be loaded (an add-in is "loaded" when it has been connected to, and loaded within, an application's host process). These .addin files are created for you automatically by the Add-in Wizard, but they can be easily created or edited by hand as well. To get a feeling for the information and structure of these files, look in the Visual Studio 2005\Addins folder under your local My Documents directory. Each registered add-in will appear here; you can poke through an add-in file by loading it into Visual Studio, Notepad, or any other text editor. So, how do you go about creating your own add-in? The easiest way is to start with the Add-in Wizard. Running the Add-in WizardThe Add-in Wizard is launched whenever you try to create a new project of the type Visual Studio Add-in. From the File, New Project dialog box, select the Extensibility node in the project types tree (Visual C#, Other Project Types, Extensibility). From here, you can see two project templates: Visual Studio Add-in and Shared Add-in (see Figure 11.14). Figure 11.14. Selecting the Visual Studio add-in project type.We'll touch on the differences between these two project types in a bit; for now, we are interested in the Visual Studio Add-in template. Clicking OK will start the Add-in Wizard. Selecting a LanguageAfter an initial welcome page, you can select the language you want to use for the add-in (see Figure 11.15). The list of languages available will depend on two things:
Figure 11.15. Picking your add-in language.
Visual Studio add-ins support Visual C#, Visual Basic, Visual J#, and both managed and unmanaged Visual C++. Picking an Application HostAfter selecting a language, you are presented with a question about "application hosts." This screen, shown in Figure 11.15, is really just asking where you want the add-in to run. Figure 11.16. Selecting the application host.
Because you have indicated that this is a Visual Studio Add-in and not a shared add-in, your host options essentially are the Visual Studio IDE or the Macros IDE. Note This is a good time to discuss the differences between a Visual Studio add-in and a shared add-in. A shared add-in is the moniker given for add-ins hosted inside a Microsoft Office application, such as Microsoft Word or Microsoft Excel. A Visual Studio add-in can only be hosted within the Visual Studio or Macros IDE. If you run through the Add-in Wizard for a shared add-in, you will find that the page which asks you to select an application host (or hosts) will be populated with a list of the installed Microsoft Office applications; you won't be able to select Visual Studio as an application host for a shared add-in. Describing the Add-inThe name and description you enter on page 3 of the wizard (see Figure 11.17) are visible in the Add-in Manager when the add-in is selected. This information is intended to give users an idea as to the add-in's functionality and purpose. Figure 11.17. Giving the add-in a name and description.
Setting Add-in OptionsThe next wizard page, shown in Figure 11.18, allows you to specify various add-in options. You can indicate whether you want the add-in to appear in the Tools menu, when you want the add-in to load, and whether the add-in could potentially display a modal dialog box during its operation. Figure 11.18. Setting add-in options.
Setting About Box InformationThe second-to-last wizard page captures the text that Visual Studio will display in its About Box dialog box (see Figure 11.19). Figure 11.19. Entering text for the Visual Studio About Box dialog box.
This is the place to include such details as where users can contact the author of the add-in, support and licensing information, copyright and version information, and so on. Finishing the WizardThe last page of the wizard contains a summary of the options that you have selected. After you click the Finish button, the wizard will start creating the code for your add-in based on all the selections you have made in the wizard. Because add-ins are DLLs, the Add-in Wizard will create the add-in source as part of a class library project in the IDE. The primary code file that is created implements a class called Connect. This class inherits from all of the necessary COM interfaces to make the add-in work in the context of the IDE. Listing 11.5 shows the Connect class as it was generated by the Add-in Wizard. Listing 11.5. Code Generated by the Add-in Wizard
At this stage, the add-in doesn't actually do anything. You still have to implement the custom logic for the add-in. What the wizard has done, however, is implement much (if not all) of the tedious plumbing required to wire the add-in into the IDE, expose it on the Tools menu, and intercept the appropriate extensibility events to make the add-in work. Now that you have a baseline of code to work with, you're ready to examine the source to understand the overall structure and layout of an add-in. The Structure of an Add-inThe first thing to notice is that the Connect class inherits from two different interfaces: IDTCommandTarget and IDTExtensibility2. public class Connect : IDTExtensibility2, IDTCommandTarget The IDTCommandTarget interface provides the functionality necessary to expose the add-in via a command bar. The code to inherit from this interface was added by the wizard because the Yes, Create a Tools Menu Item box was checked on page 4. The IDTExtensibility2 interface provides the eventing glue for add-ins. It is responsible for all of the events that constitute the life span of an add-in. The Life Cycle of an Add-inAdd-ins progress through a sequence of events every time they are loaded or unloaded in their application host. Each of these events is represented by a method defined on the IDTExtensibility2 interface. These methods are documented in Table 11.1.
The diagrams in Figure 11.20 and Figure 11.21 show how these methods (which really represent events) fall onto the normal load and unload path for an add-in. Figure 11.20. Load sequence of events.
Figure 11.21. Unload sequence of events.
If you look back at the template code for the add-in, you can see that each one of these IDTExtensibility2 methods has been implemented. The OnDisconnection, OnAddInsUpdate, OnStartupComplete, and OnBeginShutdown methods are empty; the wizard has merely implemented the method signature. The OnConnection method, however, already has a fair bit of code to it before you even lift a hand to modify or add to the wizard-generated code. Now you're ready to investigate what happens in each of the IDTExtensibility2 methods. OnAddInsUpdateThe OnAddInsUpdate method is called when any add-in is loaded or unloaded from Visual Studio; because of this, the OnAddInsUpdate method is primarily useful for enforcing or dealing with dependencies between add-ins. If your add-in depends on or otherwise uses the functionality contained in another add-in, this is the ideal injection point for containing the logic that deals with that relationship. Here is the OnAddInsUpdate method as implemented by the Add-in Wizard: /// <summary>Implements the OnAddInsUpdate method of the IDTExtensibility2 /// interface. Receives notification when the collection of Add-ins has /// changed.</summary> /// <param term='custom'>Array of parameters that are host application /// specific.</param> /// <seealso class='IDTExtensibility2' /> public void OnAddInsUpdate(ref Array custom) { } Tip Because you don't know which add-in has triggered the OnAddInsUpdate method, you would need to iterate through the DTE.AddIns collection and query each add-in's Connected property to determine its current state. OnBeginShutdownOnBeginShutdown is called for every running add-in when Visual Studio begins its shutdown sequence. If an IDE requires any clean-up code (including perhaps resetting IDE settings that have been changed during the add-in's life), you would place that code within this method. A user may elect to cancel Visual Studio's shutdown process. OnBeginShutdown will fire regardless of whether the Visual Studio shutdown process was successful. This forces you, as an add-in author, to always assume that Visual Studio has, in fact, terminated and therefore act accordingly in your code. Here is the OnBeginShutdown method: /// <summary>Implements the OnBeginShutdown method of the IDTExtensibility2 interface. Receives notification that the host application is being unloaded.</summary> /// <param term='custom'>Array of parameters that are host application specific.</param> /// <seealso class='IDTExtensibility2' /> public void OnBeginShutdown(ref Array custom) { } OnConnectionOnConnection indicates that an add-in has been loaded. It accepts four parameters: public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom) The first parameter, application, is the most important; it provides a reference to the DTE object representing the IDE. You know from the preceding chapter that the DTE object is the key to accessing the entire automation object model. With macros, the DTE object is held as a global variable. For add-ins, the OnConnection method is the sole provider of this object, thus providing the crucial link between the add-in and its host IDE. The second parameter, connectMode, is an ext_ConnectMode enumeration. It indicates exactly how the add-in was loaded (see Table 11.2 for a list of the possible ext_ConnectMode values).
The addInInst parameter is actually a reference to the add-in itself. And last, the custom parameter is an empty Array object. This array is passed by reference and can be used to pass parameters into and out of the add-in. The Add-in Wizard has taken the first two parameters, explicitly cast them to their underlying types, and assigned them into two class fields for later reference: _applicationObject = (DTE2)application; _addInInstance = (AddIn)addInInst; The next block of code examines the ext_ConnectMode value. If this is the first time that the add-in was loaded (for example, ext_ConnectMode is equal to ext_cm_UISetup), then the code does two things: It creates a Tools menu entry for the add-in, and it creates a custom, named command to launch the add-in (this named command is called when you select the add-in from the Tools menu). if(connectMode == ext_ConnectMode.ext_cm_UISetup) { object []contextGUIDS = new object[] { }; Commands2 commands = (Commands2)_applicationObject.Commands; string toolsMenuName; try { //If you would like to move the command to a different menu, change the // word "Tools" to the English version of the menu. // This code will take the culture, append on the name of the menu // then add the command to that menu. You can find a list of all // the top-level menus in the file // CommandBar.resx. ResourceManager resourceManager = new _ ResourceManager("MyFirstAddin.CommandBar", _ Assembly.GetExecutingAssembly()); CultureInfo cultureInfo = new _ System.Globalization.CultureInfo(_applicationObject.LocaleID); string resourceName = String.Concat(cultureInfo.TwoLetterISOLanguageName, "Tools"); toolsMenuName = resourceManager.GetString(resourceName); } catch { //We tried to find a localized version of the word Tools, but one //was not found. // Default to the en-US word, which may work for the current culture. toolsMenuName = "Tools"; } //Place the command on the tools menu. //Find the MenuBar command bar, which is the top-level command bar holding //all the main menu items: Microsoft.VisualStudio.CommandBars.CommandBar menuBarCommandBar = _ ((Microsoft.VisualStudio.CommandBars.CommandBars)__ applicationObject.CommandBars)["MenuBar"]; //Find the Tools command bar on the MenuBar command bar: CommandBarControl toolsControl = menuBarCommandBar.Controls[toolsMenuName]; CommandBarPopup toolsPopup = (CommandBarPopup)toolsControl; //This try/catch block can be duplicated if you wish to add multiple commands //to be handled by your Add-in, // just make sure you also update the QueryStatus/Exec method to include // the new command names. try { //Add a command to the Commands collection: Command command = commands.AddNamedCommand2(_addInInstance, _ "MyFirstAddin", "MyFirstAddin", _ "Executes the command for MyFirstAddin", true, 59, _ ref contextGUIDS, (int)vsCommandStatus.vsCommandStatusSupported +(int)vsCommandStatus.vsCommandStatusEnabled, (int)vsCommandStyle.vsCommandStylePictAndText, vsCommandControlType.vsCommandControlTypeButton); //Add a control for the command to the tools menu: if((command != null) && (toolsPopup != null)) { command.AddControl(toolsPopup.CommandBar, 1); } } catch(System.ArgumentException) { //If we are here, then the exception is probably because a command with //that name already exists. If so there is no need to recreate the //command and we can // safely ignore the exception. } Tip You can see that the Add-in Wizard is quite liberal with its code comments; when you set out to write your own add-in, it is often useful to read the auto-generated comments and use copy/paste methods to duplicate functionality that the wizard has generated for you. OnDisconnectionOnDisconnection fires when the add-in is unloaded from Visual Studio. This is the opposite action from that signaled by the OnConnection method. As with OnConnection, an enumerationext_DisconnectModeis provided to this method that indicates the circumstances surrounding the unload action. For a list of the possible ext_DisconnectMode values, see Table 11.3.
Here is the OnDisconnection method:
OnStartupCompleteIf an add-in is set to load automatically during Visual Studio startup, the OnStartupComplete method will fire after that add-in has been loaded. Here is the OnStartupComplete method: /// <summary>Implements the OnStartupComplete method of the IDTExtensibility2 /// interface. Receives notification that the host application has completed /// loading.</summary> /// <param term='custom'>Array of parameters that are host application /// specific.</param> /// <seealso class='IDTExtensibility2' /> public void OnStartupComplete(ref Array custom) { } Reacting to CommandsAdd-ins can react to commands issued within the IDE. If you recall from the discussion on commands in the preceding chapter, and in the previous section on macros, this is done through the concept of "named commands." A named command is really nothing more than an action that has a name attached to it. You already know that Visual Studio comes with its own extensive set of commands that cover a wide variety of actions in the IDE. Using the Commands/Commands2 collection, you can create your own named commands by using the AddNamedCommand2 method. To repeat the dissection of the OnConnection method, the wizard has created a body of code responsible for creating a new named command, adding it to the Tools menu, and then reacting to the command. The IDTCommandTarget.Exec method is the hook used to react to an issued command. Here is its prototype: void Exec ( [InAttribute] string CmdName, [InAttribute] vsCommandExecOption ExecuteOption, [InAttribute] ref Object VariantIn, [InAttribute] out Object VariantOut, [InAttribute] out bool Handled ) To handle a command issued to an add-in, you write code in the Exec method that reacts to the passed-in command. CmdName is a string containing the name of the command; this is the token used to uniquely identify a command, and thus is the parameter you will examine in the body of the Exec method to determine if and how you will react to the command. ExecuteOption is a vsCommandExecOption enumeration that provides information about the options associated with the command (see Table 11.4).
The VariantIn parameter is used to pass any arguments needed for the incoming command, and VariantOut is used as a way to pass information back out of the add-in to the caller. Lastly, Handled is a Boolean that indicates to the host application whether the add-in handled the command. As a general rule, if your add-in processed the command, it will set this to TRue. Otherwise, it will set it to false, which is a signal to Visual Studio that it needs to continue to look for a command invocation target that will handle the command. The code to handle the Tool menu command looks like this: /// <summary>Implements the Exec method of the IDTCommandTarget ///interface. This is called when the command is invoked.</summary> /// <param term='commandName'>The name of the command to execute.</param> /// <param term='executeOption'>Describes how the command should ///be run.</param> /// <param term='varIn'>Parameters passed from the caller to the command /// handler.</param> /// <param term='varOut'>Parameters passed from the command handler to /// the caller.</param> /// <param term='handled'>Informs the caller if the command was handled /// or not.</param> /// <seealso class='Exec' /> public void Exec(string commandName, vsCommandExecOption executeOption, ref object varIn, ref object varOut, ref bool handled) { handled = false; if(executeOption == vsCommandExecOption.vsCommandExecOptionDoDefault) { if(commandName == "MyFirstAddin.Connect.MyFirstAddin") { handled = true; return; } } } A Sample Add-in: Color PaletteTo cap this discussion of add-ins, let's look at the process of developing a functioning add-in from start to finish. The add-in will be a color picker. It will allow users to click on an area of a color palette, and the add-in will then emit code to create an instance of a Color structure that matches the selected color from the palette. Here is a summary list of requirements for the add-in:
Getting StartedTo start the development process, you will create a new solution and a Visual Studio Add-in Project called PaletteControlAddIn. The Add-in Wizard will create a code base for you inside a Connect class just as you saw earlier in this chapter. The Connect class is the place where all of the IDE and automation object modelspecific code will go. In addition to the core add-in plumbing, you will also need to create a User Control class that encapsulates the user interface and the processing logic for the add-in. Creating the User ControlFirst, you can work on getting a user control in place that has the functionality you are looking for. After you have a workable control, you can worry about wiring that control into Visual Studio using the Connect class created by the Add-in Wizard. Add a user control (called PaletteControl) to the add-in project by selecting Project, Add User Control. After the control is added, you'll immediately add a picture box to the design surface. The picture box will display the palette of colors, stored as a simple bitmap in a resource file. With the palette in place, you now need a few label controls to display RGB values per your requirements. And finally, in the finest tradition of gold-plating, you'll also add an additional picture box that repeats the current color selection and a label that shows the code that you would generate to implement that color in a Color structure. Figure 11.22 provides a glimpse of the user control after situating these controls on the designer. Figure 11.22. The PaletteControl user control.Handling Movement over the PaletteWith the UI in place, you can now concentrate on the code. First, you can add an event handler to deal with mouse movements over the top of the palette picture box. With the MouseMove event handler, you can update your label controls and the secondary picture box instantly as the pointer roves over the palette bitmap: public PaletteControl() { InitializeComponent(); this.pictureBox1.MouseMove += new MouseEventHandler(pictureBox1_MouseMove); this.pictureBox1.Cursor = System.Windows.Forms.Cursors.Cross; } void pictureBox1_MouseMove(object sender, MouseEventArgs e) { // Get the color under the current pointer position Color color = GetPointColor(e.X, e.Y); // Update the RGB labels and the 2nd pic box // using the retrieved color DisplayColor(color); // Generate our VB or C# code for the Color // structure SetCode(color); } Looking at the Code Generation PropertiesThe PaletteControl class will expose two properties: Code is a string property that holds the Color structure code generated by clicking on the palette, and GenerateVB is a Boolean that specifies whether the control should generate Visual Basic code (GenerateVB = true) or C# code (GenerateVB = false). Here are the field and property declarations for these two properties: string _code = ""; public string Code { get { return _code; } } bool _generateVB = false; public string GenerateVB { get { return _generateVB; } } Implementing the Helper RoutinesWhenever the mouse pointer moves over the picture box region, you need to capture the color components of the point directly below the cursor (GetPointColor), update the label controls and the secondary picture box control to reflect that color (DisplayColor), and then generate the code to implement a matching color structure (SetCode). Here are the implementations of these routines: /// <summary> /// Returns a Color structure representing the color of /// the pixel at the indicated x and y coordinates. /// </summary> /// <param name="x"></param> /// <param name="y"></param> /// <returns>A Color structure</returns> private Color GetPointColor(int x, int y) { // Get the bitmap from the palette picture box Bitmap bmp = (Bitmap)pictureBox1.Image; // Use GetPixel to retrieve a color // structure for the current pointer position Color color = bmp.GetPixel(x, y); // Return the color structure return color; } /// <summary> /// Displays the RGB values for the given color. Also sets /// the background color of the secondary picture box. /// </summary> /// <param name="color">The Color to display</param> private void DisplayColor(Color color) { // pull out the RGB values from the // color structure string R = color.R.ToString(); string G = color.G.ToString(); string B = color.B.ToString(); // set our secondary picture box // to display the current color this.pictureBox2.BackColor = color; // display RGB values in the label // controls this.labelR.Text = R; this.labelG.Text = G; this.labelB.Text = B; } /// <summary> /// Generates a string representing the C# or VB code necessary to /// create a Color structure instance that matches the passed in /// Color structure. This string is then assigned to this /// user control's _code field. /// </summary> /// <param name="color">The color to represent in code.</param> /// <param name="isVB">Boolean flag indicating the language /// to use: false indicates C#, true indicates VB</param> private void SetCode(Color color, bool isVB) { // Read in add-in settings from registry SetPropFromReg(); string code = ""; if (isVB) { code = "Dim color As Color = "; } else { code = "Color color = "; } code = code + "Color.FromArgb(" + color.R.ToString() + ", " + color.G.ToString() + ", " + color.B.ToString() + ");"; _code = code; this.labelCode.Text = _code; } /// <summary> /// Reads a registry entry and sets the language output fields /// appropriately. /// </summary> private void SetPropFromReg() { RegistryKey regKey = Registry.CurrentUser.OpenSubKey(@"Software\Contoso\Addins\ColorPalette"); string codeVal = (string)regKey.GetValue("Language", "CSharp"); if (codeVal == "CSharp") { _generateVB = false; } else { _generateVB = true; } } Signaling a Color SelectionBecause you will need some way for the control to indicate that a user has selected a color (for example, has clicked on the palette), you will also define an event on the user control class that will be raised whenever a click is registered in the palette picture box: public event EventHandler ColorSelected; protected virtual void OnColorSelected(EventArgs e) { if (ColorSelected != null) ColorSelected(this, e); } private void pictureBox1_Click(object sender, EventArgs e) { OnColorSelected(new EventArgs()); } Tip To isolate and test the user control, you may want to add a Windows forms project to the solution and host the control on a Windows form for testing. Just drop the control onto the form and run the forms project. With the user control in place, you are ready to proceed to the second stage of the add-in's development: wiring the user control into the IDE. Finishing the Connect ClassThe Connect class already has the basic add-in code; now it's time to revisit that code and add the custom code to drive the user control. You'll want the add-in to integrate seamlessly into the development environment, so you can use a tool window to display the user control that you previously created. Harking back to the discussions of the automation object model, you know that the Windows2 collection has a CreateToolWindow2 method, which allows you to create your own custom tool windows. Note Prior versions of Visual Studio required you to create a shim control (using C++) that would host a control for display in a tool window. The tool window, in turn, would then host the shim. With Visual Studio 2005 (and its improved Windows2.CreateToolWindow2 method), this is no longer necessary. Now you can directly host a managed user control in a tool window. Here is the method prototype: Window CreateToolWindow2 ( AddIn Addin, string Assembly, string Class, string Caption, string GuidPosition, [InAttribute] out Object ControlObject ) Displaying the Tool Window and User ControlBecause you want the tool window to be created and displayed after the add-in has loaded, this CreateToolWindow2 method call will be placed in the Connect.OnConnection method. First, you set up a local object to point to the DTE.ToolWindowscollection: // The DTE.ToolWindows collection Windows2 toolWindows= (Windows2)_applicationObject.Windows; Then you need an object to hold the reference to the tool window that you will create: // Object to refer to the newly created tool window Window2 toolWindow; And finally, you need to create the parameters to feed to the CreateToolWindow2 method: // Placeholder object; will eventually refer to the user control // hosted by the user control object paletteObject = null; // This section specifies the path and class name for the palette // control to be hosted in the new tool window; we also need to // specify its caption and a unique GUID. Assembly asm = System.Reflection.Assembly.GetExecutingAssembly(); string assemblyPath = asm.Location; string className = "PaletteControlAddIn.PaletteControl"; string guid = new Guid().ToString(); string caption = "Palette Color Picker"; With that in place, you are only a few lines of code away from creating and displaying the tool window: // Create the new tool window with the hosted user control toolWindow = (Window2)toolWindows.CreateToolWindow2(_addInInstance, assemblyPath, className, caption, guid, ref paletteObject); // If tool window was created successfully, make it visible if (toolWindow != null) { toolWindow.Visible = true; } Capturing User Control EventsThe add-in is missing one last piece: You need to react whenever the user clicks on the palette by grabbing the generated code (available from the PaletteControl.Code property) and inserting it into the currently active document. There are two tasks at hand. First, you need to write an event handler to deal with the click event raised by the PaletteControl object. But to do that, you need a reference to the user control. This is the purpose of the paletteObject object that you pass in as the last parameter to the CreateToolWindow2 method. Because this is passed in by reference, it will hold a valid instance of the PaletteControl after the method call completes and returns. You can then cast this object to the specific PaletteControl type, assign it to a field within the Connect class, and attach an event handler to the PaletteControl.ColorSelected event: // retrieve a reference back to our user control object _paletteControl = (PaletteControl)paletteObject; // wire up event handler for the PaletteControl.ColorSelected event _paletteControl.ColorSelected += new System.EventHandler(paletteControl1_ColorSelected); Tip Getting a reference to the user control can be a bit tricky. If the user control is not a part of the same project as your add-in class, CreateToolWindow2 will return only a null value instead of a valid reference to the user control. If you want to develop your user control outside the add-in project, you have to make sure that the user control is fully attributed to be visible to calling COM components. See the topic "Exposing .NET Framework Components to COM" in MSDN for details on how this is accomplished. Inserting the Generated CodeYou react to the ColorSelected event by grabbing the content of the PaletteControl.Code property and writing it into the currently active document. Again, you will use your automation object model knowledge gained from the preceding chapter to make this happen. The DTE.ActiveDocument class will hold a reference to the currently active document. By using an edit point, you can easily write text directly into the text document: TextDocument currDoc = _applicationObject.ActiveDocument.Object; EditPoint2 ep = currDoc.Selection.ActivePoint.CreateEditPoint(); ep.Insert(_paletteControl.Code); ep.InsertNewLine(); Exposing Add-in SettingsThe final step is to make the add-in's language choice a configurable option. Users should be able to indicate whether they want the add-in to emit C# or Visual Basic code. To do this, you need to have a user interface in the form of an Options page (that will display in the Options dialog box), and you need a place to persist the option selections. Creating the Option Page UIAdd-ins can reference an Options page that will appear in the Tools Options dialog box. Again, as you did with the custom tool window, you will build a user control to implement the logic and the user interface for the Options page. You start by creating a new user control to the existing add-in project. For this example, call this class PaletteControlOptionPage. Adding a label control and two radio button controls will enable you to indicate the language preference for the palette add-in. Figure 11.23 shows the design surface of the Options page. Figure 11.23. The user control's design surface.The user control for the Options page needs to inherit from IDTToolsOptionsPage: public partial class PaletteControlOptionPage : UserControl, IDTToolsOptionsPage { public PaletteControlOptionPage() { InitializeComponent(); } } The IDTToolsOptionsPage interface defines five methods, outlined in Table 11.5.
These methods are called as the Options page progresses through its normal sequence of states, as you can see in Figure 11.24. Figure 11.24. Tools Options page action sequence.By placing code within these methods, you can read in and store any configuration changes that a user makes through the Options page. In this case, you can keep things simple: Read in a value from a Registry entry as part of the OnAfterCreated method and update that same entry as part of the OnOK method: public void OnAfterCreated(DTE DTEObject) { // read our current value from registry // TODO: we should really include contingency code here for creating // the key if it doesn't already exist, dealing with unexpected values, // exceptions, etc. RegistryKey regKey = Registry.CurrentUser.OpenSubKey(@"Software\Contoso\Addins\ColorPalette"); string codeVal = (string)regKey.GetValue("Language", "CSharp"); if (codeVal == "CSharp") { this.radioButtonCSharp.Checked = true; this.radioButtonVB.Checked = false; } else { this.radioButtonCSharp.Checked = true; this.radioButtonVB.Checked = false; } } public void OnOK() { string codeValue = "CSharp"; // our default value if (this.radioButtonVB.Checked) { codeValue = "VB"; } // update the registry with the new setting RegistryKey regKey = Registry.CurrentUser.OpenSubKey(@"Software\Contoso\Addins\ColorPalette"); regKey.SetValue("Language", codeVal); } Note It is up to you to decide where and how you persist your add-in's settings. The Registry is one logical place; you could also elect to store your settings in an XML file that is deployed along with your binaries. Registering the Options PageThe registration mechanism for an Options page is the same as that for an add-in: The .addin file is used. By adding a few lines of XML, you can indicate to Visual Studio that an Options page exists with the custom add-in. You can do this easily by editing the .addin file right in Visual Studio (because it is automatically created as part of the project). To include the necessary XML registration information, edit the .addin file and place the following XML before the closing </extensibility> tag:
You use the Category tag to specify the name of the option category displayed in the Tools Options dialog box. The SubCategory tag specifies the subnode under that category. The Assembly tag provides a path to the add-in's DLL file, and the FullClassName tag contains the full name for the add-in class. With this final step complete, the add-in is fully functional. You can compile the project and then immediately load the add-in using the Add-in Manager. Figure 11.25 shows the add-in in action, and a complete code listing for the Connect, PaletteControl, and PaletteControlOptionPage classes (in that order) is provided in Listing 11.6. Figure 11.25. The color palette add-in.Listing 11.6. The Connect, PaletteControl, and PaletteControlOptionPage Classes
|