The SuperSaver Add-In


Now that you have some idea of the issues associated with add-ins, I thought it best to discuss some real-world add-ins because they offer the best way to learn. The first add-in I created was SuperSaver, which originally appeared in a Bugslayer column I wrote in MSDN Magazine. However, the add-in version in this book is completely and radically different from the original and is an excellent example of the trials and tribulations associated with writing add-ins.

SuperSaver's job in life is to solve two problems. The first is to add a background automatic save to the Visual Studio .NET IDE so that you don't lose any changed documents. The second is to trim white space off the ends of lines when saving files. With C++ and C# files, the Visual Studio .NET IDE contradicts the computer gods and leaves the white space on the ends of lines. In order to make everything right with the world, I simply had to fix that.

Solving the background file saving was almost too easy. I set up a background timer, and whenever the timer triggers, I execute the command associated with the File Save All menu option, File.SaveAll. The problem was implementing the white space trimming.

As I played around with saving the active item, I noticed that the command that actually executed was File.SaveSelectedItems. Interestingly, instead of saving just the current document being edited, that command also saved the project as well as any items that were selected in Solution Explorer. (You can select multiple items with a Ctrl+left mouse click.) Since I wanted my command to be a drop-in replacement for File.SaveSelectedItems, I needed to mimic that behavior. My initial design was the following algorithm:

get the DTE.SelectedItems collection foreach item in the selected items     if it's a document and not saved         get the text document         call ReplacePattern on the text document     end if     call Save on the item next

An algorithm that seems so simple certainly caused all sorts of problems for me. In all, I ended up writing seven completely unique versions of SuperSaver trying to work around issues. The first problem I ran into was related to the projects themselves. Not all project types support the Save method. The setup projects are a good example. This meant there was no guarantee I could save all the projects. I tried to special-case the calls to the Save method, but there was no way I could properly handle all the future project types that might not support the save.

The second issue I stumbled into was a bug in the TextDocument object. I was calling the ReplacePattern method with "[ \t]+$" (without the quotes) as the regular expression search pattern and an empty string as the replacement text. Life was good, but that expression also will clear any blank lines that are simply indented white space. So if I saved the active file with the cursor indented 20 characters using tabs or spaces, the save would remove the automatic indenting and I'd end up having to type it all back in. Though I like my tab key as much as the next guy, I thought it would be better to fix SuperSaver to trim the white space only on the lines that actually contained text.

After many hours of messing around with regular expressions, I found the ultimate search expression, "{[^ \t]+}{[ \t]#$}", and replacement expression, "\1". Subexpression replacement like this is one of the coolest things about regular expressions. In case you're not a regular expression maven, the search expression says to match any expression in the first group (delineated by the first set of braces) that contains any characters other than a space or a tab. This ensures that the match will occur only on lines that have characters in them. The second group (delineated by the second set of braces) says to match one or more space or tab characters at the end of a line. These two groupings will match only when there is white space at the end of the line. The replacement string "\1" tells the regular expression parser to replace the text with that matching the first grouping. In this case, the matching text is the characters at the end of the string without any white space. Thus, the white space is removed from the string.

I changed the parameter for TextDocument.ReplacePattern to my new search expression and let a save rip. The result was that any lines ending with extra spaces now ended with the ending text plus a "\1" that replaced the extra spaces! This was an interesting result, but the idea of actually corrupting the code probably would not make SuperSaver that useful. Come to find out, TextDocument.ReplacePattern has a bug in it that prevents subexpression substitution from working.

If I couldn't get regular expression subexpression substitution to work, SuperSaver wasn't going to be that useful. While playing around with different workaround possibilities, I recorded a find-and-replace macro that used a global Find object and worked with subexpression substitution. Although not as clean as the ReplacePattern method, it was at least the start of a workaround.

The first thing I realized about using the Find object was that because it's a global object, any changes I make to it while doing my regular expression subexpression substitution inside SuperSaver modifies the current values the user leaves in the Find dialog box. I created the SafeFindObject to wrap the Find object so that I could save and restore all the values, preventing the user from losing any settings.

An additional issue I found when using the global Find object was that when a Windows Forms designer was the active document, I could get the TextDocument object for the document. However, the Find.Execute method, which does the actual search and replace, caused an exception. In SafeFindObject, I decided that the only thing I could do was to eat any exceptions thrown out when calling Find.Execute.

Everything was moving along when I decided I really wanted the auto save to strip trailing white space as well. After all the major work of simply trying to get the active document properly stripped and saved, I thought I was home free. Unfortunately, if I told the global Find object to do its magic on all files and any read-only files were open, things were not so good. Therefore, I simply looped through all open files and, if any of them were marked as read-only, I didn't call my SafeFindObject to do the trim white-space save.

Although I thought I had something going with the auto save, I noticed a problem when I had "virgin files," which are files created with File New but not saved. Calling the Save method on those files brought up the Save File As dialog box. I thought that since the dialog box was up, I'd find a blocking call inside the Save method. However, there wasn't a blocking call, so if I left the IDE running, I'd eventually have a bunch of Save File As dialog boxes up and life would become unhappy in the IDE.

At this point, I became a little obsessed with getting auto file saving to work in a way that would strip the white space off the end of lines. You might want to pull up TrimAndSave.CS and move to the TrimAndSave.SaveAll method to follow along with this discussion. Look for the commented out region marked "Original Attempt." I'll discuss why this code does not work in a moment.

Since I had the Find object working on the current file, I thought I could pop each file that needed saving to the foreground and save it quickly. That seemed reasonable until I ran into a whopper of a problem. If I had a Windows Forms document open in both design view and code view, calling the Active method for the document always activated the design view, even when the code view was the active window for that Windows Forms code. That meant that while I was typing along in the code view for a Windows form, an auto save would kick off, and I'd end up staring at the design view. Amazingly, there's no way to activate a code view from a document in the automation model.

My quest was to find the active document caption, i.e., the active window under the tab strip. Although you can call the DTE.ActiveWindow, doing so returns the window that currently has focus in all the IDE windows, not the window in which you're editing or designing. After a lot of poking, I saw that the Window command bar happens to always have the active document caption in the menu option that starts with "&1" (the ampersand indicates the item in the menu is to be underlined). It's really ugly to poke through a menu to get the actual active document caption, but there was no way to get it in the automation model. Armed with the active document caption string as well as the value returned by DTE.ActiveWindow, I could finally consider how to do the saving because I could at least restore the current active document window and the actual focus window.

As I looped through the documents, I needed to do a couple of things before I could save the file. The first was to determine whether the file was a virgin file by looking at the first character in the filename. If the first character was a ~ (tilde), the file was created but never saved. Because I wanted TrimAndSave.SaveAll to behave like a real auto save (see SlickEdit's Visual SlickEdit for the perfect auto save), I had the option of telling TrimAndSave.SaveAll not to save virgin files or read-only files. Doing that would allow me to avoid being inundated with Save File As dialog boxes each time the auto save was triggered. I could specify in SuperSaver's option dialog box that I wanted to skip virgin files and read-only files. If the file was a virgin file or a read-only file, TrimAndSave.SaveAll would skip the current file and loop back for the next one.

After I determined that the document needed saving, it was time to bring the document to the foreground so that the DTE.Find object could work on it. Since I needed to ensure that the text editing window of the file got brought to the foreground, I had to look for a window caption that had the same name as the document. If I found that window, I could finally strip the white space from the lines. If I didn't find a text window, I simply moved on to save the file.

If the file is a virgin file, I do my own Save File As dialog box, which is no big deal. If the file already has a name, I can simply call the Document.Save method. Interestingly, the Document.Save method is a classic example of how not to design your exception handling. If the file is read-only, Document.Save will pop up the dialog box that asks whether you want to overwrite the read-only file or save the file to a new name. If you click Cancel to skip saving the file, Document.Save throws a COMException class whose message is "User pressed escape out of save dialog." Because this is a normal user interaction, it should have been reported through a return value.

After winding through all the documents, I could finally turn to restoring the original active document window as well as the active window itself. After restoring the windows, I could turn to saving the projects and the solution.

With a project that needs saving, the first action is to determine whether the project is read-only. There's no property on a Project object that will tell you whether a file is read-only. Consequently, I have to get the project's file attributes and check them. If the project is read-only and the user doesn't want to be prompted on auto saves, I won't save that project.

If the project needs to be saved, some more fun begins. As I mentioned back in the "Problems with Projects" section, the project object model in Visual Studio .NET isn't completely thought out or well documented. Because the Project object doesn't map well onto a VCProject in particular, I attempt to get the VCProject out of a project by first checking the project's language. If the project is a C++ project, I call Project.Object and cast the return to a VCProject. Armed with the VCProject, I can call VCProject.Save with confidence. If the project isn't a VCProject, I attempt first to call Save, and if calling Save causes an exception, I call SaveAs, passing the full project name in each case. Because Microsoft hasn't fully documented the different types of projects, this is the best I can do to get the project saved.

Once the projects are taken care of, I can finally save the solution, if necessary. Like projects and documents, when the solution is read-only and the user doesn't want to be bothered with Save File As dialog boxes, I don't save the solution.

While I thought I had a working implementation, a little bit of testing quickly disabused me of that notion. As I tested the auto save a little bit with the white space strip option turned on, I thought there was too much flashing going on because of all the text windows being brought to the foreground. I remembered reading that the DTE object supported a SuppressUI property that, if set to true, blocked UI display when code was running. Figuring that SuppressUI would solve the flashing taskbar issues, I set it to true near the beginning of TrimAndSave.SaveAll. Alas, that seemed to have no effect whatsoever; the flashing continued unabated.

While I could have lived with the flashing, the other problem with using the Window.Active method was that it attempted to bring the whole IDE to the foreground, not just activating a particular document window. Additionally, if the IDE was minimized, Window.Active restored the window.

The final problem was that by using the Find object in the background save, which occurs on a different thread because of the timer, seems to mess up its state. Calling SuperSaver.SuperSaverSave, which I assigned to Ctrl+S worked fine. However, after a background save, whenever I used SuperSaver.SuperSaverSave, the Find/Replace message box that pops up after you've used the Find dialog box started appearing. While I loathe requiring you to turn off the Find/Replace message boxes to use SuperSaver, I was willing to consider it. You can turn off the Find/Replace message box by unchecking Show message boxes in the Options dialog box, Environment folder, Documents property page. With the Find/Replace message boxes turned off, I heard the default beep, like you do with the Find dialog box, every time SuperSaver.SuperSaverSave executed.

At this point, I was extremely frustrated, but bound and determined to get something working. Fortunately, my final attempt, while not perfect, got me mostly what I wanted. Since I was stuck using the Find object with the vsFindTarget.vsFindTargetOpenDocuments option, which dictates to search and replace in open documents only, I had to be careful. I could safely strip only white space in the background only if there were no read-only or virgin files in the active documents. While I would have really liked white space stripping on all files when doing a background save, this was the best I could do. To handle the save itself, the only option I had was to call the real File.SaveAll. Because I still wanted the option of not facing Save File As dialogs or overwrite warning message boxes popping up, I will not call File.SaveAll if the user has unchecked Save New And Read-only Files When Auto Saving in the SuperSaver options; there are no read-only files that need saving or virgin files in the active documents.

Of course, even though the above paragraph describes a fairly straight forward algorithm, I would have to run into one more bug. The Document.ReadOnly property, which is supposed to return true if the file is read-only, does not work. I had to manually check the file read-only state with the File.GetAttributes method.

I finally had two commands in my add-in, SuperSaver.SuperSaverSaveAll and SuperSaver.SuperSaverSave, that I thought were working fairly well. I turned my attention to creating a command toolbar for them and ran into the bitmap masking issues that I discussed earlier. After fixing those, I ran into the final problem with SuperSaver.

Since my intention was to write replacement commands for File.SaveSelectedItems and File.SaveAll, I wanted to make sure my toolbar buttons reacted in the same way they do on real toolbars. With lots of experimentation, I noticed that only the File.SaveSelectedItems button greyed out to indicate it was disabled. I tried everything I could think of to get my SuperSaver.SuperSaverSave toolbar button to behave the same way. Since the active state was controlled by what was selected in the current solution and project, I could not find the magic incantation of checks that File.SaveSelectedItems was performing to enable and disable its button.

Just as I was about to give up, it dawned on me that I certainly didn't need to go about it the hard way. All I had to do was retrieve the File.SaveSelectedItems command object and check whether the IsAvailable property was true; if it was, the toolbar button was enabled. Consequently, in my IDTCommandTarget.QueryStatus method, when the File.SaveSelectedItems command is not active, I return vsCommandStatus.vsCommandStatusUnsupported and all is right with my buttons and the world.

SuperSaver was a total pain in the neck to develop, but I'm glad I did it. Not only did it teach me a tremendous amount about the foibles of add-ins and the Visual Studio .NET IDE automation model, but I made the programming gods very happy by killing those spaces at the end of lines. In comments in SuperSaver, I left all the algorithms of what should work so that you can implement the commands again using the fixed versions of the automation problems in future versions of Visual Studio .NET.




Debugging Applications for Microsoft. NET and Microsoft Windows
Debugging Applications for MicrosoftВ® .NET and Microsoft WindowsВ® (Pro-Developer)
ISBN: 0735615365
EAN: 2147483647
Year: 2003
Pages: 177
Authors: John Robbins

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net