如何在Visual Studio中隐藏自定义工具生成的文件

35
我希望我的定制工具生成的文件被隐藏,但我找不到任何关于如何做到这一点的文档。
一个我正在寻找的例子是WPF代码后台文件。这些文件在Visual Studio项目视图中不显示,但与项目一起编译,并在IntelliSense中提供。 WPF代码后台文件(例如Window1.g.i.cs)由自定义工具生成。

你将生成的文件保存在哪里(相对于源文件)? - luke
输出目录与输入目录相同。 - jaws
当你说WPF代码后台文件是隐藏的时,你是什么意思?如果我创建一个WPF应用程序,我会得到一个名为MainWindow.xaml的文件,它可以展开以显示我认为是代码后台文件的MainWindow.xaml.cs。 - ErikHeemskerk
有一个隐藏文件是自动生成的。在构建项目时查看输出窗口。 - jaws
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>之前),即使在Visual Studio中没有显示,您的“HiddenFile.cs”也会被包含在编译中。 使用单独的.targets文件 与其直接放置在.csproj文件中,通常应将其放置在单独的.targets文件中,并用以下代码括起来:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  ...
</Project>

将其导入到您的.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将其称为“Build Action”)的文件。如果您的项目是一种新的文件类型,则可以使用自己的自定义项类型。例如,如果您的输入文件被称为“Xyz”文件,则您的项目文件可以将“Xyz”定义为有效的项类型:
<ItemGroup>
  <AvailableItemName Include="Xyz" />
</ItemGroup>

之后,Visual Studio就会允许你在文件属性的“构建操作”中选择“Xyz”,这将添加以下内容到你的.csproj文件:

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

现在您可以使用“Xyz”项目类型来创建工具输出的文件名,就像我们以前使用“Resource”项目类型一样:
<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)。
通常情况下,您的自定义生成任务将运行:
1.在Visual Studio打开解决方案时后台运行,如果生成的文件不是最新的。
2.在Visual Studio中保存任何输入文件时后台运行。
3.每当您构建时,如果生成的文件不是最新的。
4.每当您重新构建时。
更精确地说:
1.Visual Studio启动时会运行IntelliSense增量构建,并在Visual Studio中保存任何文件时运行。如果输出文件缺少任何输入文件或输入文件比生成器输出更新,则会运行生成器。
2.每当您在Visual Studio中使用任何“构建”或“运行”命令(包括菜单选项和按F5键)或从命令行运行“MSBuild”时,都会运行常规增量构建。与IntelliSense增量构建一样,如果生成的文件不是最新的,则也只会运行生成器。
3.每当您在Visual Studio中使用任何“重新构建”命令或从命令行运行“MSBuild /t:Rebuild”时,都会运行常规完整构建。如果有任何输入或输出,则始终会运行生成器。

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

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

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

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

最后说明

您可能会收到来自 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>

如果您有任何问题或对此处的内容不理解,请告诉我。


感谢您详细的回复。只有一个问题:使用自定义工具方法,我可以在我的VSIP中以编程方式触发生成文件的构建。使用您的方法,我能否做类似的事情? - jaws
顺便提一下,对于VS2010,请使用HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\10.0\MSBuild\SafeImports - devstuff
1
雷,Visual Studio 抱怨它无法识别你的最终 .targets 文件中的“ItemGroup”属性。示例是否正确? - Jack Wester
@RayBurns 我也遇到了这个问题 - "元素<Output>中的属性"ItemGroup"无法识别"。有人知道该怎么办吗? - Ani
2
非常好的答案。不过需要做出一些更正:在<Output>上使用ItemName代替ItemGroup属性,从任务的Execute()方法返回true,并在OutputFiles字段上添加[Output]属性。 - Shay Rojansky
显示剩余2条评论

20
为了在Visual Studio中隐藏项目,需要对该项目添加一个名为Visible的元数据属性。据说InProject元数据也可以实现同样的效果。
Visible:http://msdn.microsoft.com/en-us/library/ms171468(VS.90).aspx InProject:Link
<ItemGroup>
  <Compile Include="$(AssemblyInfoPath)">
    <!-- either: -->
    <InProject>false</InProject>
    <!-- or: -->
    <Visible>false</Visible>
  </Compile>
</ItemGroup>

这比得到更多投票并完美运行的被接受答案容易得多。 - Livven

0

我知道的唯一方法是将生成的文件添加为依赖项,以便在proj文件中隐藏想要隐藏的文件。

例如:

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

如果你移除了DependentUpon元素,那么文件将会显示在其他文件旁边而不是后面...你的生成器是如何添加这些文件的?你能简要介绍一下使用情况以及你希望它如何工作吗?


我正在尝试完全隐藏生成的文件。根据定义,由自定义工具生成的文件依赖于自定义工具,并且Visual Studio管理它们的生成。 - jaws

0

这个文件需要在设计时存在。我需要它包含的类可以通过智能感知对用户可用,就像WPF代码后台一样。 - jaws

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接