29

I use Visual Studio 2010 to maintain around 40 different web applications that are all housed on the same website upon deployment. These projects are in the same solution but housed in different projects as they each do very different things.

I'm trying to share css/js files among the various projects via the "Add As Link" option that way I can update the css or js files once and automatically have the updates reflected in the various projects without having to build and redeploy each project.

The issue that I am having is when I am trying to run a project locally on my PC for debugging purposes those linked files aren't working. I'm getting a file not found.

My Thought: I assume that this because the folder structure is different when running the applications locally. I'm curious if there is a way to copy the files to the project only when building in debug mode and adjust the relative URLs accordingly so that I will be able to properly test the application before publishing to our web server.

Thanks,

5
  • Just out of curiosity, what does the link look like? I'm guessing if it's literally a direct path, that would be why you can't deal with it locally.
    – IyaTaisho
    Commented Dec 13, 2012 at 19:43
  • @lyaTaisho You are correct it isn't setup as an .NET style link. It uses just a basic relative URL similar to "../../../SharedProjectFolder/css/style.css"
    – Chattah
    Commented Dec 13, 2012 at 19:47
  • Same file structure and all on your local box?
    – IyaTaisho
    Commented Dec 13, 2012 at 20:23
  • @lyaTaisho Yes it's the same structure on my local box.
    – Chattah
    Commented Dec 18, 2012 at 15:13
  • Nice and simple way to solve this: stackoverflow.com/a/17428953/114029 Commented Oct 31, 2014 at 17:13

4 Answers 4

36

The solution to this problem is copying content files (like js, css or others) which are added as link during each build. There are several ways to do this. I can advice to use MSBuild target which can be reused by different web application projects.

So you can create the following file (for example by naming it WebApplication.Extension.targets) with the following content:

<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

    <!-- Override the default target dependencies to -->
    <!-- include the new CopyLinkedContentFiles target. -->
    <PropertyGroup>
        <BuildDependsOn>
            CopyLinkedContentFiles;
            $(BuildDependsOn);
        </BuildDependsOn>
    </PropertyGroup>

    <!--
    ============================================================
    CopyLinkedContentFiles

    A new target to copy any linked content files into the 
    web application output folder.

    NOTE: This is necessary even when '$(OutDir)' has not been redirected.
    ============================================================
    -->
    <Target Name="CopyLinkedContentFiles">
        <!-- Remove any old copies of the files -->
        <Delete Condition=" '%(Content.Link)' != '' AND Exists('$(WebProjectOutputDir)\%(Content.Link)') "
                Files="$(WebProjectOutputDir)\%(Content.Link)" />
        <!-- Copy linked content files recursively to the project folder -->
        <Copy Condition=" '%(Content.Link)' != '' " SourceFiles="%(Content.Identity)"
              DestinationFiles="$(WebProjectOutputDir)\%(Content.Link)" />
    </Target>
</Project>

And then add this target to you web application project by placing the following line in .csproj file:

<Import Project="$(MSBuildProjectDirectory)[RelativePathToFile]\WebApplication.Extension.targets" />

Basically you can add this line in .csproj file after the following one:

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

After this problem with content files added as link should be resolved.

Edit: To execute the following logic only when debug configuration is built in MSBUILD you have ability to specify Condition element.

For example to import specified target only for debug configuration you can update import statement to the following:

<Import Project="$(MSBuildProjectDirectory)[RelativePathToFile]\WebApplication.Extension.targets" Condition=" '$(Configuration)' == 'Debug' "/>

Edit2:

To overcome this problem some time ago I created a 'MSBuild.WebApplication.CopyContentLinkedFiles' nuget package. This package adds MsBuild target which copies all content files added as link to project folder during build.

6
  • 1
    Would doing this only copy the files during the debug process and not during a release build when it will be published to the webserver and relative urls should work? I don't want each project to have it's own copy of the the JS and CSS files. Thanks for the answer.
    – Chattah
    Commented Dec 18, 2012 at 15:12
  • 1
    @Chattah, I update my answer to answer how to make provided solution to be executed only for debug configuration. Please see edit section. Commented Dec 22, 2012 at 14:09
  • This has one caveat. For example, if you link files, add this target, and build a project, content files (e.g. aspx) get copied. If you later delete linked files, the copied files remain since they were never part of the solution. With a website, this results in aspx files remaining and the code-behind no longer exists and thus isn't compiled. When you run aspnet_compiler.exe (for pre-compilation), it will pick up the aspx file, parse it, and complain that it can't load the type associated with the non-existent code-behind. On a build server this is harder to fix. Commented Oct 23, 2013 at 18:47
  • Even if you don't have compile errors, you could be including old linked files that shouldn't be there such as HTML content, etc. One way around that is doing a full clean of files not in source control before building (with TFS: tfpt scorch), but that of course adds to your build time. Commented Oct 23, 2013 at 18:49
  • 2
    @MaximKornilov Love that you answered a post with a NuGet package... that is right up there with OP providing jsbin samples of web issues ;)
    – felickz
    Commented Dec 12, 2014 at 2:57
35

A solution to automatically copy all linked content files to their "virtual" locations at each build using MSBuild is here:

http://mattperdeck.com/post/Copying-linked-content-files-at-each-build-using-MSBuild.aspx

This ensures that your linked files will be available to the browser when you hit F5.

2
  • 1
    This is a great solution that deserves more attention!
    – NS.X.
    Commented Sep 27, 2013 at 20:12
  • 1
    I agree! Very simple fix, and it doesn't seem to affect the TFS Build if you're using TFS for continuous builds.
    – ps2goat
    Commented Oct 22, 2014 at 6:13
7

It can also be solved by pasting the following snippet into the end of your .proj file which will contain the linked files

<Target Name="CopyLinkedContentFiles" BeforeTargets="Build">
<Copy SourceFiles="%(Content.Identity)" 
      DestinationFiles="%(Content.Link)" 
      SkipUnchangedFiles='true' 
      OverwriteReadOnlyFiles='true' 
      Condition="'%(Content.Link)' != ''" />

0

There's one big problem with copying linked files to the virtual location: if you use Go To Definition (Shift + F2) on a JavaScript reference that points to one of the linked files, you'll be taken to the locally-copied file, NOT to the linked file. Then, you're bound to make the mistake of editing the local version, thus eliminating the benefits of using linked files. Additionally, that can cause problems with Intellisense.

A better solution: Instead of copying the linked files to the current project directory alongside the associated links, copy them to a "hiddenDebug" folder within that directory (or any directory you wish; keeping it in the same directory makes it easier to manage, and then to manipulate the paths during debug, as I'll explain below).

Here's how to copy the files from your shared repository to your "hiddenDebug" folder (add to your source project's Post-Build event):

Single CSS File

xcopy /Y "$(ProjectDir)App_Themes\Theme1\Shared.css" "$(SolutionDir)WebApp\App_Themes\Theme1\hiddenDebug\"

JavaScript directory

xcopy /Y /S "$(ProjectDir)Scripts" "$(SolutionDir)WebApp\Scripts\Shared\hiddenDebug\"

When you're debugging, you can dynamically alter the shared files' source paths using Response.Filter in Global.asax. Here's an example:

Response Filter Class (in Shared project)

Imports System.IO

Namespace Code
    Public Class LinkedReferencesFilter
    Inherits MemoryStream

        Private ReadOnly outputStream As Stream = Nothing
        Private ReadOnly _IsDevEnvironment As Boolean = False

        Public Sub New(ByVal output As Stream, IsDevEnvironment As Boolean)
            Me.outputStream = output
            Me._IsDevEnvironment = IsDevEnvironment
        End Sub

        Public Overrides Sub Write(ByVal buffer As Byte(), ByVal offset As Integer, ByVal count As Integer)
            ' Convert the content in buffer to a string
            Dim contentInBuffer As String = UTF8Encoding.UTF8.GetString(buffer)

            If Me._IsDevEnvironment Then
                contentInBuffer = contentInBuffer.Replace("<script src=""Scripts/Shared/", "<script src=""Scripts/Shared/hiddenDebug/")
                contentInBuffer = contentInBuffer.Replace("/Scripts/Shared/", "/Scripts/Shared/hiddenDebug/")
                contentInBuffer = contentInBuffer.Replace("/App_Themes/Theme1/Shared.css", "/App_Themes/Theme1/hiddenDebug/Shared.css")
            End If

            Me.outputStream.Write(UTF8Encoding.UTF8.GetBytes(contentInBuffer), offset, UTF8Encoding.UTF8.GetByteCount(contentInBuffer))
        End Sub
    End Class
End Namespace

Global.asax

Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
    ' Simulate internet latency on local browsing
    If Request.IsLocal Then
        System.Threading.Thread.Sleep(50)
    End If

    Dim currentRelativePath As String = Request.AppRelativeCurrentExecutionFilePath

    If request__1.HttpMethod = "GET" Then
        If currentRelativePath.EndsWith(".aspx") Then
            Dim IsDevEnvironment As Boolean = False
            //Use whatever method you want to determine whether your current environment is a development environment:
            #If CONFIG = "Develop" Then
                IsDevEnvironment = True
            #End If

            Response.Filter =
                New Shared.Code.LinkedReferencesFilter(
                    output:=Response.Filter,
                    IsDevEnvironment:=IsDevEnvironment)
        End If
    End If
End Sub

Troubleshooting: Try unloading and reloading the project with linked items. If that doesn't help, add the hiddenDebug directory to your project. I had to do that, but then I was able to remove it later. It's finicky...it would be nice if Microsoft polished this feature, but I'm set for now.

In case you didn't know: when you publish your web application, the source (linked) files are automatically copied to the publish target. Once this is set up, you can forget about it. The best part is, you won't lose Intellisense or quick navigation.

After migrating to TypeScript, most JavaScript file reference challenges will be simplified or eliminated (I'm hoping for easy cross-project referencing like what's available for other managed .NET languages, but there may already be some workarounds to enable such functionality).

Please let me know whether it works for you, or if you have a better way.

1
  • 1
    Good point to bring up. I think for most people they are linking asset files from nuget packages which should never be modified, and if they must be modified they should be overridden in a separate file. The approach mentioned above removes the clutter from code check in so developers only need to worry about the code they maintain.
    – spyder1329
    Commented Apr 21, 2020 at 2:59

Not the answer you're looking for? Browse other questions tagged or ask your own question.