|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionVisual Studio is a great environment for Rapid Application Development. It also provides a rich extensibility API for customizing it for your specific needs. When a file is selected in the Solution Explorer, the property grid displays several attributes, common across any file type. One of these is the "Custom Tool" attribute. When this attribute is set correctly, it refers to an Assembly/Type that can generate a new file (whenever the source file is saved), based upon the source file as input, and store it as a child node of the source file, in the Solution Explorer. The complexities behind creating and deploying your own Custom Tools are surprisingly few. The name that is placed in the "Custom Tool" attribute, is simply referring to a node in the registry, which in turn, points to an Assembly and a Type in that Assembly (in the case of Custom Tools written in Managed Code). This allows Visual Studio to instantiate the Custom Tool, and execute its interface methods to generate the required output. One pre-requisite of a Custom Tool is that it implements a specific Managed Interface, to allow Visual Studio to call the transformation, for a given file. This interface, BackgroundThis document does not explain the procedure for creating or deploying a Custom Tool, you are expected to already know this. This document explains how to create a reusable base class, which overcomes the 'Single File' limitation. An Extensible Approach to Multiple File GenerationObviously, generating multiple files from a single source is going to have some inherent application specific dependencies. I have used .NET 2.0, and generics to make the implementation highly flexible, allowing you to simply override the base class, named ' To begin, I will explain the inner workings of the base class. Under the HoodThe attached project implements an abstract base class (an implementation of Firstly, let's begin the class declaration: Required References and PrerequisitesBefore you begin, you will need to make sure that you have the Visual Studio SDK installed, to have access to the assemblies you will need. You can download this from the Microsoft Visual Studio Extensibility website. You will need the following references added to your project to use the required classes and interfaces.
Our Type DeclarationWe define our class as public abstract class VsMultipleFileGenerator<IterativeElementType><T> :
IEnumerable<T><IterativeElementType>,
IVsSingleFileGenerator,
IObjectWithSite
{
#region Visual Studio Specific Fields
private object site;
private ServiceProvider serviceProvider = null;
#endregion
#region Our Fields
private string bstrInputFileContents;
private string wszInputFilePath;
private EnvDTE.Project project;
private List<string><STRING> newFileNames;
#endregion
protected EnvDTE.Project Project
{
get
{
return project;
}
}
protected string InputFileContents
{
get
{
return bstrInputFileContents;
}
}
protected string InputFilePath
{
get
{
return wszInputFilePath;
}
}
private ServiceProvider SiteServiceProvider
{
get
{
if (serviceProvider == null)
{
IServiceProvider oleServiceProvider =
site as IServiceProvider;
serviceProvider = new ServiceProvider(oleServiceProvider);
}
return serviceProvider;
}
}
The class uses a generic type declaration, defining The Visual Studio specific interfaces are Our class then declares a few private instance variables to provide as service fields to our concrete sub-class.
Finally, we have a variable called public VsMultipleFileGenerator()
{
EnvDTE.DTE dte = (EnvDTE.DTE)Package.GetGlobalService(typeof(EnvDTE.DTE));
Array ary = (Array)dte.ActiveSolutionProjects;
if (ary.Length > 0)
{
project = (EnvDTE.Project)ary.GetValue(0);
}
newFileNames = new List<STRING><string>();
}
The constructorHere we perform some basic field initialization. After obtaining a reference to the DTE object, we can grab the active Project from the solution. Finally, we must instantiate the Enumerationpublic abstract IEnumerator<IterativeElementType>
These methods satisfy the
More Methods to OverrideOur generator needs to know some more things about the files we will be generating. Firstly, we need to know what to call each file: protected abstract string GetFileName(IterativeElementType element);
The implementation class must override this, and interrogate whatever type their ' We also need to actually generate some content for each of these target files, which is what the following public abstract byte[] GenerateContent(IterativeElementType element);
Finally, we have to deal with some legacy stuff. The interface we have implemented, I have taken the approach that this single file can contain summary information about our generation process, so I have named the method accordingly. I usually declare this file as a ".txt" file, and fill it with some auto-generated descriptive information regarding the set of files that have generated, and the name of the tool responsible.
public abstract string GetDefaultExtension();
public abstract byte[] GenerateSummaryContent();
Where all the Magic HappensThe public void Generate(string wszInputFilePath, string bstrInputFileContents,
string wszDefaultNamespace, out IntPtr rgbOutputFileContents,
out int pcbOutput, IVsGeneratorProgress pGenerateProgress)
{
this.bstrInputFileContents = bstrInputFileContents;
this.wszInputFilePath = wszInputFilePath;
this.newFileNames.Clear();
int iFound = 0;
uint itemId = 0;
EnvDTE.ProjectItem item;
VSDOCUMENTPRIORITY[] pdwPriority = new VSDOCUMENTPRIORITY[1];
// obtain a reference to the current project as an IVsProject type
Microsoft.VisualStudio.Shell.Interop.IVsProject VsProject =
VsHelper.ToVsProject(project);
// this locates, and returns a handle to our source file, as a ProjectItem
VsProject.IsDocumentInProject(InputFilePath, out iFound,
pdwPriority, out itemId);
// if our source file was found in the project (which it should have been)
if (iFound != 0 && itemId != 0)
{
Microsoft.VisualStudio.OLE.Interop.IServiceProvider oleSp = null;
VsProject.GetItemContext(itemId, out oleSp);
if (oleSp != null)
{
ServiceProvider sp = new ServiceProvider(oleSp);
// convert our handle to a ProjectItem
item = sp.GetService(typeof(EnvDTE.ProjectItem))
as EnvDTE.ProjectItem;
}
else
throw new ApplicationException
("Unable to retrieve Visual Studio ProjectItem");
}
else
throw new ApplicationException
("Unable to retrieve Visual Studio ProjectItem");
// now we can start our work,
// iterate across all the 'elements' in our source file
foreach (IterativeElementType element in this)
{
try
{
// obtain a name for this target file
string fileName = GetFileName(element);
// add it to the tracking cache
newFileNames.Add(fileName);
// fully qualify the file on the filesystem
string strFile = Path.Combine( wszInputFilePath.Substring(0,
wszInputFilePath.LastIndexOf
(Path.DirectorySeparatorChar)), fileName);
// create the file
FileStream fs = File.Create(strFile);
try
{
// generate our target file content
byte[] data = GenerateContent(element);
// write it out to the stream
fs.Write(data, 0, data.Length);
fs.Close();
// add the newly generated file to the solution,
// as a child of the source file...
EnvDTE.ProjectItem itm =
item.ProjectItems.AddFromFile(strFile);
/*
* Here you may wish to perform some addition logic
* such as, setting a custom tool for the target file if it
* is intended to perform its own generation process.
* Or, set the target file as an 'Embedded Resource' so that
* it is embedded into the final Assembly.
EnvDTE.Property prop = itm.Properties.Item("CustomTool");
//// set to embedded resource
itm.Properties.Item("BuildAction").Value = 3;
if (String.IsNullOrEmpty((string)prop.Value) ||
!String.Equals((string)prop.Value, typeof
(AnotherCustomTool).Name))
{
prop.Value = typeof(AnotherCustomTool).Name;
}
*/
}
catch (Exception)
{
fs.Close();
if ( File.Exists( strFile ) )
File.Delete(strFile);
}
}
catch (Exception ex)
{
}
}
// perform some clean-up, making sure we delete any old
// (stale) target-files
foreach (EnvDTE.ProjectItem childItem in item.ProjectItems)
{
if (!(childItem.Name.EndsWith(GetDefaultExtension()) ||
newFileNames.Contains(childItem.Name)))
// then delete it
childItem.Delete();
}
// generate our summary content for our 'single' file
byte[] summaryData = GenerateSummaryContent();
if (summaryData == null)
{
rgbOutputFileContents = IntPtr.Zero;
pcbOutput = 0;
}
else
{
// return our summary data, so that Visual Studio may write it to disk.
rgbOutputFileContents = Marshal.AllocCoTaskMem(summaryData.Length);
Marshal.Copy(summaryData, 0,
rgbOutputFileContents, summaryData.Length);
pcbOutput = summaryData.Length;
}
}
Using the codeNow that we have written our extensible base class, we can begin the fun part, actually providing an implementation. As an example, I am going to generate a custom tool that takes an HTML file as its source, and retrieves all the images ( [Guid("6EE05D8F-AAF9-495e-A8FB-143CD2DC03F5")]
public class HtmlImageEmbedderCustomTool : VsMultipleFileGenerator<STRING>
{
public override IEnumerator<STRING> GetEnumerator()
{
Stream inStream = File.OpenRead(base.InputFilePath);
Regex regAnchor = new Regex("<img src=[\"']([^\"']+)[\"'][^>]+[/]?>",
RegexOptions.IgnoreCase);
try
{
StreamReader reader = new StreamReader(inStream);
string line = null;
while ((line = reader.ReadLine()) != null)
{
MatchCollection mc = regAnchor.Matches(line);
foreach (Match match in mc)
{
// yield each element to the enumerator
yield return match.Groups[1].Value;
}
}
}
finally
{
inStream.Close();
}
}
protected override string GetFileName(string element)
{
return element.Substring(element.LastIndexOf('/') + 1);
}
public override byte[] GenerateContent(string element)
{
// create the image file
WebRequest getImage = WebRequest.Create(element);
return StreamToBytes( getImage.GetResponse().GetResponseStream() );
}
public override byte[] GenerateSummaryContent()
{
// I'm not going to put anything in here...
return new byte[0];
}
public override string GetDefaultExtension()
{
return ".txt";
}
protected byte[] StreamToBytes(Stream stream)
{
MemoryStream outBuffer = new MemoryStream();
byte[] buffer = new byte[1024];
int count = 0;
while( (count = stream.Read( buffer, 0, buffer.Length )) > 0 )
{
outBuffer.Write( buffer, 0, count );
}
return outBuffer.ToArray();
}
}
You can then perform the usual registry additions to register the custom tool assembly (remember to add it to the GAC first), then activate the Custom Tool against an HTML file that has image tags with absolute URLs. This will result in all the images linked from the HTML, being downloaded and saved into the project as child items of the HTML node. NotesIf you are using the sample solution, I have already created a pre/post build event which adds the assembly to the GAC every time you compile (removing it first - to refresh any changes). There is also a .reg file in the solution folder which will register the custom tool for you. If you have downloaded the demo zip, the .reg file is also included, but you will need to manually drag the assembly into 'c:\windows\assembly', to register it in the GAC. Remember: If you are running Microsoft Vista, you will need to run Visual Studio as Administrator for the build events to work. Once you have registered the custom tool, start up a new instance of Visual Studio (so that it may re-load the registry configuration), and add an HTML file to the solution (make sure all the ' Additional CodeThe downloadable sample project contains additional helper code in the form of a History
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||