35

我希望隐藏我的自定义工具生成的文件,但我找不到任何有关如何完成此操作的文档。

我正在寻找的一个例子是文件背后的 WPF 代码。这些文件不会显示在 Visual Studio 项目视图中,但会与项目一起编译并在 IntelliSense 中可用。WPF 代码隐藏文件(例如 Window1.gics)由自定义工具生成。

4

4 回答 4

69

解决方案是创建一个 Target,将您的文件添加到 Compile ItemGroup,而不是将它们显式添加到您的 .csproj 文件中。这样,Intellisense 将看到它们并将它们编译到您的可执行文件中,但它们不会显示在 Visual Studio 中。

简单的例子

您还需要确保将目标添加到CoreCompileDependsOn属性中,以便在编译器运行之前执行它。

这是一个非常简单的例子:

<PropertyGroup>
  <CoreCompileDependsOn>$(CoreCompileDependsOn);AddToolOutput</CoreCompileDependsOn>
</PropertyGroup>

<Target Name="AddToolOutput">
  <ItemGroup>
    <Compile Include="HiddenFile.cs" />
  </ItemGroup>
</Target>

如果您将它添加到 .csproj 文件的底部(就在 之前</Project>),您的“HiddenFile.cs”将包含在您的编译中,即使它没有出现在 Visual Studio 中。

使用单独的 .targets 文件

与其将它直接放在您的 .csproj 文件中,您通常会将其放在一个单独的 .targets 文件中,该文件由以下内容包围:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  ...
</Project>

并使用 .csproj 导入您的 .csproj <Import Project="MyTool.targets">。即使是一次性使用 .targets 文件也是推荐的,因为它将您的自定义代码与 Visual Studio 维护的 .csproj 中的内容分开。

构造生成的文件名

如果您正在创建通用工具和/或使用单独的 .targets 文件,您可能不想明确列出每个隐藏文件。相反,您希望从项目中的其他设置生成隐藏文件名。例如,如果您希望所有资源文件在“obj”目录中都有相应的工具生成文件,那么您的目标将是:

<Target Name="AddToolOutput">
  <ItemGroup>
    <Compile Include="@(Resource->'$(IntermediateOutputPath)%(FileName)%(Extension).g.cs')" />
  </ItemGroup>
</Target>

“IntermediateOutputPath”属性是我们都知道的“obj”目录,但是如果您的 .targets 的最终用户自定义了它,您的中间文件仍然会在同一个地方找到。如果您希望生成的文件位于主项目目录中而不是“obj”目录中,则可以将其关闭。

如果您只希望自定义工具处理现有项目类型的某些文件?例如,您可能希望为所有带有“.xyz”扩展名的页面和资源文件生成文件。

<Target Name="AddToolOutput">
  <ItemGroup>
    <MyToolFiles Include="@(Page);@(Resource)" Condition="'%(Extension)'=='.xyz' />
    <Compile Include="@(MyToolFiles->'$(IntermediateOutputPath)%(FileName)%(Extension).g.cs')"/>
  </ItemGroup>
</Target>

请注意,您不能在顶级 ItemGroup 中使用像 %(Extension) 这样的元数据语法,但可以在 Target 中这样做。

使用自定义项目类型(又名构建操作)

上述处理具有现有项目类型的文件,例如 Page、Resource 或 Compile(Visual Studio 将此称为“构建操作”)。如果您的项目是一种新类型的文件,您可以使用您自己的自定义项目类型。例如,如果您的输入文件被称为“Xyz”文件,您的项目文件可以将“Xyz”定义为有效的项目类型:

<ItemGroup>
  <AvailableItemName Include="Xyz" />
</ItemGroup>

之后,Visual Studio 将允许您在文件属性的构建操作中选择“Xyz”,从而将其添加到您的 .csproj 中:

<ItemGroup>
  <Xyz Include="Something.xyz" />
</ItemGroup>

现在您可以使用“Xyz”项目类型来创建工具输出的文件名,就像我们之前使用“资源”项目类型所做的那样:

<Target Name="AddToolOutput">
  <ItemGroup>
    <Compile Include="@(Xyz->'$(IntermediateOutputPath)%(FileName)%(Extension).g.cs')" />
  </ItemGroup>
</Target>

使用自定义项目类型时,您可以通过将项目映射到另一个项目类型(也称为构建操作)来使您的项目也由内置机制处理。如果您的“Xyz”文件确实是 .cs 文件或 .xaml 或者需要制作它们,这将很有用

嵌入式资源。例如,您可以编译所有带有 Xyz 的“构建操作”的文件:

<ItemGroup>
  <Compile Include="@(Xyz)" />
</ItemGroup>

或者,如果您的“Xyz”源文件应该存储为嵌入式资源,您可以这样表达:

<ItemGroup>
  <EmbeddedResource Include="@(Xyz)" />
</ItemGroup>

请注意,如果将第二个示例放在 Target 中,则第二个示例将不起作用,因为直到核心编译之前才会评估目标。要在 Target 中完成这项工作,您必须在 PrepareForBuildDependsOn 属性中列出目标名称,而不是 CoreCompileDependsOn。

从 MSBuild 调用自定义代码生成器

在创建 .targets 文件之后,您可能会考虑直接从 MSBuild 调用您的工具,而不是使用单独的预构建事件或 Visual Studio 有缺陷的“自定义工具”机制。

去做这个:

  1. 创建一个引用 Microsoft.Build.Framework 的类库项目
  2. 添加代码以实现您的自定义代码生成器
  3. 添加一个实现 ITask 的类,并在 Execute 方法中调用您的自定义代码生成器
  4. 向您的 .targets 文件添加一个UsingTask元素,并在您的目标中添加对新任务的调用

以下是实现 ITask 所需的全部内容:

public class GenerateCodeFromXyzFiles : ITask
{
  public IBuildEngine BuildEngine { get; set; }
  public ITaskHost HostObject { get; set; }

  public ITaskItem[] InputFiles { get; set; }
  public ITaskItem[] OutputFiles { get; set; }

  public bool Execute()
  {
    for(int i=0; i<InputFiles.Length; i++)
      File.WriteAllText(OutputFiles[i].ItemSpec,
        ProcessXyzFile(
          File.ReadAllText(InputFiles[i].ItemSpec)));
  }

  private string ProcessXyzFile(string xyzFileContents)
  {
    // Process file and return generated code
  }
}

这是 UsingTask 元素和调用它的 Target:

<UsingTask TaskName="MyNamespace.GenerateCodeFromXyzFiles" AssemblyFile="MyTaskProject.dll" />


<Target Name="GenerateToolOutput">

  <GenerateCodeFromXyzFiles
      InputFiles="@(Xyz)"
      OutputFiles="@(Xyz->'$(IntermediateOutputPath)%(FileName)%(Extension).g.cs')">

    <Output TaskParameter="OutputFiles" ItemGroup="Compile" />

  </GenerateCodeFromXyzFiles>
</Target>

请注意,此目标的 Output 元素将输出文件列表直接放入 Compile,因此无需使用单独的 ItemGroup 来执行此操作。

旧的“自定义工具”机制如何存在缺陷以及为什么不使用它

关于 Visual Studio 的“自定义工具”机制的说明:在 NET Framework 1.x 中,我们没有 MSBuild,因此我们不得不依赖 Visual Studio 来构建我们的项目。为了在生成的代码上获得 Intellisense,Visual Studio 有一种称为“自定义工具”的机制,可以在文件的“属性”窗口中进行设置。该机制在几个方面存在根本缺陷,这就是它被 MSBuild 目标取代的原因。“自定义工具”功能的一些问题是:

  1. 每当编辑和保存文件时,“自定义工具”都会构建生成的文件,而不是在编译项目时。这意味着从外部修改文件的任何内容(例如修订控制系统)都不会更新生成的文件,并且您经常会在可执行文件中获得陈旧的代码。
  2. “自定义工具”的输出必须与您的源代码树一起提供,除非您的收件人同时拥有 Visual Studio 和您的“自定义工具”。
  3. “自定义工具”必须安装在注册表中,不能简单地从项目文件中引用。
  4. “自定义工具”的输出不存储在“obj”目录中。

如果您使用的是旧的“自定义工具”功能,我强烈建议您切换到使用 MSBuild 任务。它可以很好地与 Intellisense 配合使用,甚至可以让您在不安装 Visual Studio 的情况下构建您的项目(您只需要 NET Framework)。

您的自定义构建任务何时运行?

通常,您的自定义构建任务将运行:

  • 在 Visual Studio 打开解决方案的后台,如果生成的文件不是最新的
  • 随时在后台保存 Visual Studio 中的输入文件之一
  • 任何时候构建,如果生成的文件不是最新的
  • 任何时候重建

更准确地说:

  1. 当 Visual Studio 启动并且每次在 Visual Studio 中保存任何文件时,都会运行 IntelliSense 增量构建。如果输出文件丢失任何输入文件比生成器输出新,这将运行您的生成器。
  2. 每当您在 Visual Studio 中使用任何“构建”或“运行”命令(包括菜单选项并按 F5)或从命令行运行“MSBuild”时,都会运行常规增量构建。与 IntelliSense 增量构建一样,如果生成的文件不是最新的,它也只会运行您的生成器
  3. 每当您在 Visual Studio 中使用任何“重建”命令或从命令行运行“MSBuild /t:Rebuild”时,都会运行常规完整构建。如果有任何输入或输出,它将始终运行您的生成器。

您可能希望强制生成器在其他时间运行,例如当某些环境变量更改时,或者强制它同步运行而不是在后台运行。

  • 为了使生成器即使在没有更改输入文件的情况下也能重新运行,最好的方法通常是向目标添加一个额外的输入,这是一个存储在“obj”目录中的虚拟输入文件。然后,每当环境变量或某些外部设置发生更改,迫使您的生成器工具重新运行时,只需触摸此文件(即创建它或更新其修改日期)。

  • 要强制生成器同步运行,而不是等待 IntelliSense 在后台运行,只需使用 MSBuild 构建您的特定目标。这可以像执行“MSBuild /t:GenerateToolOutput”一样简单,或者 VSIP 可以提供一种内置方式来调用自定义构建目标。或者,您可以简单地调用 Build 命令并等待它完成。

请注意,本节中的“输入文件”指的是目标元素的“输入”属性中列出的任何内容。

最后的笔记

你可能会收到来自 Visual Studio 的警告,它不知道是否信任你的自定义工具 .targets 文件。要解决此问题,请将其添加到 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\MSBuild\SafeImports 注册表项。

下面总结了一个实际的 .targets 文件在所有部分都到位后的样子:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <PropertyGroup>
    <CoreCompileDependsOn>$(CoreCompileDependsOn);GenerateToolOutput</CoreCompileDependsOn>
  </PropertyGroup>

  <UsingTask TaskName="MyNamespace.GenerateCodeFromXyzFiles" AssemblyFile="MyTaskProject.dll" />


  <Target Name="GenerateToolOutput" Inputs="@(Xyz)" Outputs="@(Xyz->'$(IntermediateOutputPath)%(FileName)%(Extension).g.cs')">

    <GenerateCodeFromXyzFiles
        InputFiles="@(Xyz)"
        OutputFiles="@(Xyz->'$(IntermediateOutputPath)%(FileName)%(Extension).g.cs')">

      <Output TaskParameter="OutputFiles" ItemGroup="Compile" />

    </GenerateCodeFromXyzFiles>
  </Target>

</Project>

如果您有任何问题或这里有任何您不明白的地方,请告诉我。

于 2010-06-19T04:31:29.743 回答
20

若要在 Visual Studio 中隐藏项目,请向项目添加Visible元数据属性。InProject元数据显然也是这样做的。

可见: http: //msdn.microsoft.com/en-us/library/ms171468 (VS.90).aspx

项目内:http : //blogs.msdn.com/b/jomo_fisher/archive/2005/01/25/360302.aspx

<ItemGroup>
  <Compile Include="$(AssemblyInfoPath)">
    <!-- either: -->
    <InProject>false</InProject>
    <!-- or: -->
    <Visible>false</Visible>
  </Compile>
</ItemGroup>
于 2010-06-22T03:56:38.647 回答
0

我知道这样做的唯一方法是添加生成的文件以依赖于您希望它隐藏在后面的文件 - 在 proj 文件中。

例如:

 <ItemGroup>
    <Compile Include="test.cs" />
    <Compile Include="test.g.i.cs">
      <DependentUpon>test.cs</DependentUpon>
    </Compile>
  </ItemGroup>

如果您删除了 DependentUpon 元素,则该文件显示在另一个文件旁边而不是在其后面......您的生成器如何添加文件?您能告诉我们用例以及您希望它如何工作吗?

于 2010-06-14T00:36:44.463 回答
0

我想你想看看这里:http: //msdn.microsoft.com/en-us/library/ms171453.aspx

具体来说,“在执行期间创建项目”部分。

于 2010-06-18T18:16:40.847 回答