Extending Visual Studio (part 1)

I love Visual Studio, always have. From the moment I wrote my first “Hello World!” in C, until becoming an expert C# developer.

With all due respect, however, there was always something missing. One user needs this, the other needs that. Of course, an IDE can’t have it all. The question is only whether an average user can extend VS to inlcude whatever is missing. This series of posts will try to fill the gaps, trying to answer this question: how to get into Visual Studio that which is missing. I will stick them all into a category named “Extending Visual Studio” for easy reference.

But before I begin, here’s a disclaimer – I am no expert in VS extensions; these posts are simply a collection of my experiences and adventures in the world of extending Visual Studio.

Before we are able to extend Visual Studio, we need the Visual Studio SDK. Install it and then continue reading!

So, you have the SDK? Great!

We’re done with technicalities, let’s create an extension!

If Visual Studio SDK was correctly installed, you will see a few new template categories when you choose to make a new Project. This time, let’s choose the Visual C# | Extensibility | Visual Studio Package.

package

The goal: Create an extension that will show us how many of each filetype we have in our solution.

This is not really useful, but it will teach us several things:

  1. How to create a basic extension
  2. How to show a simple tool window
  3. How to get to the extension environment
  4. How to get to the solution projects and files

Some other time, we’ll switch to more advanced and useful scenario. For now, this is Extending Visual Studio 101.

So, for the Project name, choose File Stats, then follow the wizard instructions. Choose the language and how to generate the signing key.w01

 

Then, fill in some basic info (you choose your own name, not mine):w02

 

Select a Menu Command and the Tool Window. The menu command will open the tool window; that’s what the template will do for us by default if we select both.

w03

Now, give a name the the Command and the Tool window. You can choose whatever you want, but for now you can leave them as cmdidFileStats and cmdidFileStatsToolWindow. Command name is what’s shown as a menu item text, and Window name is the tool window’s header title.w04 w05

Uncheck the Integration and Unit test projects. In normal circumstances, these would be benefitial, but for now, we choose to opt-out.w06

 

Click Finish and wait while Visual Studio generates the extension for you.

The template provides a working extension, so if you’d like, you can simply run the project and see the extension in action. Note that whenever you debug an extension, a new instance of Visual Studio is started (named Experimental Instance). That instance will have your extension installed. You can find our menu item under View / Other Windows / File Stats. By default it only shows the title and a button, but you can move around and dock the tool window just as you would any other tool window in Visual Studio.

toolwindowThat’s our window. It doesn’t do much. If you click the button, you will see a simple message box. That’s it. That’s what the default template gives us. And I guess, that’s enough. We just need to expand it to a desired level of usefulness.

First, let’s take a look at the solution – this is what we have:

solutionResources folder contains a package icon (shown in Extensions window) and the image used to determine the menu item’s icon. Nothing special.

As the file itself says, FileStats.vsct defines the actual layout and type of the commands. This is an XML definition file that describes what to do with your commands. Commands being actions that the extension does when a button is clicked, a menu item is selected etc. The file has more comments than the code itself, so take a look and try to see what’s what. For the purposes of this guide, I will not get into details.

GlobalSuppressions.cs is as boring as it sounds, simply supressing certain warnings. In the file itself, we are also advised not to edit the file manually anyway, so enough about that. Guids.cs is simply a static way to access some randomly generated guids. Similarly, the PkgCmdID simply states some IDs for the package to use. There is no need to modify them or even look at them. Key.snk is a key for strong-named assemblies and there’s no need to get into that in this post.

Resources (.resx files) are the usual resource files which in this case contain only some images and text strings like title or error message. Feel free to ignore them.

As you might have noticed, I skipped a few files. These are FileStatsPackage.cs, MyControl.xaml and source.extension.vsixmanifest.

source.extension.vsixmanifest is a manifest file describing the extension. Open it and modify it according to your wish (though you don’t actually need to change anything).

MyControl.xaml is a WPF control that will be embedded in a Visual Studio tool window. That’s what was open when you created the project – the form with a title and a button. We’re gonna change this later into something we need.

The FileStatsPackage.cs is the class that governs the package itself. It contains a class that inherits from Package which has an Initialize method. That method is called when the package is initialized. This will be the first one we’re going to change.

But first, let’s create a new class – VisualStudio.cs.

namespace KornelijePetak.FileStats
{
	using EnvDTE80;

	internal static class VisualStudio
	{
		public static DTE2 Environment { get; set; }
	}
}

Simple as that.
Now, open the FileStatsPackage.cs and add these to the beginning:

using EnvDTE;
using EnvDTE80;

Then find the Initialize() method and just after base.Initialize() add this:

protected override void Initialize()
{
	// ... some code here
	base.Initialize();

	VisualStudio.Environment = GetService(typeof(DTE)) as DTE2;

	// ...	additional code here
}

What we’ve done here is globally stored the DTE (“Development Tools Environment”) object. That is the core of everything you want to do with Visual Studio. Assigning it to a global variable may not be the best practice, but for this guide, bear with me. It’s so we can access it from different parts of the extension.

Let’s create a new class – FileExtensionStat. This class will represent a single extension and tell us how many files with that extension the solution has.

public class FileExtensionStat
{
	public FileExtensionStat(String extension, int count)
	{
		Extension = extension;
		Count = count;
	}

	public String Extension { get; private set; }
	public int Count { get; private set; }
}

Now that we have a class that will represent our stats, let’s create a view model (in order to keep things simple, I will not implement commands, but for serious development, you really should).

namespace KornelijePetak.FileStats
{
	using System.Collections.ObjectModel;

	public class ViewModel
	{
		public ViewModel()
		{
			Files = new ObservableCollection<FileExtensionStat>();
		}

		public void Reload()
		{
			// Empty for now
		}

		public ObservableCollection<FileExtensionStat> Files { get; private set; }
	}
}

We have not yet implemented the Reload method, but we have prepared the observable collection for data binding. Let’s open MyControl.xaml and instead of its Grid element, paste this one:

<Grid>
	<Grid.RowDefinitions>
		<RowDefinition Height="Auto" />
		<RowDefinition Height="*" />
	</Grid.RowDefinitions>

	<Button Click="onReloadClicked"
			Content="Reload"
			Padding="5" />
	<ListBox Grid.Row="1"
			 ItemsSource="{Binding Files}" />
</Grid>

Now right-click anywhere on the designer and choose View Code. You can delete the old button’s event handler which is no longer used. Add your own event handler and instantiate the ViewModel. Like this:

public partial class MyControl : UserControl
{
	public MyControl()
	{
		InitializeComponent();

		DataContext = model = new ViewModel();
	}

	private ViewModel model;

	private void onReloadClicked(object sender, RoutedEventArgs e)
	{
		model.Reload();
	}
}

Now we only need to implement the Reload method in the View Model.

public void Reload()
{
	Solution solution = VisualStudio.Environment.Solution;

	if (solution == null)
		return;

	Dictionary<String, int> extensions = new Dictionary<string, int>();

	foreach (Project project in solution.Projects)
		foreach (ProjectItem item in project.ProjectItems)
			findFilesInProjectItems(item, extensions);

	Files.Clear();

	extensions
		.Select(kvp => new FileExtensionStat(kvp.Key, kvp.Value))
		.ToList()
		.ForEach(fes => Files.Add(fes));
}

This is the place where we need that VisualStudio.Environment we have saved earlier. We are going to use Dictionary (line #8) to store the number of files for each extension. Lines #10 through #12 walk through all the projects items that are found in all of the solution projects.

Now, the term ProjectItem needs clarification. ProjectItem usually means a file in a project, however it can be other things. One of the most important ProjectItem kinds is a Folder. So, if you have a folder somewhere in your project, that will be represented by a ProjectItem (which in turns contains other ProjectItems). The other important scenarion in which a ProjectItem does not map directly to a file is when it maps to several files. For example, creating a Windows Forms class through Visual Studio will create a file with the code-behind and a designer generated initialization file. These are represented as a single ProjectItem, albeit with two associated filenames. The same holds for LESS and CoffeeScript editors. Just remember that even though it’s only one ProjectItem (of kind PhysicalFile) it may be associated with several.

So, for each ProjectItem we call a recursive method that will count the number of files by their filename extensions and fill the dictionary accordingly. The resulting mappings are converted to a previously defined class FileExtensionStat and the ViewModel is updated.

The mentioned recursive function looks like this:

public void findFilesInProjectItems(ProjectItem item, Dictionary<String, int> extensions)
{
	if (item.Kind == Constants.vsProjectItemKindPhysicalFile)
	{
		for (short i = 0; i < item.FileCount; i++)
		{
			string extension = Path.GetExtension(item.FileNames[i]);

			if (!extensions.ContainsKey(extension))
				extensions[extension] = 0;

			extensions[extension]++;
		}
	}

	if (item.Kind == Constants.vsProjectItemKindPhysicalFolder)
	{
		foreach (ProjectItem childrenItem in item.ProjectItems)
		{
			findFilesInProjectItems(childrenItem, extensions);
		}
	}
}

For physical files, we simply move through all their filenames and count the extensions. For folder, we call this method recursively to go through their ProjectItems.

That’s it.

  • Run the debugger
  • Open a random project in a newly opened VS intance
  • Find your menu and open the tool
  • Click the buton!

If you open the FileStats project itself, you should be able to something like this:

demo

Basically, that’s it. There is a whole lot of stuff you can do by browsing through the VisualStudio.Environment object. Start in debug mode, set a breakpoint and see what else is there. For this post it will be enough.

Next time – handling events. That’s gonna be interesting.

 

Leave a Reply

Your email address will not be published. Required fields are marked *