|Extending the Visual Studio .NET IDE |
|Download the code for this article: Bugslayer0201.exe (115KB)|
Browse the code for this article at Code Center: SuperSaver Utility
| ven though Microsoft works very hard to think of everything that developers want in an IDE, they don't always succeed. That's not a slight against the hard-working IDE developers, but when people are as opinionated and strong-willed as developers tend to be, you can't please all of them all the time. While developers might be opinionated, they're certainly not afraid to add any missing functionality themselves. Since Visual C++® 1.0, the extensibility of the IDE has been improving gradually, but in my opinion has never been strong enough. Because I have worked on tools to integrate with the IDE, I have been inundated with e-mail over the years from developers with ideas about how to fix limitations or add completely new functionality to it. When developers realized that integrating with previous IDEs required heroic feats of reverse engineering and hacking, it thwarted their dreams of IDE improvements.|
Fortunately, Microsoft has heard the pleas of developers, and designed Visual Studio® .NET to offer an extensibility model that should meet the needs of nearly anyone who needs to create an add-in. At first, when I took a cursory glance at the add-in extensibility model, I didn't think much had changed. That was partly because some of the documentation in the Beta 2 release is missing and partly because Beta 2 did not ship with any add-in samples. However, when I took a hard look at everything, I was very happily surprised to see how much a paragon of extensibility the new Visual Studio .NET IDE has become.
In this month's Bugslayer I will cover some of the capabilities of the new extensibility model. To get started, I will briefly cover some of the features that never existed before in the IDE. One of the biggest stumbling blocks with developing add-ins is the actual add-in Wizard itself. In order to spare you the pain I went through, I'll show you how to fix the Add-in Wizard-generated code so it works properly. Finally, since MSDN® Magazine always includes code, I'll discuss the SuperSaver add-in I wrote for this month's column.
The first thing SuperSaver does is add a save command that strips off trailing white space from source files, a feature inexplicably missing from Visual Studio .NET. The second command is an automatic save to save everything in the IDE at predefined intervals so that changes aren't lost.
Extensibility Overview Figure 1 shows a partial list of the various objects accessible in the IDE. While previous instances of the IDE allowed access to the Documents and Debugger objects, the really interesting improved Window and new Code Model objects are where the action is. For the first time, everyone can add their own user interface windows to the IDE! The Code Model objects are what you can use to find, change, and update the actual code without having to rely on parsing. The great news is that the Code Model is updated in real time. Many developers have told me about their ideas for tools that work on source code, but have been stymied because of the daunting prospects of writing a parser for a single language, let alone the multiple parsers necessary in today's projects.
There have also been great improvements in the events that tell you when various actions occur in the IDE. Figure 2 lists the various event objects and what they mean.
The extensibility model is very good, but be aware that there are actually two of them. One extensibility model is for managed projects and the other is for Visual C++. While I can understand why the Visual C++ unmanaged code model is different, the fact that there are two completely separate project models is quite annoying to me. The debugger object is also different between managed and unmanaged projects. It certainly would have made life much easier if Microsoft had derived all languages and project types from a common ancestry.
The other thing to keep in mind is that the object model(s) documentation is certainly not complete at this point because it's still in beta. The good news is that there is enough documentation to get you started.
There's far more to the extensibility model than I can cover in this month's column. However, I can give you some very strong hints to make figuring out the model much easier. Before you ever contemplate writing an add-in, take some time and write lots of macros first. The IDE fully supports Visual Studio for Applications (VSA) and those macros support all objects as well as the events. In fact, I prototyped the SuperSaver add-in core logic as macros before I even considered writing the add-in.
Add-in Overview While you can do a tremendous amount of IDE extension with VSA macros, there are good reasons for using add-ins instead. Where macros are distributed in source form, add-ins are compiled so they execute faster and can protect your intellectual property. Additionally, add-ins can support user interface elements such as modal and modaless dialogs, tool windows, and dialogs in the Options dialog. In case you aren't familiar with them, tool windows are windows in the IDE such as the Solution Explorer or Command Window. You can turn any tool window into an MDI window by undocking it (right-clicking in the title bar, and unchecking "Dockable").
The big news with add-ins is that they can be written in any language that supports COM. If you are familiar with C# or Visual Basic® .NET, you no longer have to crawl through the weird ATL macros just to get a simple add-in working. While the ease of developing in managed languages boosts productivity, I do have to mention that managed languages cannot host tool windows directly. Since add-ins are really ActiveX® controls, in order to have managed code as a tool window you'll need to have a shim ActiveX control that hosts the common language runtime (CLR). The good news is that in the final release of Visual Studio .NET, Microsoft should have a sample hosting control to show you exactly what you need to do to accomplish this.
Those of you who have written Office XP add-ins should feel right at home with the IDE extensibility since the core interface for add-ins is IDTExtensibility2. With all the documentation on writing COM add-ins for Office XP, it's fairly easy to figure out how to proceed even when the background explanation is missing. The main job of IDTExtensibility2 is to take care of communicating with the IDE. The most interesting method, IDTExtensibility2.OnConnection, is where the IDE tells your add-in that it's being loaded. The second parameter to OnConnection, ConnectionMode, is important since it's the only place you are notified of the ext_ConnectMode.ext_cm_UISetup. The IDE will only tell your add-in exactly one time the first time it's loaded on the machine. I'll discuss how to reset the IDE so you can get the initial startup message when you need it.
The other important interface is the IDTCommandTarget, which allows you to have your own named commands. Obviously, having an add-in without commands to offer the user is fairly useless, so all your add-ins will implement a class to handle the two methods in this interface. The first method is the IDTCommandTarget.QueryStatus, which is called after the user tries to execute your command or when the IDE needs to draw a menu item containing the command. Based on the IDE state (design mode, debugging, and so on) you'll tell the IDE if the command is enabled. Additionally, you'll also be asked to provide the text for the commands you add. This allows you to return appropriate text for localized editions of the IDE. The second method you'll implement is IDTCommandTarget.Exec which, as the name implies, is where you are asked to perform the requested command.
While the IDE add-in samples do not come with the default installations of Visual Studio .NET, if you have a valid beta test ID, you can download the missing samples from the Visual Studio .NET Beta site at http://beta.visualstudio.net. Numerous examples will help clarify how add-ins work and show you what they can do. You can also find an ActiveX control that will host managed code Tool windows so you don't have to do it yourself. Unfortunately, the more involved samples that show many techniques in C++ are very poor programming techniques. Nothing annoys me more than seeing code written with macros that expand out to conditional statements, GOTOs on failures, and assumptions that you are using specific variable names and label names. Sadly, using the Add-in Wizard to create a C++-based add-in generates ugly code using the same poor-practice macros. I certainly hope Microsoft will fix the C++ code generation before release. Be prepared to spend lots of time reading code, but the examples do show how to take advantage of all the extensibility in the IDE.
Slaying the Add-in Wizard Armed with the briefest of add-in overviews, it's time to turn to some specifics. As with most nontrivial projects, there's a wizard in the IDE to get you started. Unfortunately, if you rely on the wizard to take care of everything for you, you won't get very far. Keep in mind that you are dealing with beta code and things should improve after the release. Even if you are reading this after the release ships, you shouldn't skip this section because I'll discuss several salient points about how add-ins are initialized and how you can avoid potential problems. If you are using the beta release, you can slay the wizard with the following steps:
Before you get too far developing your add-in, you will probably want to fix some of the code that the Add-in Wizard generates for you. The first thing you might want to do is to remove the string "Connect" from the ProgID as well as all the commands. It's not really necessary and will just mean more typing for users who call your add-in commands from the Command window.
- Use the Add-in Wizard, located in the Other Projects Extensibility Projects of the New Project dialog to create your add-in. Bear in mind that you can also create that work in the VSA Macros IDE as well.
- In the Configuration Manager dialog, make sure to uncheck "building the setup project." Unfortunately, each time you build your add-in, it causes the dependency in the setup project to be out of date, so the build has to create the 21MB (or bigger) setup project every time. Unless you like watching the hard disk spin or are paid by the hour regardless of productivity, you can skip building the installation until you are closer to being finished with the project.
- Build the Debug build of the add-in. Inexplicably, this is the only way to register the add-in. By building the add-in, you can extract the important registry entries so you can change values or reinitialize settings later.
- If you chose to mark the add-in as usable by all users, the registry key will start at HKLM; otherwise it will start at HLCU. Go to \Software\Microsoft\VisualStudio\7.0\AddIns and save your add-in's key and all values. If you are going to add additional information here, you might need to write your own setup program and remove the Add-in Wizard-created version. If you are building an add-in that works with the VSA IDE, you will need to enter your add-in key name under \Software\Microsoft\VSA\7.0\AddIn. After saving these files, add them to your project.
- To ensure that your add-in is properly registered, add-ins written with managed code will have a file created called ReCreateCommands.REG. Before you do anything else with your add-in, run this file. For add-ins written in Visual C++, the Add-in Wizard does not create this file. The following registry file shows the entry you will need to create:
Substitute your add-in name for <YourAddInName>. If you ever change any of the commands in your add-in, you will need to rerun this file to ensure that your new commands get added to the IDE. Setting these keys tells the IDE to call the one-time initialization for the add-in.
The Add-in Wizard IDTExtensibility2.OnConnection is the second item you will likely want to fix. The default version just blindly tries to add the commands. Of course, the IDE does the right thing and throws an exception when you try to add a command that already exists. The Add-in Wizard code simply catches the error and ends the function. Since it's entirely possible that you'll be adding and changing commands as you're developing your add-in, having the OnConnection method skipping out early won't help much. Figure 3 shows the example code from SuperSaver where I do the initial command setup. In RemoveCommandsAndCommandBars, I manually go through and remove any command.
As you develop your add-in, you might want to get rid of all traces of the add-in so you can start your testing fresh. While the uninstall might take care of it, you should be prepared to do it manually. After hunting around for a while, I found the following steps worked every time.
- In the IDE's Add-in Manager dialog, uncheck all checkboxes next to your add-in name. After clearing the checks, shut down all instances of the IDE.
- Run REGASM or REGSVR32 (depending on the type of code you used to write your add-in) to unregister your objects in the registry.
- If you chose to mark the add-in as usable by all users, the registry key will start at HKLM; otherwise it will start at HLCU. Go to \Software\Microsoft\VisualStudio\7.0\AddIns and remove your add-in's key. If you support running in the VSA IDE, remove your add-in's key from \Software\Microsoft\VSA\7.0\AddIns as well.
SuperSaver Can Save Your Bacon While you are chomping at the bit to start developing add-ins, let me turn your attention to the sample code that accompanies this month's column. As I have been using the IDE, I've seen two features that the IDE developers forgot to add. The first is that the IDE does not strip white space off the end of files when it saves your source, which is what the programming gods say you should do. Since it's trivial to create a regular expression to search for this condition, "[ \t]+$", I figured that all I needed to do was to use one of the regular expression classes to do the work for me. In the end, I found that the IDE TextDocument object has a ReplacePattern method just for this contingency.
Implementing the SuperSaver.StripTrailingWhiteSpaceSave was trivial. I reset my keyboard bindings so that Ctrl+S uses SuperSaver.StripTrailingWhiteSpaceSave. While your add-in can easily do the key binding automatically, I left it as an optional exercise for you. If you do add the key binding, please make sure that you ask the user if it's OK for you to do that.
The second feature needed in the IDE was an automatic save. All programs such as Microsoft® Excel, Word, and third-party programming editors now offer a feature that automatically saves your work at periodic intervals. By starting the SuperSaver add-in, it will take care of this for you every 10 minutes by default. The first time you load SuperSaver, you'll see the dialog shown in Figure 4. You can set the number of minutes between autosaves as well as strip trailing white space.
Figure 4 AutoSave
Implementing the automatic save turned out to be much less work than I expected. I needed some kind of notification when time had elapsed, and that task was screaming out to use System.Timers.Timer. However, I was concerned when I noticed that the timer processing method executes in another thread. I did not know if the IDE allowed a different thread to access the open documents as well as the project lists. A little experimenting showed that after several weeks of using it this way there didn't seem to be any problems.
The final point I want to mention about SuperSaver is how the IDE finds resources such as bitmaps, string tables, and icons. Since add-ins are really COM objects, just placing resources in your project as managed code resources does not work. I set up SuperSaver to use a satellite resources DLL. After you download the SuperSaver project (available from the link at the top of this article), you'll see it as a resource-only DLL called SuperSaverResources. Look at the registry entries in the SuperSaver.AddIn.REG file to see how to specify the string values to pull out of the satellite resource DLL. For example, the AboutBoxDetails entry, which appears in the IDE About box to show information about the add-in, is "#1001" in my SuperSaverAddIn.REG file. The number sign (#) followed by a number tells the IDE to pull the resources out of the satellite resource DLL.
You can also look in the IDTExtensibility2.OnConnection to see the calls to Commands.AddNamedCommand where I pass the IDs of the specific satellite resource DLL bitmaps to associate with the commands. In order to get the satellite resource DLL properly hooked up so the IDE can find it, you'll need to change the very last entry in SuperSaverAddIn.REG file, SatelliteDllPath. That entry must point to the complete path, except for the local language ID. For example, if you are running the English language IDE, the complete path to the satellite resource DLL would be D:\SuperSaver\resources\1033. Your SatelliteDllPath entry in SuperSaver.AddIn.REG would point to D:\\SuperSaver\\resources\\. The registry editor needs the double slashes to process the text properly.
Wrap-up The new IDE extensibility in Visual Studio .NET should allow you to make the IDE do whatever you want. I can't wait to see what you come up with. If you want to extend SuperSaver, please do. Some ideas that I have considered are adding an option to save a copy of the file each hour of editing so that you can roll back the day's changes.
Tips Yes, that is Jack Frost nipping at your nose to remind you to send your tips to firstname.lastname@example.org.
Tip 49 Information about all active add-ins is shown in the Visual Studio .NET About box, as shown in Figure 5. The Add-in Wizard gives you a default icon to display. What's interesting is that the icon is specified as hex data in your add-in's registry key. To change the icon, you'll need to use the GenerateIconData.exe program included with the sample download to get the hex bytes corresponding to your icon.
Figure 5 Installed Add-ins
Tip 50 If you are having trouble with Visual Studio .NET crashing because of a bad add-in, you can start the IDE in safe mode by specifying the /safemode switch on the command line. Only the default environment and services are loaded in safe mode.
Send questions and comments for John to email@example.com.
| John Robbins is a cofounder of Wintellect, a software consulting, education, and development firm that specializes in programming for .NET and Windows. He is the author of Debugging Applications (Microsoft Press, 2000). You can contact John at http://www.wintellect.com.|