Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Visual Studio Custom Tools - Make It Smarter

0.00/5 (No votes)
13 Jan 2014 1  
Improve the way your custom tool creates file...

Introduction

When one uses Visual Studio, one uses Custom Tools. It's true even if one never notices it. Resource files (resx) using Custom Tools. DataSet (xsd) and EntityModel (edmx) using Custom Tools. WCF Service References (svcmap) and Text Templates (tt) using Custom Tools. Custom Tools used by Visual Studio not without reason, these are very powerful tools. Why? Because simplicity. Custom Tools transform a single input file into a single output file that can be added to a project.
Even I found Custom Tools very powerful I also found it has one weak point. In this article I'll try to explain that point and suggest a solution...

Background

Most of my development involves JavaScript. For reasons of deployment I'm embedding most of my JavaScript into dll and referencing them from there. I also minifying those files before compilation. The minifying process uses home-made Custom Tool. And there is the problem. When Visual Studio adds the newly created file to the project it assigns the default Build Action to it. In my case it's not perfect, as I already declared the original file as Embedded Resource and want to embed also the new - minified - file too (JavaScript file's default Build Action is Content). In the code below you may see how to overcome this particular restriction.

Using the Code

The codes snippets inside the article ware taken from the demo, but are not will make into a complete solution - for that use the attached project...  

Ways to Access Visual Studio from the Inside

In Visual Studio there is three different ways to access the development environment and its content.

Microsoft.VisualStudio.Shell

This namespace is the foundation of any extension you may writ ever. It's including all the interfaces and ID codes you need to link your functionality to existing parts of Visual Studio, or create a new menu item, toolbar button, editor or whatever (http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.shell.aspx).

DTE (Development Tools Environment)

DTE is used to access the current instance of different parts of VS, like active document and project, option pages and so. (http://msdn.microsoft.com/en-us/library/envdte.dte.aspx).

Microsoft.Build.Evaluation

This namespace - with all its classes - enables to see the currently opened solution (project) as MSBuild sees them. It can be useful to get project's property values that are no part of the VS Shell environment but are int the saved project file (http://msdn.microsoft.com/en-us/library/microsoft.build.evaluation(v=vs.110).aspx).

I mentioned here these ways of access just because I will use all of them in my solution...

The Solution

My solution based on these steps:

  • Write the output file
  • Add the file to the project
  • Update properties of original and new file in the project to create link and dependency
  • Reload project

To be familiar with the working environment I put here an outline, how project that implement Custom Tool looks like...

public sealed class VSMinifier : Package, IVsSingleFileGenerator
{
	int IVsSingleFileGenerator.DefaultExtension ( out string DefaultExtension )
	{
		DefaultExtension = GetDefaultExtension( );
 
		return ( VSConstants.S_OK );
	}
 
	int IVsSingleFileGenerator.Generate ( string InputFilePath, string InputFileContents, string DefaultNamespace, IntPtr[ ] OutputFileContents, out uint Output, IVsGeneratorProgress GenerateProgress )
	{
		byte[ ] bOutputFileContents = GenerateCode( InputFileContents );
 
		if ( bOutputFileContents == null )
		{
			Output = 0;
 
			return ( VSConstants.E_FAIL );
		}
		else
		{
			Output = ( uint )bOutputFileContents.Length;
 
			OutputFileContents[ 0 ] = Marshal.AllocCoTaskMem( bOutputFileContents.Length );
			Marshal.Copy( bOutputFileContents, 0, OutputFileContents[ 0 ], bOutputFileContents.Length );
			
			return ( VSConstants.S_OK );
		}
	}
}

The first method - DefaultExtension - is used to inform VS what extension to use when creating the new file (the name will be the same as the original file).

The other method - Generate - used to generate and return the binary content of the new file based on the original one.  

The Generate method is the point I will change everything... Let start with the file creation process. 

int IVsSingleFileGenerator.Generate ( string InputFilePath, string InputFileContents, string DefaultNamespace, IntPtr[ ] OutputFileContents, out uint Output, IVsGeneratorProgress GenerateProgress )
{
	byte[ ] bOutputFileContents = GenerateCode( InputFileContents );
 
	string szOutputFilePath = InputFilePath.Replace ( Path.GetExtension ( _InputFilePath ), _Ext );
	FileStream oOutputFile = File.Create ( szOutputFilePath );
 
	oOutputFile.Write ( bOutputFileContents, 0, bOutputFileContents.Length );
	oOutputFile.Close ( );
 
	Output = 0;
 
	return ( VSConstants.E_FAIL );
} 

There is not to much in this file-saving, but please notice that I return E_FAIL to VS. This will stop VS to try and create a file. It seems to be the only (known to me) drawback of the solution, but fortunately VS does not care!

The second step - of adding the file to the project - is more complicated and involves all the mentioned methods of accessing VS...

EnvDTE.DTE oDTE = ( DTE )Package.GetGlobalService ( typeof ( DTE ) );
System.IServiceProvider oServiceProvider = new ServiceProvider ( ( Microsoft.VisualStudio.OLE.Interop.IServiceProvider )oDTE );
Microsoft.Build.Evaluation.Project oBuildProject = ProjectCollection.GlobalProjectCollection.GetLoadedProjects ( oDTE.ActiveDocument.ProjectItem.ContainingProject.FullName ).Single ( );
ProjectProperty oGUID = oBuildProject.AllEvaluatedProperties.Single ( oProperty => oProperty.Name == "ProjectGuid" );
IVsHierarchy oVsHierarchy = VsShellUtilities.GetHierarchy ( oServiceProvider, new Guid ( oGUID.EvaluatedValue ) );
IVsProject oVsProject = ( IVsProject )oVsHierarchy;
VSADDRESULT[ ] oAddResult = new VSADDRESULT[ 1 ];
 
string szItemPath = ( string )oDTE.ActiveDocument.ProjectItem.Properties.Item ( "FullPath" ).Value;
uint nItemId;
string szNewItemPath = ( szItemPath ).Replace ( Path.GetExtension ( InputFilePath ), GetDefaultExtension ( ) );
 
oVsHierarchy.ParseCanonicalName ( szItemPath, out nItemId );
 
oVsProject.AddItem ( nItemId, VSADDITEMOPERATION.VSADDITEMOP_OPENFILE, szNewItemPath, 1, new string[ ] { szNewItemPath }, IntPtr.Zero, oAddResult );
 
if ( oAddResult[ 0 ] != VSADDRESULT.ADDRESULT_Success )
{
	GenerateProgress.GeneratorError ( 1, 1, "VSMinifier can't add minified file to solution...", 0, 0 );
}
else
{
} 

Huh. It's got complicated. Let try some explanations...

First let see the AddItem method. The parameters - in order - are these: parent item, how to add the new file (in my case is opening a file), the name for the new item (in my case it must be the full path), number of files to add (obviously 1), the path to the file to add (could be array of paths),  a dialog to open to choose the item to add (it's zero as I have no need of dialog) and an output parameter to receive response.

It's a relatively simple line of API call, but the on the way to get it run I had to utilize a lot of functionality. One of the most interesting parts is that also VisualStudio.Shell and Build.Evaluation containing a Project class but they have a different set of properties, for instance to get the GUID of the project I had to use Build.Evaluation, but for the canonical name of the original item I had to use VisualStudio.Shell

You may also noticed that I'm casting the same object to different types when I need to access different properties. It's may seem screwy, but one thing that I have learned that every object inside VS has a huge number of interfaces. For example an object representing a project an be seen also a hierarchy... 

The next part of my adventure is to declare the dependencies between the original and the newly added file. First let see how VS declares those dependencies.

<Content Include="Scripts\modernizr-2.6.2.js">
  <Generator>VSMinifier</Generator>
  <LastGenOutput>modernizr-2.6.2.min.js</LastGenOutput>
</Content>
<Content Include="Scripts\modernizr-2.6.2.min.js">
  <AutoGen>True</AutoGen>
  <DesignTime>True</DesignTime>
  <DependentUpon>modernizr-2.6.2.js</DependentUpon>
</Content>

This XML snippet show how VS declares the dependencies inside the project file. This part not documented but it's the same since VS 2008 so I took it and write code to recreate it...

EnvDTE.DTE oDTE = ( DTE )Package.GetGlobalService( typeof( DTE ) );
System.IServiceProvider oServiceProvider = new ServiceProvider( ( Microsoft.VisualStudio.OLE.Interop.IServiceProvider )oDTE );
Microsoft.Build.Evaluation.Project oBuildProject = ProjectCollection.GlobalProjectCollection.GetLoadedProjects( oDTE.ActiveDocument.ProjectItem.ContainingProject.FullName ).Single( );
ProjectProperty oGUID = oBuildProject.AllEvaluatedProperties.Single( oProperty => oProperty.Name == "ProjectGuid" );
IVsHierarchy oVsHierarchy = VsShellUtilities.GetHierarchy( oServiceProvider, new Guid( oGUID.EvaluatedValue ) );
IVsBuildPropertyStorage oVsBuildPropertyStorage = ( IVsBuildPropertyStorage )oVsHierarchy;
string szItemPath = ( string )oDTE.ActiveDocument.ProjectItem.Properties.Item( "FullPath" ).Value;
uint nItemId;
string szNewItemPath = ( szItemPath ).Replace( Path.GetExtension( _InputFilePath ), GetDefaultExtension( ) );
uint nNewItemId;
 
oVsHierarchy.ParseCanonicalName( szItemPath, out nItemId );
 
oVsBuildPropertyStorage.SetItemAttribute( nItemId, "LastGenOutput", Path.GetFileName( szOutputFilePath ) );
 
oVsHierarchy.ParseCanonicalName( szNewItemPath, out nNewItemId );
 
oVsBuildPropertyStorage.SetItemAttribute( nNewItemId, "AutoGen", "True" );
oVsBuildPropertyStorage.SetItemAttribute( nNewItemId, "DesignTime", "True" );
oVsBuildPropertyStorage.SetItemAttribute( nNewItemId, "DependentUpon", Path.GetFileName( _InputFilePath ) );
 
Microsoft.Build.Evaluation.ProjectItem oItem = oBuildProject.Items.Where( item => item.EvaluatedInclude.EndsWith( Path.GetFileName( szItemPath ) ) ).Single( );
Microsoft.Build.Evaluation.ProjectItem oNewItem = oBuildProject.Items.Where( item => item.EvaluatedInclude.EndsWith( Path.GetFileName( szNewItemPath ) ) ).Single( );
 
oNewItem.ItemType = oItem.ItemType;

Using BuildPropertyStorage on original and new item enabled in no time to add the required attributes - it was an easy part (you may noticed that I do not add the Generator attribute - VS will do it for me). There are two reasons to create these dependencies. One is that VS uses them to display connected files in hierarchy. The other that when removing the Custom Tool from the original file VS finds the connected file using these attributes, so to keep consistent with original VS behavior I created exactly the same structure for project file.

And then... the reason for all this mess, copying ItemType from original to new item! 

At this point we are done, the project contains the new file, the dependencies are in place... But nothing changed visually! If you go and save your project (the one using the new Custom Tool) and reload it you will see the new file, perfectly connected to its origin. For sure this is not useful - every time you run a Custom Tool with this kind of extension, you have to save and reload your project! Nonsense! 

So here the last bit of code that do exactly the same thing for you!

oDTE.Windows.Item ( EnvDTE.Constants.vsWindowKindSolutionExplorer ).Activate ( );
( ( DTE2 )oDTE ).ToolWindows.SolutionExplorer.GetItem ( string.Format ( "{0}\\{1}", Path.GetFileNameWithoutExtension ( oDTE.Solution.FullName ), oName.EvaluatedValue ) ).Select ( vsUISelectionType.vsUISelectionTypeSelect );
 
oBuildProject.Save ( );
 
oDTE.ExecuteCommand ( "Project.UnloadProject" );
oDTE.ExecuteCommand ( "Project.ReloadProject" );
 
oDTE.Windows.Item ( EnvDTE.Constants.vsWindowKindSolutionExplorer ).Activate ( );
( ( DTE2 )oDTE ).ToolWindows.SolutionExplorer.GetItem ( string.Format ( "{0}\\{1}\\{2}", Path.GetFileNameWithoutExtension ( oDTE.Solution.FullName ), oName.EvaluatedValue, oItem.EvaluatedInclude ) ).Select ( vsUISelectionType.vsUISelectionTypeSelect );

The first and last part (which are identical) are used to focus on the right project before saving and after reloading. That will ensure continuous work for the developer. In the middle there is code to save and reload the project, where the interesting part is the way commands that are not part of the interfaces are executed... 

Summary 

That's all folks...

There are several things I learned from this journey inside Visual Studio, the most important is that it's far too complicated... At the bottom line I achieved a very minor change (event it's very important for me) with a large amount of code. On the other hand I also learned that even with total (almost) lack of documentation from Microsoft you can go really far... 

I think that you should go and carve yourself into this only if you really need!  

Demo 

The attached demo project includes my Custom Tool for minifying JavaScript and CSS files, with the ability to choose and configure the minifying engine.

You can install the tool itself from here http://visualstudiogallery.msdn.microsoft.com/0b301ee9-4bf1-4fba-b524-95d89dc31a19... 

 

 

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here