Как скрыть файлы, сгенерированные пользовательским инструментом в Visual Studio

Я хотел бы, чтобы файлы, созданные моим пользовательским инструментом, были скрыты, но я не могу найти документацию о том, как это делается.

Примером того, что я ищу, является код WPF за файлами. Эти файлы не отображаются в представлении проекта Visual Studio, но скомпилированы с проектом и доступны в IntelliSense. Код WPF за файлами (например, Window1.gics) генерируется с помощью специального инструмента.

4 ответа

Решение

Решение состоит в том, чтобы создать цель, которая добавляет ваши файлы в группу элементов компиляции, а не добавляет их явно в файл.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 с <Import Project="MyTool.targets">, Файл.targets рекомендуется даже для одноразовых случаев, поскольку он отделяет ваш пользовательский код от содержимого в.csproj, которое поддерживается Visual Studio.

Построение сгенерированных имен файлов

Если вы создаете обобщенный инструмент и / или используете отдельный файл.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>

Обратите внимание, что вы не можете использовать синтаксис метаданных, такой как%(Extension) в ItemGroup верхнего уровня, но вы можете сделать это внутри Target.

Использование пользовательского типа элемента (иначе называемого действия сборки)

Выше обрабатываются файлы, которые имеют существующий тип элемента, такой как Page, Resource или Compile (Visual Studio называет это "действием сборки"). Если ваши элементы представляют собой новый тип файла, вы можете использовать свой собственный тип элемента. Например, если ваши входные файлы называются "Xyz", ваш файл проекта может определить "Xyz" как допустимый тип элемента:

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

после чего Visual Studio позволит вам выбрать "Xyz" в Build Action в свойствах файла, в результате чего он будет добавлен в ваш.csproj:

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

Теперь вы можете использовать тип элемента "Xyz" для создания имен файлов для вывода инструмента, как мы делали это ранее с типом элемента "Ресурс":

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

При использовании пользовательского типа элемента вы можете также заставить свои элементы обрабатываться встроенными механизмами, сопоставляя их с другим типом элемента (иначе называемый "Build Action"). Это полезно, если ваши файлы "Xyz" на самом деле являются файлами.cs или.xaml или если их нужно сделать

EmbeddedResources. Например, вы можете заставить все файлы с "Build Action" Xyz также быть скомпилированными:

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

Или, если ваши исходные файлы "Xyz" должны храниться как встроенные ресурсы, вы можете выразить это следующим образом:

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

Обратите внимание, что второй пример не будет работать, если вы поместите его в Target, так как цель не оценивается, пока не будет скомпилировано ядро. Чтобы это работало внутри цели, вы должны указать имя цели в свойстве PrepareForBuildDependsOn вместо CoreCompileDependsOn.

Вызов вашего собственного генератора кода из MSBuild

Дойдя до создания файла.targets, вы можете рассмотреть возможность вызова вашего инструмента непосредственно из MSBuild, а не использовать отдельное событие перед сборкой или некорректный механизм Visual Studio "Custom Tool".

Сделать это:

  1. Создайте проект библиотеки классов со ссылкой на Microsoft.Build.Framework
  2. Добавьте код для реализации вашего собственного генератора кода
  3. Добавьте класс, который реализует ITask, и в методе Execute вызовите свой собственный генератор кода
  4. Добавить UsingTask элемент в ваш файл.targets, и в вашей цели добавьте вызов к вашей новой задаче

Вот все, что вам нужно для реализации 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 и цель, которая его вызывает:

<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 для этого.

Как старый механизм "Custom Tool" имеет недостатки и почему бы не использовать его

Примечание о механизме "Custom Tool" в Visual Studio: в NET Framework 1.x у нас не было MSBuild, поэтому для построения наших проектов нам пришлось полагаться на Visual Studio. Чтобы получить Intellisense для сгенерированного кода, в Visual Studio был механизм под названием "Пользовательский инструмент", который можно установить в окне свойств файла. Механизм был в корне ошибочным по нескольким причинам, поэтому его заменили цели MSBuild. Некоторые проблемы с функцией "Custom Tool" были:

  1. "Пользовательский инструмент" создает сгенерированный файл всякий раз, когда файл редактируется и сохраняется, а не когда проект компилируется. Это означает, что все, что изменяет файл извне (например, система контроля версий), не обновляет сгенерированный файл, и вы часто получаете устаревший код в вашем исполняемом файле.
  2. Вывод "Пользовательского инструмента" должен был поставляться с вашим исходным деревом, если только у вашего получателя не было и Visual Studio, и вашего "Пользовательского инструмента".
  3. "Пользовательский инструмент" должен был быть установлен в реестре, и на него нельзя было просто ссылаться из файла проекта.
  4. Вывод "Custom Tool" не сохраняется в каталоге "obj".

Если вы используете старую функцию "Custom Tool", я настоятельно рекомендую вам перейти к использованию задачи MSBuild. Он хорошо работает с Intellisense и позволяет создавать свой проект даже без установки Visual Studio (все, что вам нужно, - это NET Framework).

Когда будет выполняться ваша пользовательская задача сборки?

В общем случае ваша пользовательская задача сборки будет выполняться:

  • В фоновом режиме, когда Visual Studio открывает решение, если сгенерированный файл не обновлен
  • В фоновом режиме каждый раз, когда вы сохраняете один из входных файлов в Visual Studio
  • Каждый раз, когда вы создаете, если сгенерированный файл не обновлен
  • Каждый раз, когда вы восстанавливаете

Чтобы быть более точным:

  1. Инкрементная сборка IntelliSense запускается при запуске Visual Studio и каждый раз, когда любой файл сохраняется в Visual Studio. Это запустит ваш генератор, если в выходном файле отсутствует какой-либо из входных файлов новее, чем выходной файл генератора.
  2. Обычная инкрементная сборка запускается всякий раз, когда вы используете любую команду "Сборка" или "Выполнить" в Visual Studio (включая пункты меню и нажатие клавиши F5), или когда вы запускаете "MSBuild" из командной строки. Как и в случае инкрементальной сборки IntelliSense, он также будет запускать ваш генератор, только если сгенерированный файл не обновлен
  3. Обычная полная сборка запускается всякий раз, когда вы используете любую из команд "Перестроить" в Visual Studio, или когда вы запускаете "MSBuild /t:Rebuild" из командной строки. Он всегда будет запускать ваш генератор, если есть какие-либо входы или выходы.

Возможно, вы захотите заставить ваш генератор работать в другое время, например, при изменении какой-либо переменной среды, или заставить его работать синхронно, а не в фоновом режиме.

  • Чтобы генератор перезапустился, даже если никакие входные файлы не изменились, обычно лучше всего добавить дополнительный вход в вашу цель, который является фиктивным входным файлом, хранящимся в каталоге "obj". Затем, когда переменная окружения или какой-либо внешний параметр изменяется, что должно заставить ваш генераторный инструмент перезапуститься, просто коснитесь этого файла (т.е. создайте его или обновите дату его изменения).

  • Чтобы заставить генератор работать синхронно, а не ждать, пока IntelliSense запустит его в фоновом режиме, просто используйте MSBuild для построения вашей конкретной цели. Это может быть так же просто, как выполнить "MSBuild /t:GenerateToolOutput", или VSIP может предоставить встроенный способ вызова пользовательских целей сборки. В качестве альтернативы вы можете просто вызвать команду Build и дождаться ее завершения.

Обратите внимание, что "Входные файлы" в этом разделе относится к тому, что указано в атрибуте "Входные данные" элемента Target.

Финальные заметки

Вы можете получить предупреждение от 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>

Пожалуйста, дайте мне знать, если у вас есть какие-либо вопросы или есть что-то, что вы не поняли.

Чтобы скрыть элементы из Visual Studio, добавьте Visible свойство метаданных для элемента. InProject метаданные, очевидно, тоже это делают.

Видимый: http://msdn.microsoft.com/en-us/library/ms171468(VS.90).aspx

InProject: 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>

Единственный известный мне способ сделать это - добавить сгенерированный файл, чтобы иметь зависимость от файла, за которым вы хотите его скрыть, - в файле proj.

Например:

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

Если вы удалили элемент DependentUpon, то этот файл отображается рядом с другим файлом, а не позади него... как ваш генератор добавляет файлы? Можете ли вы рассказать нам об этом сценарии использования и о том, как бы вы хотели, чтобы он работал?

Я думаю, что вы хотите посмотреть здесь: http://msdn.microsoft.com/en-us/library/ms171453.aspx.

В частности, раздел "Создание элементов во время выполнения".

Другие вопросы по тегам