Как скрыть файлы, сгенерированные пользовательским инструментом в Visual Studio - PullRequest
27 голосов
/ 03 июня 2010

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

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

Ответы [ 4 ]

59 голосов
/ 19 июня 2010

Решение состоит в том, чтобы создать цель, которая добавит ваши файлы в группу элементов компиляции, а не добавит их явно в файл .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» в действии сборки в свойствах файла, в результате чего он будет добавлен в ваш .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>

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

15 голосов
/ 22 июня 2010

Чтобы скрыть элементы из 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>
0 голосов
/ 18 июня 2010

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

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

0 голосов
/ 14 июня 2010

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

Например:

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...