Project Override and Reload

Feb 20, 2012 at 10:00 AM


I've tried to find an answer for this on here and places like StackOverflow but so far no luck, so I was hoping somebody would be able to help me out!

I've created a NuGet package that enables MSBuild tasks for CodeAnalysis, StyleCop, JSHint and CSSLint so that our projects are standardised and highlight errors as soon as we build.  This works great but the problem I have is that whenever I install this in to a project my NuGet install.cs1 script conflicts with Visual Studio which is trying to add references to the .csproj file, and my powershell script is also trying to modify the .csproj file to add/edit the propertygroups and imports.

What compounds this is that we are using Team Foundation Server, so we want the project to be editted by both powershell and Visual Studio, whilst preserving requests to checkout files if they haven't been already.  We've played with using locks in powershell but this hasn't worked thus far.

Any ideas?  I can post my powershell if needed.



Feb 21, 2012 at 8:47 AM

When you say your script conflicts with VS, what do you mean? Does it manifest as a runtime exception or something?

Yes, please post your powershell script. 

Feb 21, 2012 at 9:26 AM
Edited Feb 21, 2012 at 9:28 AM

Hi dotnetjunky,

There are no runtimes exceptions no, please see the screenshot below which shows the dialog which appears at the time of installation.  This appears when a change has occurred to a .csproj that you currently have loaded, normally Visual Studio asks you to reload the project but instead it is showing this (regardless of whether I have the .csproj file checked out by Team Foundation Server or not - in this case I'm using a new MVC3 web application with no source control).

No matter which option I select (and this happens for web applications, class libraries, winforms) I either get just the NuGet package reference added, or just the install.ps1 code, I never get both.

The following is the code in my install.ps1, and a screenshot showing the folder structure.  I realise that the assemblies for StyleCop are not in the /lib/net40 folder which is the desired functionality as I don't want them referenced by the project.  I realise the XPath sucks (and this was my first attempt at PowerShell) and I've got a better solution I've not implemented it just yet, but this all works and gives me the desired outcome if I reject the changes Visual Studio 2010 has made to the .csproj file.


param($installPath, $toolsPath, $package, $project)

$xml = New-Object xml

# load the .csproj file as XML

# select the Project node
$parentNode = $xml.Project

# create the StyleCop.Targets import node
$childNode = $xml.CreateElement("Import", $xml.Project.xmlns)

# get the package folder name
$packageName = $package.Id + '.' + $package.Version

# set the StyleCop.Targets Project path
$childNode.SetAttribute("Project", "$" + "(SolutionDir)\packages\" + $packageName + "\Applications\StyleCop\StyleCop.Targets")

# add the StyleCop.Targets import node after the <Import Project="$(MSBuildBin\ToolsPath)\Microsoft.CSharp.targets" /> node
$targetNode = $xml.Project.Import[0];

$parentNode.InsertAfter($childNode, $targetNode)

# create the propertyGroup node
$childNode = $xml.CreateElement("PropertyGroup", $xml.Project.xmlns)

# set the propertyGroup configuration mode
$childNode.SetAttribute("Condition", " '$" + "(Configuration)' != 'Debug' ")

# create the stylecopTreatErrorsAsWarnings node
$settingsNode = $xml.CreateElement("StyleCopTreatErrorsAsWarnings", $xml.Project.xmlns)

# set the stylecopTreatErrorsAsWarnings value
$settingsNode.InnerText = "false"

# add the stylecopTreatErrorsAsWarnings node

# create the runCodeAnalysis node
$settingsNode = $xml.CreateElement("RunCodeAnalysis", $xml.Project.xmlns)

# set the runCodeAnalysis value
$settingsNode.InnerText = "true"

# add the runCodeAnalysis node

# create the defineConstants node
$settingsNode = $xml.CreateElement("DefineConstants", $xml.Project.xmlns)

# set the defineConstants value
$settingsNode.InnerText = "TRACE;CODE_ANALYSIS"

# add the defineConstants node

# create the optimize node
$settingsNode = $xml.CreateElement("Optimize", $xml.Project.xmlns)

# set the optimize value
$settingsNode.InnerText = "true"

# add the optimize node

# create the documentationFile node
$settingsNode = $xml.CreateElement("DocumentationFile", $xml.Project.xmlns)

# set the documentationFile value
$settingsNode.InnerText = "bin\$" + "(MSBuildProjectName).xml"

# add the documentationFile node

# create the codeAnalysisRuleSet node
$settingsNode = $xml.CreateElement("CodeAnalysisRuleSet", $xml.Project.xmlns)

# set the codeAnalysisRuleSet value
$settingsNode.InnerText = "$" + "(SolutionDir)\packages\" + $packageName + "\Applications\CodeAnalysis\xxx.ruleset"

# add the codeAnalysisRuleSet node

# create the codeAnalysisTreatWarningsAsErrors node
$settingsNode = $xml.CreateElement("CodeAnalysisTreatWarningsAsErrors", $xml.Project.xmlns)

# set the codeAnalysisTreatWarningsAsErrors value
$settingsNode.InnerText = "true"

# add the codeAnalysisTreatWarningsAsErrors node

# add the propertyGroup node as the first <PropertyGroup> (with a Condition)
$parentNode.InsertAfter($childNode, $xml.Project.PropertyGroup[0])

# create the itemGroup node
$childNode = $xml.CreateElement("ItemGroup", $xml.Project.xmlns)

# create the itemGroup codeAnalysisDictionary node
$settingsNode = $xml.CreateElement("CodeAnalysisDictionary", $xml.Project.xmlns)

# set the include node path for Dictionary.xml
$settingsNode.SetAttribute("Include", "$" + "(SolutionDir)\packages\" + $packageName + "\Applications\CodeAnalysis\xxxDictionary.xml")

# create the itemGroup include node

# add the codeAnalysisDictionary itemGroup node before the <Import Project="$(MSBuildBin\ToolsPath)\Microsoft.CSharp.targets" /> node
$parentNode.InsertBefore($childNode, $targetNode)

# save the .csproj file


I've tried playing around with file locking too (unfinished pasted as I lost the completed solution which didn't work either):

# get the .csproj file readonly status
$projectFile = Get-ChildItem $project.FullName

# if the file is under source control in Team Foundation Server, it needs to be checked out
If ($projectFile.isreadonly)
    # did the file checkout succeed?
# the file has either been checked out in Team Foundation Server or is not under source control
    # we need to lock the .csproj file so that Visual Studio waits to modify it and doesn't cause conflicts
    $projectFile = [System.IO.File]::Open($projectFile, 'Open', 'Read', 'None')
    # were we able to get the .csproj file lock successfully?
    # perform our .csproj file modifications
    # we need to unlock the .csproj file so that Visual Studio can makes it changes


Feb 29, 2012 at 9:26 AM

Any ideas on this dotnetjunky? :)

Mar 1, 2012 at 2:31 AM

I'm about to do something similar, and here's some of the things I'm referencing - not sure if this will solve your issue.

Try using the Microsoft.Build.Evaluation library instead of directly modifying the XML. This example is for adding an Import, but you should have access to pretty much anything in the project file through it:

Have seen a couple of other reports about the problem when Visual Studio already has an in-memory change to the project and you modify the underlying project file. I didn't see any clear "this fixes it", but you may find a useful nugget in these:

Mar 1, 2012 at 3:05 AM

Another thought. Since the envDTE Project is being passed to you in as a Powershell argument, you may want to try calling $project.Save() in your script before you make any changes to the underlying .csproj file. Ideally this would flush any in-memory changes in VS out to the file.

No idea if this will work, but seems like it's worth a try.

Other things you can do with $project:

Mar 2, 2012 at 10:35 AM
Edited Mar 2, 2012 at 10:35 AM

Thanks ReedRector you pointed me in the right direction!  I've now got a more robust solution for install.ps1 and uninstall.ps1 if anyone is interested, and I'll be pushing the changes as a request to the PowerTools GitHub repo so it's more usable (if the author accepts):


param($installPath, $toolsPath, $package, $project)

# Get a reference to the MSBuildProject using a function from NuGetPowerTools.
$buildProject = Get-MSBuildProject $project.ProjectName

# Persist the package folder name.
$packageName = $package.Id + '.' + $package.Version

# Persist the build enforcement PropertyGroup condition.
$propertyGroupCondition = " '$" + "(Configuration)' != 'Debug' "

# Add the build enforcement PropertyGroup.
$propertyGroup = $buildProject.Xml.AddPropertyGroup()
$propertyGroup.Condition = $propertyGroupCondition

# Add the build enforcement properties to the PropertyGroup.
$propertyGroup.AddProperty("StyleCopTreatErrorsAsWarnings", "false")
$propertyGroup.AddProperty("RunCodeAnalysis", "true")
$propertyGroup.AddProperty("DefineConstants", "TRACE;CODE_ANALYSIS")
$propertyGroup.AddProperty("Optimize", "true")
$propertyGroup.AddProperty("DocumentationFile", "bin\$" + "(MSBuildProjectName).xml")
$propertyGroup.AddProperty("CodeAnalysisRuleSet", "..\packages\" + $packageName + "\Applications\CodeAnalysis\Rules.ruleset")
$propertyGroup.AddProperty("CodeAnalysisTreatWarningsAsErrors", "true")

# Add the TRACKERDictionary ItemGroup.
$itemGroup = $buildProject.Xml.AddItemGroup()
$itemGroup.Label = "CodeAnalysisDictionary"

# Add the build enforcement properties to the ItemGroup.
$itemGroup.AddItem("Include", "..\packages\" + $packageName + "\Applications\CodeAnalysis\Dictionary.xml")

# Add the StyleCop.Targets Import.
Add-Import ("..\packages\" + $packageName + "\Applications\StyleCop\StyleCop.Targets") $project.ProjectName

# Save the MSBuildProject.

# Save the project from the params.



param($installPath, $toolsPath, $package, $project)

# Get a reference to the MSBuildProject using a function from NuGetPowerTools.
$buildProject = Get-MSBuildProject $project.ProjectName

# Persist the build enforcement PropertyGroup condition.
$propertyGroupCondition = " '$" + "(Configuration)' != 'Debug' "

# Remove the build enforcement PropertyGroup.
$buildProject.Xml.PropertyGroups | ForEach-Object {
    if ($_.Condition -eq $propertyGroupCondition) {

# Remove the TRACKERDictionary ItemGroup and CodeReview Reference Hint.
$buildProject.Xml.ItemGroups | ForEach-Object {
    if ($_.Label -eq "CodeAnalysisDictionary") {

# Persist the package folder name.
$packageName = $package.Id + '.' + $package.Version

# Remove the StyleCop.Targets Import.
$buildProject.Xml.Imports | ForEach-Object {
    if ($_.Project -eq "..\packages\" + $packageName + "\Applications\StyleCop\StyleCop.Targets") {

# Save the MSBuildProject.

# Save the project from the params.



Jul 30, 2013 at 1:35 AM

Actually, I found that if you want to avoid the nasty [Overwrite] [Discard] dialog you should jettison the $buildProject.Save() and just use $project.Save(). The user will be prompted to save before exiting and its a friendlier experience. Otherwise, you are saving the csproj outside of Visual Studio and VS will understandably get confused. Worse yet, if you're installing other nuget dependencies (packages) that are only writing to the in memory csproj settings, well, then you'll be losing those if you reload that written to the hard copy.

-H. Tuttle