65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Visual Studio 2005 中创建自定义工具以生成多个文件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (16投票s)

2006 年 11 月 25 日

9分钟阅读

viewsIcon

133614

downloadIcon

1775

Visual Studio 提供了通过“自定义工具”扩展环境以从一个文件生成另一个文件的接口。<br>现在,学习如何轻松且可扩展地生成多个文件。

Solution Explorer: An Existing Custom Tool

Property Grid: Setting Your Custom Tool

The End Result

引言

Visual Studio 是一个出色的快速应用程序开发环境。它还提供了一个丰富的可扩展性 API,用于根据您的特定需求对其进行自定义。
一个很好的例子就是被广泛使用的“自定义工具”功能。

当在解决方案资源管理器中选中一个文件时,属性网格会显示几个属性,这些属性对于任何文件类型都是通用的。其中之一就是“自定义工具”属性。当此属性设置正确时,它会引用一个程序集/类型,该程序集/类型可以根据源文件作为输入,在源文件保存时生成一个新文件,并将其作为源文件的子节点存储在解决方案资源管理器中。
例如,有 MSDiscoGeneratorMSDataSetGeneratorResXFileCodeGenerator

创建和部署自己的自定义工具的复杂性出奇地少。放置在“自定义工具”属性中的名称,仅仅是指向注册表中的一个节点,该节点又指向一个程序集以及该程序集中的一个类型(对于用托管代码编写的自定义工具而言)。这允许 Visual Studio 实例化自定义工具,并执行其接口方法来生成所需的输出。

自定义工具的一个先决条件是它实现了特定的托管接口,以便 Visual Studio 可以调用给定文件的转换。这个接口 Microsoft.VisualStudio.TextTemplating.VSHost.IVsSingleFileGenerator,不幸的是,它只包含生成单个目标文件(从每个源文件)的挂钩。

背景

本文档不解释创建或部署自定义工具的步骤,您应已了解这些。本文档解释如何创建一个可重用的基类,该类克服了“单文件”的限制。
有关创建自定义工具的信息,请阅读 Jasmin Muharemovics 的文章,题为“VS.NET 基于 CodeDOM 的字符串资源管理自定义工具”。

用于多文件生成的扩展方法

显然,从单个源生成多个文件将具有一些固有的应用程序特定依赖关系。
例如,如果您的输入文件是 HTML,并且您要为该 HTML 中每个出现的 <IMG> 标签生成一个子文件,您将需要特定的代码来找到每个实例,然后迭代地执行“生成”例程。

我使用了 .NET 2.0 和泛型来使实现高度灵活,允许您仅覆盖名为 'VsMultipleFileGenerator' 的基类,并实现 3 个简单的方法。

首先,我将解释基类的工作原理。

幕后

附加的项目实现了一个抽象基类(IVsSingleFileGenerator 的实现),可以轻松地对其进行子类化以实现多文件生成。

首先,我们开始类声明

所需的引用和先决条件

在开始之前,您需要确保已安装 Visual Studio SDK,以便能够访问您需要的程序集。

您可以从Microsoft Visual Studio 可扩展性网站下载。

您将需要以下引用添加到您的项目中,才能使用所需的类和接口。

  1. EnvDTE
  2. Microsoft.VisualStudio.OLE.Interop
  3. Microsoft.VisualStudio.Shell
  4. Microsoft.VisualStudio.Shell.Interop
  5. Microsoft.VisualStudio.Shell.Interop.8.0
  6. Microsoft.VisualStudio.TextTemplating.VSHost

我们的类型声明

我们将类定义为 abstract,这样我们就强制实现声明某些方法,这些方法对于迭代和生成过程是必需的。

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;
        }
    }

该类使用泛型类型声明,将 IterativeElementType 定义为将传递给我们的生成方法的那种类型。
此类还实现了 IEnumerable<>,它也接收 IterativeElementType 类型作为其枚举类型。这样做的结果是,我们的类将提供强类型枚举功能,以从底层文件中检索每个元素类型。这种类型可能是一个 System.Xml.XmlNode,或者如果您正在执行流的某些自定义反序列化,则可能是其他类型。

特定的 Visual Studio 接口是 IVsSingleFileGeneratorIObjectWithSite,这些接口为 Visual Studio 提供了实际初始化和执行自定义工具所需的挂钩。

然后,我们的类声明了几个私有实例变量,以作为服务字段提供给我们的具体子类。

  1. bstrInputFileContents
    此变量由 Visual Studio 填充,它通常包含源文件的实际字符串内容。我们将它放在此变量中,以便我们的接口方法(例如 IEnumerable 接口)可以访问它。
  2. wszInputFilePath
    这是源文件在磁盘上的物理位置,同样由 Visual Studio 填充。出于与上述相同的原因,我们将其放入实例字段中,并为我们的具体基类提供一个只读的受保护属性以供访问。
  3. project
    因为我们实际上是在重写 Visual Studio 的单文件生成器的功能,所以我们需要访问 IDE 对象模型,以便能够将我们要创建的文件与源文件关联起来,使它们能够作为子节点出现在解决方案资源管理器中。
    我们为此字段创建了一个实例声明,而不是仅仅将其实现隐藏在生成方法中,以便我们可以为具体子类提供访问权限。让我们的子类在获取另一个 DTE 引用时重复两次开销是没有意义的,因为我们已经完成了!

最后,我们有一个名为 newFileNames 的变量,它存储了在自定义工具执行时我们正在生成的文件的列表。目的是允许我们的代码在所有子文件生成后,确保任何在生成过程中名称发生更改的子文件都被删除其前一个文件。
使用简单的 IVsSingleFileGenerator 时,不需要这种清理,因为每次自定义工具执行时文件名都保持不变,从而确保它始终会覆盖旧文件。我们的 VsMultipleFileGenerator 每次执行时都可能改变文件的数量和名称,因此它必须在完成后清理任何旧文件。

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>();
} 

构造函数

在这里,我们执行一些基本的字段初始化。在获得 DTE 对象引用后,我们可以从解决方案中获取活动项目。最后,我们必须实例化 newFileNames 集合。

枚举

public abstract IEnumerator<IterativeElementType> GetEnumerator();

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
    return GetEnumerator();
}

这些方法满足 IEnumerable<IterativeElementType> 接口,并且我们的具体子类必须实现它们。

VsMultipleFileGenerator 要求任何具体子类实现这些方法,以返回一个 IEnumerator,其中包含一个列表,该列表包含子类声明要表示的任何类型。您将在“代码使用”部分看到如何实现这些方法。

更多要重写的方法

我们的生成器需要了解我们将要生成的文件的更多信息。首先,我们需要知道每个文件的名称。

protected abstract string GetFileName(IterativeElementType element);   

实现类必须覆盖此方法,并检查其 'element' 变量的任何类型,以确定返回什么文件名。这是一个非限定文件名,所以应该看起来像“MyFirstFile.txt”。

我们还需要实际为这些目标文件中的每一个生成一些内容,这就是下面的 abstract 方法的作用。

public abstract byte[] GenerateContent(IterativeElementType element); 

最后,我们必须处理一些遗留问题。我们实现的接口 IVsSingleFileGenerator 显然只打算生成一个文件。所以,不幸的是,我们的代码必须满足这个要求,并为这个文件生成内容,以及我们所有的其他文件。我们对这个“单个”文件的名称没有太多控制权,Visual Studio 会根据源文件名并附加“默认扩展名”字符串来调用它。这很可能是为了降低自定义工具生成命名冲突的风险,从而可能生成覆盖解决方案中现有文件的文件——这是我们必须警惕的。

我的方法是让这个单个文件包含我们生成过程的摘要信息,所以我相应地命名了该方法。我通常将此文件声明为“.txt”文件,并用一些关于生成文件集合以及负责该工具名称的自动生成描述性信息来填充它。

GetDefaultExtension 告诉 Visual Studio 在我们的单个文件名后附加什么扩展名。它可以返回类似“.txt”的内容。

GenerateSummaryContent 返回一个字节数组数据,用于填充该文件的内容。

public abstract string GetDefaultExtension();

public abstract byte[] GenerateSummaryContent();

魔鬼就在细节中

Generate 方法由 Visual Studio 调用,是生成过程的根源。在这里,我们初始化我们的实例变量,遍历我们源代码文件中的元素(由我们的枚举方法定义),创建目标文件,填充它们,将它们添加到解决方案,然后清理任何过时的目标文件,并生成我们的摘要文件。嵌入的注释更清楚地逐步解释了正在发生的事情。

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;
    }
}

使用代码

现在我们已经编写了可扩展的基类,我们可以开始有趣的部分了,即实际提供实现。

作为示例,我将创建一个自定义工具,它以 HTML 文件作为源,检索该文件中的所有图像(<a href=""> 标签),从互联网下载它们,并将它们嵌入到程序集中。我想不出这种工具的任何实际用途,但嘿,这是一个有趣的例子。

[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();
    }
}

然后,您可以执行通常的注册表添加操作来注册自定义工具程序集(记住先将其添加到 GAC),然后针对具有绝对 URL 的图像标签的 HTML 文件激活自定义工具。这将导致从 HTML 链接的所有图像被下载并作为 HTML 节点的子项保存到项目中。

注释

如果您使用的是示例解决方案,我已创建了一个预/后生成事件,该事件会在每次编译时将程序集添加到 GAC(先删除它 - 以刷新任何更改)。解决方案文件夹中还有一个 .reg 文件,它将为您注册自定义工具。

如果您已下载演示 zip 文件,则其中也包含 .reg 文件,但您需要手动将程序集拖放到“c:\windows\assembly”中,以在 GAC 中注册它。

请记住:如果您正在运行 Microsoft Vista,则需要以管理员身份运行 Visual Studio 才能使生成事件正常工作。

注册自定义工具后,启动一个新的 Visual Studio 实例(以便它可能重新加载注册表配置),并将一个 HTML 文件添加到解决方案中(确保 HTML 中的所有 'img' 标签都是绝对引用:即 'http://...')。
然后,在解决方案资源管理器中选中 HTML 文件,并将其“自定义工具”属性(在属性网格中)设置为“HtmlImageEmbedder”(不含引号)。
一旦按下回车键,您应该会在解决方案资源管理器中看到 HTML 文件出现的子节点,文件名与 HTML 中的图像相同。打开其中一些,它们就是图像 - 从网站下载的!

附加代码

可下载的示例项目包含额外的辅助代码,形式为名为 VsHelperstatic 类。

历史

  • 2006.11.25 首次发布
© . All rights reserved.