Click here to Skip to main content
Click here to Skip to main content

Use Visual Studio Extensibility to Make a Solution Notebook Tool Window

, 29 Jan 2009
Rate this:
Please Sign up or sign in to vote.
How to create VSPackage add-ins for Visual Studio with persistent solution-based storage
NotebookToolbar2.png

Introduction 

Extending the Visual Studio environment is a valuable task.  The IDE has a boatload of features, but there's always going to be room for improvement. And what makes one developer a happy clam, might not work for everyone (or anyone) else.

The inspiration for this tool came to me while working on my latest project. When it comes time to check in my latest changes to the source, I find that I have to spend time thinking of all the changes I made and why I made them. It occurred to me that a note window associated with each solution that persisted even if I close the IDE would be incredibly useful. I could use the window to keep track of my changes in real time.  And besides, it would give me a chance to play around with the SDK.

Background   

Watch this video from the Microsoft Developer Center entitled: How Do I: Add Tool Windows to the Visual Studio IDE?

If you haven't already, download the Visual Studio 2008 SDK 1.1 (as time passes there will probably be a newer version, so make sure you check for updates).

Also, unless you're a Windows Installer wizard, you'll probably want to download VSSDK Assist.

Using the Code 

If all you want is a little note tool window, then run the included MSI file to install the Notebook Toolbar. The toolbar should be available the next time you open a new instance of Visual Studio and open a solution. (Use the same MSI to uninstall it.) I believe it's set to require Visual Studio 2008, and I have no idea whether it will work in earlier versions.

Enjoy!

Source Walk-through

I highly recommend you watch at least the first 5 or so minutes of the above video.  It's incredibly informative and much of the code walk-through is premised on the techniques it describes.

Starting the Package

Once you've downloaded and installed the Visual Studio SDK, you'll be able to create a project with the Integration Package Wizard. The Wizard generates a skeleton for our ToolWindow and cuts out a lot of busy-work. (VSSDK Assist has its own Wizard that's even more helpful).

The core of any VSPackage solution is the class that inherits from the Microsoft.VisualStudio.Shell.Package class. This class is the point at which the package will be tied into Visual Studio.

A lot of information is intuited by Visual Studio when it loads your package by reading the attributes and interfaces implemented by your package class. In this case, persisting data in the solution is made possible through an interface implementation (see below).

Package Attributes

One of the nicest thing the Wizard does is load up your Package class with the attributes it absolutely must have. I've added a few (ProvideAutoLoad & ProvideToolWindowVisibility) based on the particular behavior I wanted from my Tool Window.

I'll explain a few details about the interesting attributes and how they affect the Package.

It should be noted that most of these settings (and several others) are easily configurable when using VSSDK Assist's "Create/Tool Window" Wizard.

// This attribute is used to register the information needed to show the this package
// in the Help/About dialog of Visual Studio.
[InstalledProductRegistration(false, "#110", "#112", "1.0", IconResourceID = 400)]

110 and 112 are references to strings in the VSPackage.resx resource file (the Package name and Package description respectively).

IconResourceID points to a particular portion of the bitmap also stored in VSPackage.resx.

// This attributes tells the shell that this package has a load key
// embedded in its resources.
[ProvideLoadKey("Standard", "1.0", "Notebook Toolbar", "New Data Systems, Inc.", 113)]

The Package Load Key (PLK) is basically a hash of your company name, product name, and product version generated by Microsoft. The wizard sets the initial LoadKey ID (which in the example is 113) to 1. That is, your Package initially doesn't have a load key.

You can use Packages that do not have a PLK as long as you have installed the Visual Studio SDK. But once you have a PLK, it's pretty easy to incorporate it into your project. Having said that, the Package Load Key is a great big pain in the butt and it's honestly not clear to me why they went and created this particular hurdle.

I almost despaired at getting a hold of a Load Key, but I'll make it easy for you. Go to MSDN's own handy-dandy Generate Load Key page. Fill out the form with the values you'll be using in the ProvideLoadKey attribute, click the button and presto! You have your very own PLK! (Lucky dog.)

Once you have obtained your PLK, copy the information to a text file (I placed mine in the Guids.cs file). Remove all line breaks from the PLK! Now add a resource string to the VSPackage.resx file and copy and paste your PLK in. Note the resource number, as that's part of the ProvideLoadKey attribute (in the example it's 113). Alternatively you can use VSSDK Assist's "Configure VS Package Load Key" feature to set this up for you.

// This attribute is needed to let the shell know that
// this package exposes some menus.
[ProvideMenuResource(1000, 1)]

Without this attribute, your ToolWindow won't show up in the View/Other Windows list. And it might not show up anyways.

// This attribute registers a tool window exposed by this package.
[ProvideToolWindow(typeof(MyToolWindow), Transient = true,
	Style = VsDockStyle.Tabbed, Orientation = ToolWindowOrientation.Bottom)]

This attribute tells Visual Studio that we want to display a ToolWindow (the class is named MyToolWindow).

Transient informs Visual Studio that the window comes and goes (rather than staying until dismissed by the user)

Style & Orientation informs Visual Studio that the window prefers to be docked and tabbed at the bottom of the screen

// This attribute will force the Tool Window to open when a solution is opened
[ProvideAutoLoad(UIContextGuids.SolutionExists)]
// This attribute will cause the Tool Window to close when the solution closes
[ProvideToolWindowVisibility(typeof(MyToolWindow), UIContextGuids.SolutionExists)]

Since the notebook is for Solutions (and the information is stored within the Solution), we want the Tool Window open when the solution opens.
For similar reasons, we want the Tool Window to close when the Solution closes.

// This attribute tags the class with the GUID of the Package
[Guid(GuidList.guidNotebookToolbarPkgString)]

Package Interfaces

All communication between Visual Studio and the Package library is done through Interfaces. Much of this interaction is taken care of through the Package class. However, since I want to store and retrieve information from the Solution, I must implement the IVsPersistSolutionOpts interface.

This brilliantly named interface allows us to persist information in the Solution options (*.suo) file. It has describes four methods (LoadUserOptions, ReadUserOptions, SaveUserOptions, and WriteUserOptions) which encompass the entirety of writing to and reading from the Solution file. And by implementing it, we inform Visual Studio that our package is interested in these events.

/* NotebookToolbarPackage.cs */
int IVsPersistSolutionOpts.LoadUserOptions
	(IVsSolutionPersistence pPersistence, uint grfLoadOpts)
{
	if ( _myWindow == null )
		this.ShowToolWindow(this, new EventArgs());
	return this._myWindow.LoadUserOptions(pPersistence, grfLoadOpts);
}

/* MyToolWindow.cs */
public int LoadUserOptions(IVsSolutionPersistence pPersistence, uint grfLoadOpts)
{
	// initialize for a new project
	this._control.Text = String.Empty;
	this._control.IsLoaded = true;

	try {
		pPersistence.LoadPackageUserOpts(this, STR_NotebookText);
	} finally {
		Marshal.ReleaseComObject(pPersistence);
	}

	return VSConstants.S_OK;
}

When a Solution is loaded, Visual Studio will call the LoadUserOptions method on any packages that implement the IVsPersistSolutionOpts interface.

In the code, the package function checks to see if the ToolWindow has been instantiated yet -- it should be -- and if not, creates it. Then it passes along the LoadUserOptions call to the ToolWindow.

The ToolWindow does some initialization and then loads the information by calling the LoadPackageUserOpts method with the string identifier of the information stream we are interested in. Since the notebook only stores one piece of information, we only make one call. If we needed, we could make successive calls to LoadPackageUserOpts to retrieve all the streams in which our data is stored.

/* MyToolWindow.cs */
public int ReadUserOptions(IStream pOptionsStream, string pszKey)
{
	try {
		using ( Stream wrapper = (Stream)pOptionsStream ) {
			switch ( pszKey ) {
				case STR_NotebookText:
					// this writes the string from the 
					// Text property to the stream
					LoadOptions(wrapper);
					break;
				default:
					break;
			}

		}
		return VSConstants.S_OK;
	} finally {
		Marshal.ReleaseComObject(pOptionsStream);
	}
}

Each call to the LoadPackageUserOpts method results in a call to the ReadUserOptions method with the data stream and its corresponding key. We can use the stream to retrieve our information.

SaveUserOptions and WriteUserOptions work in a very similar way and are called when the solution is saved. The key here is that for each piece of data you want to store or retrieve from the Solution, you need to make a call to the Load/SavePackageUserOpts method.

Passing Information between the Package and MyControl

The actual Windows Forms control I used is simple enough. The note is displayed in the top (black background) text box. The bottom row of controls contains a textbox used for searching and a button that copies the text to the clipboard and a button that clears the text.

Designer View of the ToolWindow Control

The interaction between the MyControl class and the MyToolWindow class is mostly put together by the wizard. The note itself is passed back and forth through the Text property.

Of course, in order to use that property, I need to have a reference to the class object available. And so I have to modify a bit of the generated code.

/* NotebookToolBarPackage.cs */
/// <summary />
/// This function is called when the user clicks the menu item that shows the
/// tool window. See the Initialize method to see how the menu item is associated to
/// this function using the OleMenuCommandService service and the MenuCommand class.
/// </summary />
private void ShowToolWindow(object sender, EventArgs e)
{
	// Get the instance number 0 of this tool window. 
	// This window is single instance so this instance
	// is actually the only one.
	// The last flag is set to true so that if the tool window 
	// does not exists it will be created.
	ToolWindowPane window = this.FindToolWindow(typeof(MyToolWindow), 0, true);
	if ((null == window) || (null == window.Frame))
	{
		throw new NotSupportedException(Resources.CanNotCreateWindow);
	}

	MyWindow = (MyToolWindow)window;

	IVsWindowFrame windowFrame = (IVsWindowFrame)window.Frame;
	Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(windowFrame.Show());
}

As the comment notes, this function spawns the ToolWindow. The one modification I made to this function was to grab a reference to the Window object for my very own.

Testing and Debugging Your Package

The wizard very kindly sets up a testbed environment (which they call an Experimental Hive), so you can muck about with your Package without completely borking your Visual Studio installation. You can test out your package by using F5 or clicking on the Play button, just like you would with any application you were building. The experimental hive will come up with your package loaded (or not, depending on how successful your coding was).

The wizard also sprinkles some Trace statements throughout the package code. They will pop up in the Output window of your development environment (other messages may appear in the Output window of the Experimental Hive). If you're having trouble figuring if your package is loading properly, these Trace statements can be very handy.

Unhandled Exception Message!  You can Debug!

If your package causes an exception, you'll probably get the standard Windows unhandled exception message. Clicking on the Debug button (instead of the usual Don't Send choice) will bring you back to the useful Visual Studio debugger! Very nice.

Testing Your PLK -- Seriously? What is up with these stupid keys?

So you set your package up with your spiffy PLK, but the Experimental Hive would run your package even without the key, so how are you supposed to know if it will work for your coding brethren who don't have an SDK? Fortunately there's a way to tell Visual Studio to force a check on the PLK. In the Properties of your Package Project, under the Debug page, add the /noVSIP flag to the command line arguments (I set up a separate build for this). Once you have the command line argument in place, just use F5 to start the debugging.

If everything is set up properly, and your PLK is valid, you should see this message in the Experimental Hive's Output window "VSIP: Third party package YOURPACKAGENAME approved to load ( GUID = {YOU-RPAC-KAGEGUID} ).". Congratulations! Now you can move on to setting up the installer!

If not, you'll most likely get a dialog box that says "Package '(BUNCH OF PACKAGE INFORMATION)' has failed to load properly ( GUID = {YOU-RPAC-KAGEGUID} ). Please contact package vendor for assistance." and a message in the Output window informing you of the same thing, with the added information "Invalid Load Key".

But I AM the package vendor, codsarnit!

So your PLK didn't work. Time to check a few things:

  • Did you copy the key into the right resource file? It goes in the VSPackage.resx file, not the usual Resources.resx file.
  • Did you copy the key it correctly? Specifically, it should be all on one line with no spaces or breaks. 
  • Does the product information match what's on the Package attributes? Same company name, same product name, same product version. Check the Resource ID, while you're up.
  • Are you certain that the problem lies with the PLK?  Perhaps something else is wrong with your code.
  • You can always generate a different key by slightly changing some of the details on the PLK Generator form. 
  • If you've gotten to this point... sorry, I'm as stumped as you are. 

Creating An Installer

I used VSSDK Assist to create the WiX installer for my project. It's a simple process:

  • Select the "File/Add/New Project..." menu.
  • Open the "Guidance Packages" tree item and select the "VSSDK Assist" category.
  • Select the "Visual Studio Package Setup" template.
  • Name the new Project and click OK.

This brings up the VSSDK Assist wizard. Select the Project which contains the Package and select whether you want your library to be installed into the GAC (Assembly) or not (CodeBase). Then enter the project information you used to generate your PLK. Click Finish.

VSSDK Assist will generate a new WiX project which will build into your VSPackage installer. Check out the PackageRegistry.wxi file to see the entries it will put in the registry. If you want to modify the default installation directory, look in Main.wxs for the CustomAction SetDefault_TARGETDIR and change the Value to suit.

A Note about Assembly vs. CodeBase

If you chose the Assembly option, you will need to register your assembly with the GAC or else your Package will fail to load (because Visual Studio will not be able to find it). From my experiments, this is one area where VSSDK Assist does not quite deliver on its promise. It doesn't seem to be able to register the assembly with the GAC properly. Nor does the installer appear to have this functionality.

The GAC confuses and scares me (and find in onomatopoetic), so I selected the CodeBase option and didn't have any problems.

Points of Interest

Why I Wrote this Article

I found dealing with the documentation and the odd quirks of Visual Studio's SDK to be confusing and frustrating. (Only after I'd really completed a large portion of the project did I realize how much VSSDK Assist was willing to do for me.) Still, I feel that I now have a far greater understanding of the process and I hope that others might benefit from reading about my experience.

Retrieving and Storing Data

The methods I used to save and restore information in the Solution use the *.suo solution file. This information is segmented based on the user, so that different users of the same solution will have a different notebook. This was optimal for my purposes.

You may find you want to store information about the solution which is universally accessible by all users. In that case, check out the IVsPersistSolutionProps interface. You can even persist properties for individual project items using the IVsMonitorSelection interface. See this page on MSDN: How to: Persist the Property of a Project Item.

PLKs Are Easy to Get 

Far less daunting than they first appear. Go to MSDN's own handy-dandy Generate Load Key page.  

It's a Tool Window 

Not a Toolbar!

History

  • 1.0 (28 Jan 2009)
    • First version 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Tom Bellin
New Data Systems
United States United States
No Biography provided

Comments and Discussions

 
GeneralHave you updated this for Visual Studio 2010 PinmemberJefferys14-Jan-11 18:11 
GeneralMy vote of 2 PinmemberSoft_banuma5-Feb-10 22:09 
GeneralWix v3.0 Installer for VS Package PinmemberJohn Radley25-Jan-10 1:28 
GeneralUmm PinmemberDmitri Nesteruk29-Jan-09 7:33 
GeneralRe: Umm PinmemberTom Bellin29-Jan-09 8:11 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140721.1 | Last Updated 29 Jan 2009
Article Copyright 2009 by Tom Bellin
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid