The solution is to create a Target that adds your files to the Compile ItemGroup rather than adding them explicitly in your .csproj file. That way Intellisense will see them and they will be compiled into your executable, but they will not show up in Visual Studio.
Simple example
You also need to make sure your target is added to the CoreCompileDependsOn
property so it will execute before the compiler runs.
Here is an extremely simple example:
<PropertyGroup>
<CoreCompileDependsOn>$(CoreCompileDependsOn);AddToolOutput</CoreCompileDependsOn>
</PropertyGroup>
<Target Name="AddToolOutput">
<ItemGroup>
<Compile Include="HiddenFile.cs" />
</ItemGroup>
</Target>
If you add this to the bottom of your .csproj file (just before </Project>
), your "HiddenFile.cs" will be included in your compilation even though it doesn't appear in Visual Studio.
Using a separate .targets file
Instead of placing this directly in your .csproj file, you would generally placed it in a separate .targets file surrounded by:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
...
</Project>
and import into your .csproj with <Import Project="MyTool.targets">
. A .targets file is recommended even for one-off cases because it separates your custom code from the stuff in .csproj that is maintained by Visual Studio.
Constructing the generated filename(s)
If you are creating a generalized tool and/or using a separate .targets file, you probably don't want to explicitly list each hidden file. Instead you want to generate the hidden file names from other settings in the project. For example if you want all Resource files to have corresponding tool-generated files in the "obj" directory, your Target would be:
<Target Name="AddToolOutput">
<ItemGroup>
<Compile Include="@(Resource->'$(IntermediateOutputPath)%(FileName)%(Extension).g.cs')" />
</ItemGroup>
</Target>
The "IntermediateOutputPath" property is what we all know as the "obj" directory, but if the end-user of your .targets has customized this your intermediate files will stil be found in the same place. If you prefer your generated files to be in the main project directory and not in the "obj" directory, you can leave this off.
If you want only some of the files of an existing item type to be processed by your custom tool? For example, you may want to generate files for all Page and Resource files with a ".xyz" extension.
<Target Name="AddToolOutput">
<ItemGroup>
<MyToolFiles Include="@(Page);@(Resource)" Condition="'%(Extension)'=='.xyz' />
<Compile Include="@(MyToolFiles->'$(IntermediateOutputPath)%(FileName)%(Extension).g.cs')"/>
</ItemGroup>
</Target>
Note that you can't use the metadata syntax like %(Extension) in a top-level ItemGroup but you can do so within a Target.
Using a custom item type (aka Build Action)
The above processes files that have an existing item type such as Page, Resource, or Compile (Visual Studio calls this the "Build Action"). If your items are a new kind of file you can use your own custom item type. For example if your input files are called "Xyz" files, your project file can define "Xyz" as a valid item type:
<ItemGroup>
<AvailableItemName Include="Xyz" />
</ItemGroup>
after which Visual Studio will allow you to select "Xyz" in the Build Action in the file's properties, resulting in this being added to your .csproj:
<ItemGroup>
<Xyz Include="Something.xyz" />
</ItemGroup>
Now you can use the "Xyz" item type to create the filenames for tool output, just as we did previously with the "Resource" item type:
<Target Name="AddToolOutput">
<ItemGroup>
<Compile Include="@(Xyz->'$(IntermediateOutputPath)%(FileName)%(Extension).g.cs')" />
</ItemGroup>
</Target>
When using a custom item type you can cause your items to also be handled by built-in mechanisms by mapping them to another item type (aka Build Action). This is useful if your "Xyz" files are really .cs files or .xaml or if they need to be made
EmbeddedResources. For example you can cause all files with "Build Action" of Xyz to also be compiled:
<ItemGroup>
<Compile Include="@(Xyz)" />
</ItemGroup>
Or if your "Xyz" source files should be stored as embedded resources, you can express it this way:
<ItemGroup>
<EmbeddedResource Include="@(Xyz)" />
</ItemGroup>
Note that the second example won't work if you put it inside the Target, since the target isn't evaluated until just before the core compile. To make this work inside a Target you have to list the target name in PrepareForBuildDependsOn property instead of CoreCompileDependsOn.
Invoking your custom code generator from MSBuild
Having gone as far as creating a .targets file, you might consider invoking your tool directly from MSBuild rather than using a separate pre-build event or Visual Studio's flawed "Custom Tool" mechanism.
To do this:
- Create a Class Library project with a reference to Microsoft.Build.Framework
- Add the code to implement your custom code generator
- Add a class that implements ITask, and in the Execute method call your custom code generator
- Add a
UsingTask
element to your .targets file, and in your target add a call to your new task
Here is all you need to implement 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
}
}
And here is the UsingTask element and a Target that calls it:
<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>
Note that this target's Output element places the list of output files directly into Compile, so there is no need to use a separate ItemGroup to do this.
How the old "Custom Tool" mechanism is flawed and why not to use it
A note on Visual Studio's "Custom Tool" mechanism: In NET Framework 1.x we didn't have MSBuild, so we had to rely on Visual Studio to build our projects. In order to get Intellisense on generated code, Visual Studio had a mechanism called "Custom Tool" that can be set in the Properties window on a file. The mechanism was fundamentally flawed in several ways, which is why it was replaced with MSBuild targets. Some of the problems with the "Custom Tool" feature were:
- A "Custom Tool" constructs the generated file whenever the file is edited and saved, not when the project is compiled. This means that anything modifying the file externally (such as a revision control system) doesn't update the generated file and you often get stale code in your executable.
- The output of a "Custom Tool" had to be shipped with your source tree unless your recipient also had both Visual Studio and your "Custom Tool".
- The "Custom Tool" had to be installed in the registry and couldn't simply be referenced from the project file.
- The output of the "Custom Tool" is not stored in the "obj" directory.
If you are using the old "Custom Tool" feature, I strongly recommend you switch to using a MSBuild task. It works well with Intellisense and allows you to build your project without even installing Visual Studio (all you need is NET Framework).
When will your custom build task run?
In general your custom build task will run:
- In the background when Visual Studio opens the solution, if the generated file is not up to date
- In the background any time you save one of the input files in Visual Studio
- Any time you build, if the generated file is not up to date
- Any time you rebuild
To be more precise:
- An IntelliSense incremental build is run when Visual Studio starts and every time any file is saved within Visual Studio. This will run your generator if the output file is missing any of the input files are newer than the generator output.
- A regular incremental build is run whenever you use any "Build" or "Run" command in Visual Studio (including the menu options and pressing F5), or when you run "MSBuild" from the command line. Like the IntelliSense incremental build, It will also only run your generator if the generated file is not up to date
- A regular full build is run whenever you use any of the "Rebuild" commands in Visual Studio, or when you run "MSBuild /t:Rebuild" from the command line. It will always run your generator if there are any inputs or outputs.
You may want to force your generator to run at other times, such as when some environment variable changes, or force it to run synchronously rather in the background.
To cause the generator to re-run even when no input files have changed, the best way is usually to add an additional Input to your Target which is a dummy input file stored in the "obj" directory. Then whenever an environment variable or some external setting changes that should force your generator tool to re-run, simply touch this file (ie. create it or update its modified date).
To force the generator to run synchronously rather than waiting for IntelliSense to run it in the background, just use MSBuild to build your particular target. This could be as simple as executing "MSBuild /t:GenerateToolOutput", or VSIP may provide a build-in way to call custom build targets. Alternatively you could simply invoke the Build command and wait for it to complete.
Note that "Input files" in this section refers to whatever is listed in the "Inputs" attribute of the Target element.
Final notes
You may get warnings from Visual Studio that it doesn't know whether to trust your custom tool .targets file. To fix this, add it to the HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\MSBuild\SafeImports registry key.
Here is a summary of what an actual .targets file would look like with all the pieces in place:
<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>
Please let me know if you have any questions or there is anything here you didn't understand.