Решение состоит в том, чтобы создать цель, которая добавит ваши файлы в группу элементов компиляции, а не добавит их явно в файл .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».
Для этого:
- Создание проекта библиотеки классов со ссылкой на Microsoft.Build.Framework
- Добавьте код для реализации вашего собственного генератора кода
- Добавьте класс, который реализует ITask, и в методе Execute вызовите собственный генератор кода
- Добавьте элемент
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» были:
- «Пользовательский инструмент» создает сгенерированный файл всякий раз, когда файл редактируется и сохраняется, а не когда проект компилируется. Это означает, что все, что изменяет файл извне (например, система контроля версий), не обновляет сгенерированный файл, и вы часто получаете устаревший код в вашем исполняемом файле.
- Вывод «Пользовательского инструмента» должен был поставляться с вашим исходным деревом, если только у вашего получателя не было и Visual Studio, и вашего «Пользовательского инструмента».
- «Пользовательский инструмент» должен быть установлен в реестре, и на него нельзя просто ссылаться из файла проекта.
- Вывод "Custom Tool" не сохраняется в каталоге "obj".
Если вы используете старую функцию «Custom Tool», я настоятельно рекомендую вам перейти к использованию задачи MSBuild. Он хорошо работает с Intellisense и позволяет создавать свой проект даже без установки Visual Studio (все, что вам нужно, - это NET Framework).
Когда будет выполняться ваша пользовательская задача сборки?
В общем случае ваша пользовательская задача сборки будет выполняться:
- В фоновом режиме, когда Visual Studio открывает решение, если сгенерированный файл не обновлен
- В фоновом режиме каждый раз, когда вы сохраняете один из входных файлов в Visual Studio
- Каждый раз, когда вы создаете, если сгенерированный файл не обновлен
- Каждый раз, когда вы перестраиваете
Чтобы быть более точным:
- Инкрементная сборка IntelliSense запускается при запуске Visual Studio и каждый раз, когда любой файл сохраняется в Visual Studio. Это запустит ваш генератор, если в выходном файле отсутствует какой-либо из входных файлов новее, чем выход генератора.
- Обычная инкрементная сборка запускается всякий раз, когда вы используете любую команду «Сборка» или «Выполнить» в Visual Studio (включая пункты меню и нажатие клавиши F5), или когда вы запускаете «MSBuild» из командной строки. Как и в случае инкрементальной сборки IntelliSense, он также будет запускать ваш генератор только в том случае, если сгенерированный файл не обновлен
- Обычная полная сборка запускается всякий раз, когда вы используете любую из команд «Перестроить» в 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>
Пожалуйста, дайте мне знать, если у вас есть какие-либо вопросы или есть что-то, чего вы не поняли.