From the February 2002 issue of MSDN Magazine

MSDN Magazine

Visual Studio .NET

Custom Add-Ins Help You Maximize the Productivity of Visual Studio .NET

Leo A. Notenboom

This article assumes you're familiar with C#, Visual Basic .NET, and the CLR

Level of Difficulty     1   2   3 

Download the code for this article: VSIDE.exe (310KB)

SUMMARY Regardless of how great an integrated development environment (IDE) is, there will always be features that developers wish had been included. For that reason, the Visual Studio .NET IDE provides an extensive add-in facility that allows you to add nearly unlimited numbers of features and functionalities written in Visual Basic, C, C++, C#, or any .NET-compliant language. This article explains how add-ins work in Visual Studio .NET. It then shows how to add custom text editing by creating an add-in with two editing functions, a simple text insert of the current date, and a more complex function to reformat paragraphs of text. Finally, you'll learn how to add a page to the Options dialog.

If you've started using Microsoft® Visual Studio® .NET you know it is packed with new features and technology. While you may think of the integrated development environment (IDE) as simply a text editor for writing code, it's really much more. It provides the framework that the rest of the development tools plug into, offering a single, seamless, development experience. Of course, it can't be all things to all people; functions you or I find useful might not be there. Fortunately the IDE has been equipped with extensibility features that allow extensive customization of Visual Studio .NET to meet just about any need.
      In 1988 I wrote an article for Microsoft Systems Journal entitled "Customizing the Features of the M Editor Using Macros and C Extensions." That article discussed the macro language unique to the M editor, as well as the approach developers could use to extend the editor by writing code in C. Fortunately, in the years since, macros and macro recording have converged on a more common mechanism and language. Extending the IDE via compiled code, however, remains a powerful, albeit complex, method for modifying the coding environment.
      In this article I'll describe an add-in I developed for Visual Studio .NET that implements several new text editing functions. In detailing some of the steps involved, you'll see how add-ins interface with the IDE's automation model.
      Note that the Visual Studio .NET macro recorder uses Visual Basic® .NET by default. Visual Basic isn't a requirement and to prove the point, I'll use C#. Like Visual Basic .NET, C# also runs in the .NET Framework managed environment, providing a secure setting for building add-ins.

Getting Started

      Both add-ins and macros can be used to extend the IDE in a number of ways. Macros are recordable and can be run immediately. Therefore, they are a great way to explore the object model. Macros are distributed as a single .vsmacros file, and are loaded into the macro editor simply by double-clicking that file. Since macros are available in source form, they can easily be modified by the user.
      Add-ins, on the other hand, are compiled and thus cannot be modified once distributed. This protects your intellectual property. With add-ins, you can create tool windows that operate just like those native to Visual Studio .NET. Add-ins can dynamically alter the state of the commands on menus and the command bar and even add information to the Help About box. Since add-ins can be distributed as single Microsoft Installer (.MSI) files, they can be easily installed and uninstalled through the standard Control Panel Add/Remove Programs applet dialog.
      The steps to create an add-in are covered in the Visual Studio .NET online help, as well as on the Visual Studio .NET Automation Examples Web site (https://msdn.microsoft.com/vstudio/nextgen/automation.asp). I won't go through every step in detail here, but will review some of the choices I made to create the add-in.
      To create an add-in, you start by creating a new project. Underneath Other Projects in the New Projects dialog, you'll find Extensibility Projects. From there, select Visual Studio .NET Add-in. Figure 1 shows the New Project dialog at this point. Press OK and the add-in wizard will start.

Figure 1 Creating a New Project
Figure 1 Creating a New Project

      For my add-in, the first three pages of the wizard are quite straightforward. On Page 1, I selected C# as the programming language for the add-in. On Page 2, I chose Microsoft Visual Studio .NET as the application host. This is where the editing functions that I'll present here make the most sense. You can, of course, include the VSMacros IDE for editing functions of your own design. On Page 3, I named the add-in "Text Editing Utilities," and provided an appropriate description.

Figure 2 Add-in Wizard
Figure 2 Add-in Wizard

      Page 4 of the wizard, which is shown in Figure 2, provides you with several options.

  • Check "Yes, Create a 'Tools' menu item." This option also enables editing command processing in your add-in, even if you use no UI.<\li>
  • Leave "My Add-in will never put up modal UI �" unchecked. The example I provide here will not put up a UI, but it's quite possible that you'll want to further extend your add-in to do so.
  • Leave "I would like my Add-in to load when the host application starts" unchecked for now. This will make debugging a little easier. The user of your add-in can change this option in the Add-in Manager later.
  • I checked "My Add-in should be available to all users �." This is optional. On my machine I'm really the only user, as I'm sure is the case for most developers. This option just changes which registry hive the add-in is registered in.

      On Page 5 of the wizard, you can include some Help About information. Check the checkbox and enter whatever contact information is appropriate for you. And now, courtesy of the New Project Wizard, you have the beginnings of your add-in.
      The following methods in the Connect object are the workhorses of the add-in and can be found in connect.cs, which is now part of your new project.

  • Connect::Connect. This constructor is where you'll put your simple initialization.
  • Connect::OnConnection. This method is called when the IDE actually loads the add-in you've created. This is where you'll initialize your add-in, inform the IDE of the commands you have to offer, assign keyboard bindings, and so on.
  • Connect::QueryStatus. This method is called by the IDE to determine whether or not a command is appropriate in the current state.
  • Connect::Exec. This method is called by the IDE to actually execute commands.

      Now that all the basics are in place, let's look at the development of a simple example of an add-in.

A Simple Editing Function: InsertDate

      I created a simple command, InsertDate, that does exactly that: it inserts the current date at the current cursor location or in place of the currently selected text. There is already a sample macro included with Visual Studio .NET that does this, so you'll be able to see this same functionality implemented as both a macro and as an add-in. It is so simple that you only need to modify one of the functions listed earlier, the Exec function.
      The basic add-in I created, according to the steps in the previous section, implements a command called TextUtil, or more correctly, TextUtil.Connect.TextUtil. In the Exec function, I replaced the wizard-generated line

  handled = true;
  

with

  handled = InsertDate();
  

and added the following InsertDate method, which can be added anywhere within the Connect object:

  // InsertDate
  
// Insert the current date in my favorite format.
//
bool InsertDate()
{
if (null != applicationObject.ActiveDocument)
((TextSelection)applicationObject.ActiveDocument.Selection).Text
= DateTime.Now.ToString("dd-MMM-yyyy");
return true;
}

      The InsertDate function does the work, using the System.DateTime and System.String objects. See the sidebar "Strings in C#" for a quick introduction to System.String. At this point I commented out the three lines of code in OnConnection which reference CommandBars. These lines create the Tools menu items, which I'll return to later.
      Except for the name, the add-in is complete. To try it out, press F5 to run it. A new instance of the IDE runs and in that instance, the add-in will be listed in the Tools | Add-in Manager dialog. To load it, check the far left checkbox in the Tools | Add-in Manager to select it and press OK. Now you can open any text document, enter "TextUtil.Command.TextUtil" in the Visual Studio .NET command window, and the current date will be inserted into the text document. In fact, auto-complete will fill out that command before you've finished entering it.
      When you press F5, you're running both a new instance of the IDE and your add-in in the debugger. I've found it both informative and useful when running in the debugger to break on all exceptions, at least initially. In the normal course of events there are few, if any, exceptions. When there are exceptions, they can be easily disabled. More importantly, failures are much easier to detect if you get a chance to examine the exception information as soon as possible.

Changing the Name of the Command

      The wizard assigned the command a default name (TextUtil). Since that's not particularly descriptive and since I'll be adding more commands in a moment, I am going to change the name. Also, because I encountered some confusion when I inadvertently changed the case of a command name, I'll make the name comparison case-insensitive at the same time. A case-insensitive compare is currently all that's needed to avoid that confusion.
      The first change that I'll make is to the AddNamedCommand call in OnConnection:

  Command command = commands.AddNamedCommand(addInInstance, 
  
"InsertDate",
"Insert current date ",
"Inserts the current date",
true, 59, ref contextGUIDS,
(int)vsCommandStatus.vsCommandStatusSupported
+(int)vsCommandStatus.vsCommandStatusEnabled);

In QueryStatus, make the following change:

  if(commandName.ToLower() == "textutil.connect.insertdate")
  

Next, in Exec I made a similar change:

  if(commandName.ToLower() == "textutil.connect.insertdate")
  

      The wizard preloaded the registry for the initial run, but the name change affects the registry information. This means that in addition to rebuilding the add-in, I need to rebuild the setup project and reinstall the add-in before these changes will take effect. After rebuilding the add-in, I just right-clicked on the setup project and selected Build. Then I right-clicked on it once again, and selected Install. (As I indicated in the comments that are included in the wizard-generated code, there are other options besides building the setup project; however, you'll need it later, so I recommend you go that route from the start.)

Figure 3 Running the Macro
Figure 3 Running the Macro

      Now the command TextUtil.Connect.InsertDate will do what I want it to, as shown in Figure 3. But how does it do it? Let's look next at what's going on behind the scenes for what I've created so far.

What Makes the Add-in Work?

      The code for InsertDate that I showed you in the previous section is fairly simple, but it does make use of the automation objects that at this point probably seem somewhat magical.
      I'll start by saying that the Object Browser is your friend because it makes it easy to learn about the objects. In Visual Studio .NET, you can simply right-click on any of the objects discussed, and select Go To Definition for a quick list of members. In the resulting window, shown in Figure 4, you can select any of the members and you'll get a prototype with clickable types that will let you drill down even further. Or you can select any of the members and press F1 to go to the associated online help topic.

Figure 4 Object Browser
Figure 4 Object Browser

      The applicationObject represents the application in which the add-in is hosted, in this case the Visual Studio .NET IDE. It's passed to the add-in in the OnConnection method. You'll find "DTE" in the online help, even though applicationObject's type used in the wizard generated code is "_DTE". You'll see it has a number of interesting members that apply at the application level.
      One of those members is the ActiveDocument property, which represents the document that currently has focus. That is the document you'll want InsertDate to operate on. Since window focus and document focus are related but not identical, a good rule of thumb is that the document with focus is the one that would be saved by a File | Save, regardless of which window has the focus.
      The ActiveDocument.Selection property returns an object that represents the current selection in the document. Because it's a generic object in C#, I cast it to a TextSelection. ActiveDocument is generic because the document is not necessarily text-based, such as a forms designer document. As a result, the Selection property is also generic, and I cast it to the TextSelection that the code actually operates on.
      TextSelection represents a view on a file that behaves in accordance with the Tools | Options settings and user state. It exposes the various properties and methods you might want to use to modify the file, and it can affect the user's view, the current selection, and the insertion point. If you've ever recorded a macro, you'll see that TextSelection was used to capture your activity.
      InsertDate simply sets the Text property to be the string with the current date. This behaves as if the text assigned was being typed. This means that any existing selection is replaced, or if there was no selection, then the text is placed at the insertion point, paying attention to the current insert/overtype mode.
      A different approach is to use the Insert method. This method allows you to control text placement and always represents a single undoable action. To use this method, replace the Text property assignment with the following:

  ((TextSelection)applicationObject.ActiveDocument.Selection).Insert
  
(DateTime.Now.ToString("dd-MMM-yyyy"),
(int)EnvDTE.vsInsertFlags.vsInsertFlagsCollapseToEnd
);

      The vsInsertFlags parameter indicates exactly how the text is to be inserted and where the insertion point should be afterwards. Documentation for vsInsertFlags was inadvertently omitted from the online help, so I have included it in Figure 5. In this case, I want InsertDate to operate like a typed character so that the user can just keep typing afterwards. That means vsInsertFlagsCollapseToEnd is the choice.
      Now that you understand how the add-in works, let's make it easier to use by assigning a key combination to the command and adding it to a menu.

Hooking Command Up to the Keyboard and Menus

      Using the command window in Visual Studio .NET to execute the function is handy, but not really practical for an editing function you plan to use often. The good news is that the add-in can provide a default keyboard binding.
      Since you'll be adding more functions later, let's take this opportunity to clean up the code a little by creating a support function that wraps the AddNamedCommand call and hooks up the keyboard binding. This function, AddCommand (see Figure 6), takes as parameters only those settings, such as the command name, tooltip text, and so on that are actually different for each command added, and provides appropriate defaults for everything else. The code in OnConnect now changes to the following, and AddCommand does all the heavy lifting.

  AddCommand ("InsertDate","Insert Date","Inserts the current date",
  
"Global::alt+l,alt+d", "Edit");

      If a nonempty string is passed as the fourth argument to AddCommand, it makes the keyboard assignment by first attempting to retrieve the array of current key bindings from the newly added command. If there already are key bindings, then it assumes that the user has further customized their settings and won't attempt to replace them. Otherwise it creates the binding array consisting of a single binding, and then assigns that back to the command object.
      As I found from experience, the key binding assignment throws an exception unless you've created a non-default keyboard mapping scheme. The default scheme cannot be modified, and of course that's exactly what this code is trying to do. In Tools | Options | Environment | Keyboard, just click the Save As button and give the current keyboard mapping scheme a name of your own, and you'll be able to modify at will.
      If you run the add-in now, you'll see that the two-keystroke sequence Alt+L, Alt+D will now insert the current date at the current cursor location in the current text file.
      Attaching the add-in to the menu tree is similarly easy. I've added a fifth parameter to the AddCommand method, and if it is nonempty, the following code is executed:

  CommandBar commandBar = 
  
(CommandBar)applicationObject.CommandBars[szMenuToAddTo];
cmd.AddControl(commandBar, commandBar.Controls.Count);

This associates the CommandBar object with the named menu and then adds the Insert Date command to the end of it.
      I now have a working add-in, but it certainly seems like a lot of effort to go to just to insert the current date. Next I'll show you a few more of the objects that allow more complex interactions with the text and then use those objects to create something more useful: the text rewrapping function called Justify.

Objects Used to Access Text

      So far I've mentioned the DTE, ActiveDocument and TextSelection objects. DTE, of course, represents the application in which your add-in is operating. There can be only one, and it's provided when the OnConnection method is called. Document objects represent documents currently open. The ActiveDocument property returns such an object, the currently active document, whatever it might be. You can access all the open documents via the DTE.Documents collection.
      A TextSelection object represents text in an open document that has the selection. Not only does it contain the current state of a selection, but it also provides the mechanisms for modifying both the contents and location of the selection. Operating on a text selection is equivalent to a user editing a document; current user preferences, such as insert/overtype, or virtual spaces, are respected.
      There are some interesting objects that I haven't mentioned yet, such as Window, TextWindow, TextPane, EditPoint, TextPoint, and TextDocument. Next, I'll briefly explain their relationships and how they can be used when writing text manipulation functions.
      Window objects each represent a window in the IDE. The TextWindow object represents a window that's open in the application and which contains an editable text file. Much like the DTE.Documents collection and ActiveDocument, TextWindow objects can be retrieved from DTE.Windows and ActiveWindow. A document can appear in more than one window. In fact, there is a Windows collection in each Document object, and each Window object that is a TextWindow contains a Document object. TextWindows also contain TextPane objects, which control how multiple panes within the window are displayed.
      So far the discussions have centered on the TextSelection object as the primary editing mechanism. As I've said, it's affected by user state and user-selected options, so operations are not necessarily deterministic. For example, a function that inserts text for one person may overwrite for another.
      Enter the EditPoint and TextPoint objects. These objects, which you can retrieve from a TextDocument object, represent just the text. They don't represent a logical view on the text, but instead the text buffer itself. Operations are independent of user state, so they do the same thing every time. TextPoints are tied to the text they point to, so as text is inserted, deleted, or changed prior to a TextPoint, the TextPoint moves with it. EditPoints can almost be thought of as a byte offset into the text file. If something changes before the EditPoint, then the text at the EditPoint may have moved or changed. Another way to look at it is that the TextSelection object operates on a view of the text, whereas the EditPoint and TextPoint objects operate directly on the unfiltered text itself.
      That's enough about abstract objects for now. Let's use a few to create a new text editing function.

A More Complex Function: Justify

      Justify is an editing function that performs word wrapping on a paragraph. That means simply moving text from line to line to fill out, but not exceed, a particular line width. It's the kind of function word processors do all the time, but text editors do not. I find it particularly useful when writing comments in my code, or when using my text editor to write e-mail. In fact, I won't move to a text editor unless it has, or I can write, a Justify function for it.
      Since paragraph is an ambiguous term for a text editor, I define it as a collection of lines of text up to, but not including, the first blank line encountered. I'll add some functionality later that will allow a little more flexibility, but I'll start with this.
      The first decision I have to make is whether to operate on the view of the text (via a TextSelection) or on the raw text (via EditPoint and TextPoint). I'm going to choose the raw text, because it doesn't really make sense for paragraph justification to be affected by things like insert/overtype and the like. It's more important that it do exactly the same thing in all cases. Later I will use the text selection in effect when the function is invoked and add some user customizable properties.
      Hooking up a new function is by now fairly straightforward. I've added a call to the AddCommand function in OnConnection, added a check for the Justify function in both QueryStatus and Exec, added a call to the new JustifySimple function in Exec and, of course, added the JustifySimple function itself, shown in Figure 7.
      As its name implies, this version of the function is very simple. Using the current selection, it creates an edit point, then walks forward in the text looking for a blank line. Once that's found, the code puts all the text in the range into a local variable. Note the use of the pointCur.Delete function to delete all the text in a range between two editpoints. Once the old text is deleted, the rest of the function becomes simple string manipulation as it breaks the text into lines of less than iWidth in length. The last editing function is a call to pointCur.Insert to insert all of the newly formatted lines of text into the document.

Improvements to the Simple Justify Function

      Now I'll walk you through some incremental improvements to JustifySimple to make it more useful.
      JustifySimple pays no attention to any selection that may have been in place when it was invoked. My first change is to check for a selection which spans lines, and if there is one, only justify the lines included in the selection. The major change is the use of TopPoint rather than TextSelection.AnchorPoint as the beginning of the range. ActivePoint is where the caret is, and AnchorPoint is the opposite end of selection. TopPoint is always the upper-left point of the selection. Thus, if the TopPoint and BottomPoint are on the same line, the code operates as before, searching for the subsequent blank line to define the range. If they are different, however, the code can use their line properties to define the range explicitly, and justify the text only on those lines. The code that locates the blank line in JustifySimple is replaced with this:

  txt = (TextSelection)applicationObject.ActiveDocument.Selection;
  
pointCur = txt.TopPoint.CreateEditPoint();
lineStart = pointCur.Line;
if (txt.BottomPoint.Line == pointCur.Line)
{

}
else
{
lineEnd = txt.BottomPoint.Line+1;
}

      Right now, JustifySimple is rather inflexible about its results; the width is fixed at iWidth, which has been hardcoded. Next, I'll let the start column and end column of a selection define the area in which the text will be justified. The rules for the amount of text processed won't change; if the selection is on a single line, then the code will still grab text until the next blank line or the end of the document. If the selection spans multiple lines, then it will justify only the text on those lines.
      The code in Figure 8 is placed immediately after the code that locates the blank line. You'll note that I've restricted this functionality to happen only when a box selection is made in order to avoid unexpected results in Justify's other modes of operation. Note also that the code checks the top and bottom VirtualDisplayColumn. Virtual Spaces, when enabled, allow you to place your cursor beyond the end of a line. The spaces between the end of the line and your cursor are virtual in that they don't really exist in your file. If you're going to use the horizontal cursor placement to determine the right margin of your text for Justify, then enabling Virtual Spaces is almost a necessity.
      At some point prior to the reformatting loop, iWidth is calculated as the difference between two columns

  iWidth = iColEnd - iColStart;
  

and immediately prior to the pointCur.Insert call, the resulting line is padded from the left to put it in the correct left-hand column:

  szLineTemp = szLineTemp.PadLeft 
  
(iColStart - 1 + szLineTemp.Length,'

Controlling Undo

      If you've been stepping through and trying out the code each step of the way, chances are you tried to use undo to restore your text. As you'll have seen, each editing operation represents a separate, undoable step. That's not really how an editing function should operate—Justify should certainly be an atomic operation that you can undo in a single step.
      The UndoContext in the DTE is exactly the tool you need. By opening the context, all actions taken are bundled into a single undoable operation until the context is closed.
      I've added the following code to the beginning of the Justify function, as shown here:

  bool        fUndoWasOpen;    // undo context state on entry
  

fUndoWasOpen = applicationObject.UndoContext.IsOpen;
if (!fUndoWasOpen)
applicationObject.UndoContext.Open("TextUtil.Justify", false);

It checks to see if an undo context is already open, in which case I won't open a new one. This allows Justify to become part of a larger undo construct, if one is created prior to calling it, without interfering. I then wrap the following lines in the finally section of a try...finally block; the UndoContext.Close at the end of the function only happens if UndoWasOpen is false:

  if (!fUndoWasOpen)
  
applicationObject.UndoContext.Close();

Finally, the function returns true. It is important to place all the code in Justify after the UndoContext.Open in a try block and the UndoContext.Close in the finally block. You want to be absolutely sure that the Open is matched with a Close, regardless of any exceptions thrown along the way. An UndoContext that is left open can eat up all subsequent editing operations, even after the add-in function has terminated. If that happens, the undo functionality in the IDE can stop working or can undo many more steps than the user would naturally expect.

More Bells and Whistles

      The last functional change I'll make adds the ability to detect and preserve a string (such as the comment-starting "//") that would be removed from the beginning of each line of prejustified text, and appended to the beginning of each line after the justification has occurred. To do this I'll give the Justify function a parameter representing the string to look for. In this case, I'll pass in the double slashes, which I want to be preserved.
      The following code, inserted into Justify, turns off the string prepending operation if the text does not begin with the string:

  if ("" != szPrepend)
  
if (!rgszLines[0].StartsWith (szPrepend))
szPrepend = "";

In the code which collects the text into a single line, I need to check for and remove the prepending string on each line where it's encountered, as shown here:

  if (0 < szPrepend.Length)
  
if (szLineTemp.StartsWith (szPrepend))
szLineTemp = szLineTemp.Substring (szPrepend.Length);

And finally, I need to output the formatted line with the prepending string, as you can see in the following code:

  szLineTemp = szPrepend + szLineTemp.Trim() + "\r\n";
  

      Now, to reformat an existing comment block, all I need to do is locate the position at the first line and execute the Justify function. To create a new comment block, make sure you add "//" to the beginning, and Justify fills in the rest.
      Lastly, I've added a second command, called JustifyMail, which is similar to Justify, except that I pass it the string I want prepended to lines in e-mail (">"), and of course I'll give it a different key binding. In OnConnection I've added:

  AddCommand ("JustifyMail", "&Justify Mail", "Justify Text in Mail",
  
"Global::alt+l,alt+h", "Edit");

I've also added the appropriate checks in Exec and QueryStatus. At this point, the Justify routine itself is almost finished (see Figure 9). There are a few hardcoded values that the user should be allowed to define. Sounds like a job for an Options page.

Adding a Page to the Options Dialog

      It turns out that adding a page to the Options dialog (available in Tools | Options) is easy once the details are clear. There are a few things you need to know. A tools options page is just an ActiveX® control. To use a C#-generated form as an Options page, you'll need a shim control, which is nothing more than an ActiveX control that hosts your form. (I'll provide one.) You know that an Options page and an add-in communicate only via the registry. The setup for an add-in needs to add a registry key to cause the Options page to load. It's also helpful to know that the add-in and its Options page are really two separate pieces of code.
      That last item was by far the hardest for me to understand. Being used to monolithic projects where setting options was part of the application, I expected that creating options for my add-in would work similarly. I was quite wrong. Aside from sharing setup information and registry keys, the two could well be completely separate projects.
      First, I'll finish off the code for Justify so that I can move on to the options page. The only change that needs to be made to Justify is that it has to read the default for these new options from the registry. I've selected a total of three items that the user will be able to configure in the Options page: the default paragraph width (iWidth, in the code), the prepending string for justifying comments in code, and the prepending string for justifying e-mail.
      The registry should be reread prior to each function beginning its work in order to pick up any changes that the user might have made to options. To do this I've created a function, LoadRegistry, and placed calls to it immediately above the calls to the Justify function in Exec. I've created object variables to hold the resulting values.
      After adding the using Microsoft.Win32 directive to make the proper functions available, LoadRegistry itself is very simple:

  private void LoadRegistry()
  
{
RegistryKey key;
key = Registry.LocalMachine.OpenSubKey ("Software\\Microsoft\\
VisualStudio\\7.0\\Addins\\TextUtil.Connect\\Options");
iWidth = (int)key.GetValue("iwidth", 72);
txtCodePrefix = (string)key.GetValue ("codeprefix", "// ");
txtMailPrefix = (string)key.GetValue ("mailprefix", "| ");
}

Note that my original defaults remain, but only as parameters to the GetValue calls in case the registry keys don't exist.
      That's it. Justify is finished. Next up is the Options page itself.
      To create the form, I right-clicked on TextUtil in the Solution Explorer and selected Add and then Add User Control. I've called mine optionscontrol.cs. On that form, I placed three text controls named txtRightColumn, txtCodePrefix, and txtMailPrefix. I also added appropriate labels and set the tab order, but you can essentially make it look however you want. My version of the control is shown in Figure 10.

Figure 10 My User Control
Figure 10 My User Control

      To make a few more changes, I right-clicked on optionscontrol.cs in the Solution Explorer and selected View Code. First, I added

  using Microsoft.Win32;
  

to get the registry access support needed. The class also needs to inherit from IDTToolsOptionsPage in order to provide the appropriate support functions:

  public class OptionsControl : System.Windows.Forms.UserControl, 
  
IDTToolsOptionsPage

      The GetProperties, OnCancel, and OnHelp methods in optionscontrol.cs are required, but can be empty. In OnAfterCreated (see Figure 11), the code loads the current values of options from the registry and places them into the text controls. This method is called each time the options page is selected. In OnOK (also in Figure 11), the code takes the current values that are in the text controls and puts them into the system registry.
      As I mentioned, the user control I've just created won't plug directly into the Tools.Options pages. For that you need a shim, which is an ActiveX control that the options control can be hosted in. I won't go into detail on the shim itself. A shim, VSToolsOptionsUserControl, is part of the downloadable code accompanying this article. All you need to do is add that shim as a subdirectory in the TextUtil project and add the separate C++ project to the solution.
      There is one change you'll need to make to the shim in order to use it. In VSToolsOptionsUserCtl.cpp, in the OnShowWindow method, it references a GUID. This is how the shim locates your tools.options control. Change it to be the GUID used in the GuidAttribute statement of optionscontrol.cs. Right-click the VSToolsOptionsUserControl project, build, and you have a shim.
      Finally, the appropriate modifications need to be made to setup to get the Tools.Option control installed. Select the TextUtilSetup project in the Solution Explorer, then click on the registry editor icon. There should already be several values under the key HKEY_LOCAL_MACHINE\Software\Microsoft\VisualStudio\7.0\Addins\TextUtil.Connect. Under that key, add a subkey called Options, and under that add a subkey called TextUtil. Under that subkey add one called Settings. In that key, create a single string value named Control with a value of "VSToolsOptionsUserControlHost.VSToolsOp," which is the ProgID of the shim control. See Figure 12 for the location of this key in the Visual Studio registry editor. The keys you created under Options correspond to the location in the Options page that your control will appear, and the Control setting is used by the IDE to locate the ActiveX control to place there.
      I've left optionscontrol.cs as part of my TextUtil project so it will be installed when the project its set up, but the shim control also needs to be laid down at setup time. To do so, right-click on the TextUtilSetup project in the Solution Explorer, select Add, then Project Output. In the dropdown, select VSToolsOptionsUserControlHost, and in the list below it, select Primary Output.

Figure 13 TextUtil Options Dialog
Figure 13 TextUtil Options Dialog

      Now rebuild setup and install it. Press F5 and you should now have not only your add-in available in Visual Studio .NET, but also your Options page, as shown in Figure 13. If you're still breaking on every exception, don't panic when the IDE throws an exception saying that some component of textutil.dll isn't found when you open up the Options page. That's actually part of the IDE's mechanism for locating the DLL containing the page. Let the IDE continue (or disable breaking on that particular exception) and all should be well.

Conclusion

      I've only touched on text editing functionality here, but you can imagine the possibilities. Clearly, you can build up your own library of editing functionality that meets your specific needs which can then be easily taken to any installation of Visual Studio .NET.
      Useful add-ins range from functions that allow you to examine and operate on code at the symbolic level, to manipulating the build environment, and even to plugging in entire applications. Many automation examples are available on the MSDN Web site, and I encourage you to check them out.

For related articles see:
Visual Studio .NET automation samples

 

For background information see:
"The Spectrum of Visual Studio .NET Automation" in Visual Studio .NET.

 

Leo A. Notenboom is a long-time developer and former development manager at Microsoft, most recently with the Visual Studio .NET team. Leo can be reached at leon@exmsft.com.