How to hide files generated by custom tool in Visual Studio
Asked Answered
D

4

35

I would like the files generated by my custom tool to be hidden, but I cannot find any documentation on how this is done.

An example of what I'm looking for is WPF code behind files. These files are not displayed in the Visual Studio project view, yet are compiled with the project and are available in IntelliSense. WPF code behind files (Window1.g.i.cs, for example), are generated by a custom tool.

Dictatorial answered 2/6, 2010 at 21:30 Comment(4)
where (relative to the source files) are you saving the generated files?Orlantha
The output directory is the same as the input directory.Dictatorial
What do you mean when you say WPF code-behind files are hidden? If I create a WPF application I get a file named MainWindow.xaml, which can be expanded to show what I believe is the code-behind file, MainWindow.xaml.cs.Fluker
There is a hidden file that is auto generated. Look at the output window when you build the project.Dictatorial
M
69

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:

  1. Create a Class Library project with a reference to Microsoft.Build.Framework
  2. Add the code to implement your custom code generator
  3. Add a class that implements ITask, and in the Execute method call your custom code generator
  4. 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:

  1. 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.
  2. 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".
  3. The "Custom Tool" had to be installed in the registry and couldn't simply be referenced from the project file.
  4. 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:

  1. 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.
  2. 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
  3. 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.

Metacenter answered 19/6, 2010 at 4:31 Comment(7)
Thank you for the detailed response. Just one question: The custom tool approach allowed me to programmatically trigger the build of the generated file, from within my VSIP. Will I be able to do something similar with your approach?Dictatorial
Yes, there are several ways to control when your custom build task runs. I've added a new "When will your custom build task run?" section to my answer that explains how this works and gives some options.Metacenter
BTW, for VS2010 use HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\10.0\MSBuild\SafeImports instead.Switch
Ray, Visual Studio complains that it does not recognize the 'ItemGroup' attribute in your final .targets file. Is the example correct?Pittance
@RayBurns I'm seeing this problem too - "The attribute "ItemGroup" in element <Output> is unrecognized". Anyone know what to do?Vincents
Really great answer. A few corrections though... Use the ItemName instead of ItemGroup attribute on <Output>, return true from the tasks' Execute() method, and add the [Output] attribute on the OutputFiles fieldCreamery
Interestingly, the "old custom tool" things you mention are perfectly what we want (especially the "run tool on save, not on build"). Except we would like to hide the content of the generated file. So is there any way of hiding the output that is added to the csproj via the "Custom Tool" property?Dulia
S
20

To hide items from Visual Studio add a Visible metadata property to the item. The InProject metadata apparently does this too.

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>
Switch answered 22/6, 2010 at 3:56 Comment(1)
This is so much easier than the accepted answer that has more votes and works perfectly.Latvia
P
0

The only way I know to do it is to add the generated file to have a dependency on the file you want it hidden behind - in the proj file.

For example:

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

If you removed the DependentUpon element then the file shows up beside the other file instead of behind it ... how does your generator add the files? can you walk us through the use case and how you would like it to work?

Pep answered 14/6, 2010 at 0:36 Comment(1)
I am trying to hide the generated file entirely. By definition, the files generated by custom tools are dependent upon the custom tool, and Visual Studio manages their generation.Dictatorial
R
0

I think you want to look here: http://msdn.microsoft.com/en-us/library/ms171453.aspx.

Specifically, the "Creating Items During Execution" section.

Royceroyd answered 18/6, 2010 at 18:16 Comment(1)
This file needs to be around at design-time. I need the classes it contains available to the user via intellisense, just as WPF code behind is.Dictatorial

© 2022 - 2024 — McMap. All rights reserved.