Search code examples
c#visual-studio-2010build-events

Visual Studio Linked Files don't exist


In Visual Studio, you can do Add -> Existing Item and then Add as Link from the Add drop down button.

This is great. This let's you add a file from another project, and editing the file also edits it in the original project.

I would like to use this feature to have a config file (named Shared.config) be present in all projects within one solution. And have that file always be the same.

solution
|
|- project 1
|- Shared.config [physical]
|- project 2
|- Shared.config [linked]

After publishing, the file indeed ends up in all published projects, so no problem there.

But BEFORE publishing (during development on build) the linked file doesn't really exist. Trying to see the file in the Windows Explorer proves that the file is not in the project directory. Visual Studio only makes it look as if it exists there in the solution explorer. (Though on build, linked items are probably copied to the bin directory; But I don't want to use/access files from the bin directory.)

Now this gives problems off course. Trying to print out System.IO.File.ReadAllText(HttpContext.Current.Server.MapPath("Shared.config")) will fail before the project has been published, because of the fact that Shared.config doesn't exist in the project root directory yet.

What I would like to do, and where I need your help is:

  • I would like to ON BUILD copy all linked files from their original location to their target location.

This will make visual studio have the linked file, and a copy of the original, to exists both in the same directory with the same name.

Normally , VS won't allow you to create a linked item in a directory if that directory already contains a file with the same name.

But, I have tested by creating a linked item first; then using Windows Explorer to copy the original file to the destination directory, and see Visual Studio act ok. The solution explorer simply hides the physical file, and shows the linked item in stead. (Even if you click Show all files in the solution explorer.)

solution
|
|- project 1
|- Shared.config [physical]
|- project 2
|- Shared.config [linked]
|- Shared.config [physical, copied here during build, invisible to Solution explorer]

This is exactly what I want! When trying to edit the file, Visual Studio will open the 'linked item'. And on build, a physical file will be copied to the target directory so it exists for the code that tries to access it.

Now how do I do this? Should this be done with Build events? If so how do I say 'copy originals of all linked files to their destination directory?


Solution

  • You should probably use MSBuild features to implement this.

    Edit the csproj file (in Visual Studio, right click the project and unload it. then right click and edit)

    Scroll to the bottom and you should find this line.

    <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

    Immediately after that line, add these lines.

      <ItemGroup>
        <LinkedItem Include="@(None)" Condition="'%Link' != ''" />
      </ItemGroup>
      <Target Name="CopyLinkedFiles" BeforeTargets="Build" Inputs="@(LinkedItem)" Outputs="@(LinkedItem->'%(Filename)%(Extension)')">
        <Copy SourceFiles="@(LinkedItem)" DestinationFolder="$(MSBuildProjectDirectory)" />
      </Target>
    

    Now every time you build, right before the build action occurs, MSBuild will copy all linked files.

    Explanation

    ItemGroup contains my "array" named "LinkedItem". I generate this array by adding only the "None" items that contain a link property.

    Target is an MSBuild concept. You can think of it as a particular phase of the build. I named this phase "CopyLinkedFiles" but you can name it anything.

    BeforeTargets is a directive that tells MSBuild to run the action before the specified phase. Here, I have chosen to run "CopyLinkedFiles" before the "Build" phase.

    Inputs is an optimization parameter. It is used to speed up building by skipping the copy if not necessary. You can ignore this parameter if you don't care. MSBuild compares the Inputs to the expected Outputs timestamp to see if it needs to execute.

    Copy is an MSBuild task that accepts a file to copy and outputs to the specified folder.

    Reducing Redundancy

    You could paste this into every .csproj file, or you could put it in a central .proj and embed that into the csproj files. Sadly, no matter what you do, you will have to edit every .csproj at least 1 time. :(

    Create a file in the Common project call WhateverNameYouLike.proj Put these contents into the file.

    <?xml version="1.0" encoding="utf-8"?>
    <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
      <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
      <!-- paste the previously shown code here -->
    
      <!-- you can save yourself some hassle by linking the config file here, but if you really enjoy adding the file as a link to every project, you can skip this line -->
      <None Include="..\Common\Shared.config">
        <Link>Shared.config</Link>
      </None>
    </Project>
    

    Now the annoying part: In every .csproj, you will have to add a line like <Import Project="..\Common\WhateverNameYouLike.proj" /> probably at the end just before the </Project> closing tag.