Skip to content

Commit 1c1e50a

Browse files
committed
fix(rename): unload referencing projects before rewriting their .csproj
When a renamed project has dependents, Visual Studio keeps a file handle on each referencing .csproj (workspace, file watchers, design-time build). Writing through XmlDocument.Save would frequently fail with 'The process cannot access the file because it is being used by another process', leaving the rename in a partially applied state. Each referencing project is now unloaded via IVsSolution4.UnloadProject, rewritten, and reloaded in a finally block. If the unload cannot be performed (no GUID, service missing, HRESULT failure), we fall back to the previous direct-save behavior so this change never regresses.
1 parent c99c459 commit 1c1e50a

2 files changed

Lines changed: 88 additions & 11 deletions

File tree

src/CodingWithCalvin.ProjectRenamifier/Commands/RenamifyProjectCommand.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using CodingWithCalvin.Otel4Vsix;
99
using EnvDTE;
1010
using EnvDTE80;
11+
using Microsoft.VisualStudio.Shell.Interop;
1112

1213
namespace CodingWithCalvin.ProjectRenamifier
1314
{
@@ -172,9 +173,11 @@ private void RenameProject(Project project, DTE2 dte)
172173
var stepIndex = 0;
173174
var projectRemovedFromSolution = false;
174175
var projectReaddedToSolution = false;
175-
List<string> referencingProjects = null;
176+
List<ProjectReferenceService.ReferencingProjectInfo> referencingProjects = null;
176177
Project parentSolutionFolder = null;
177178

179+
var vsSolution = ServiceProvider.GetService(typeof(SVsSolution)) as IVsSolution;
180+
178181
try
179182
{
180183
// Step 1: Collect projects that reference this project before removal
@@ -230,7 +233,7 @@ private void RenameProject(Project project, DTE2 dte)
230233
// Step 9: Update references in projects that referenced this project
231234
ExecuteStep(progressDialog, stepIndex++, () =>
232235
{
233-
ProjectReferenceService.UpdateProjectReferences(referencingProjects, oldProjectFilePath, projectFilePath);
236+
ProjectReferenceService.UpdateProjectReferences(vsSolution, referencingProjects, oldProjectFilePath, projectFilePath);
234237
});
235238

236239
// Step 10: Re-add project to solution, preserving solution folder location

src/CodingWithCalvin.ProjectRenamifier/Services/ProjectReferenceService.cs

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using System.IO;
33
using System.Xml;
44
using EnvDTE;
5+
using Microsoft.VisualStudio;
6+
using Microsoft.VisualStudio.Shell.Interop;
57

68
namespace CodingWithCalvin.ProjectRenamifier.Services
79
{
@@ -10,17 +12,26 @@ namespace CodingWithCalvin.ProjectRenamifier.Services
1012
/// </summary>
1113
internal static class ProjectReferenceService
1214
{
15+
/// <summary>
16+
/// Metadata for a project that references the target project being renamed.
17+
/// </summary>
18+
public sealed class ReferencingProjectInfo
19+
{
20+
public string FullPath { get; set; }
21+
public string UniqueName { get; set; }
22+
}
23+
1324
/// <summary>
1425
/// Finds all projects in the solution that reference the specified project.
1526
/// </summary>
1627
/// <param name="solution">The solution to search.</param>
1728
/// <param name="targetProjectPath">The full path to the project being renamed.</param>
18-
/// <returns>A list of project paths that reference the target project.</returns>
19-
public static List<string> FindProjectsReferencingTarget(Solution solution, string targetProjectPath)
29+
/// <returns>A list of referencing project descriptors (full path + unique name).</returns>
30+
public static List<ReferencingProjectInfo> FindProjectsReferencingTarget(Solution solution, string targetProjectPath)
2031
{
2132
ThreadHelper.ThrowIfNotOnUIThread();
2233

23-
var referencingProjects = new List<string>();
34+
var referencingProjects = new List<ReferencingProjectInfo>();
2435
var targetFileName = Path.GetFileName(targetProjectPath);
2536

2637
foreach (Project project in solution.Projects)
@@ -34,7 +45,7 @@ public static List<string> FindProjectsReferencingTarget(Solution solution, stri
3445
/// <summary>
3546
/// Recursively searches a project (and solution folders) for references to the target.
3647
/// </summary>
37-
private static void FindReferencesInProject(Project project, string targetProjectPath, string targetFileName, List<string> referencingProjects)
48+
private static void FindReferencesInProject(Project project, string targetProjectPath, string targetFileName, List<ReferencingProjectInfo> referencingProjects)
3849
{
3950
ThreadHelper.ThrowIfNotOnUIThread();
4051

@@ -67,7 +78,11 @@ private static void FindReferencesInProject(Project project, string targetProjec
6778
{
6879
if (ProjectReferencesTarget(project.FullName, targetFileName))
6980
{
70-
referencingProjects.Add(project.FullName);
81+
referencingProjects.Add(new ReferencingProjectInfo
82+
{
83+
FullPath = project.FullName,
84+
UniqueName = project.UniqueName,
85+
});
7186
}
7287
}
7388
}
@@ -118,18 +133,77 @@ private static bool ProjectReferencesTarget(string projectFilePath, string targe
118133

119134
/// <summary>
120135
/// Updates project references in all projects that referenced the old project path.
136+
/// Each referencing project is temporarily unloaded via <see cref="IVsSolution4"/> so Visual Studio
137+
/// releases its file handle before we rewrite the .csproj on disk, then reloaded afterwards.
121138
/// </summary>
122-
/// <param name="referencingProjectPaths">Projects that need their references updated.</param>
139+
/// <param name="vsSolution">The Visual Studio solution service used to unload and reload projects.</param>
140+
/// <param name="referencingProjects">Projects that need their references updated.</param>
123141
/// <param name="oldProjectPath">The old path to the renamed project.</param>
124142
/// <param name="newProjectPath">The new path to the renamed project.</param>
125-
public static void UpdateProjectReferences(List<string> referencingProjectPaths, string oldProjectPath, string newProjectPath)
143+
public static void UpdateProjectReferences(IVsSolution vsSolution, List<ReferencingProjectInfo> referencingProjects, string oldProjectPath, string newProjectPath)
126144
{
145+
ThreadHelper.ThrowIfNotOnUIThread();
146+
127147
var oldFileName = Path.GetFileName(oldProjectPath);
148+
var solution4 = vsSolution as IVsSolution4;
149+
150+
foreach (var info in referencingProjects)
151+
{
152+
UpdateSingleProjectReference(vsSolution, solution4, info, oldFileName, oldProjectPath, newProjectPath);
153+
}
154+
}
155+
156+
private static void UpdateSingleProjectReference(
157+
IVsSolution vsSolution,
158+
IVsSolution4 solution4,
159+
ReferencingProjectInfo info,
160+
string oldFileName,
161+
string oldProjectPath,
162+
string newProjectPath)
163+
{
164+
ThreadHelper.ThrowIfNotOnUIThread();
165+
166+
var projectGuid = System.Guid.Empty;
167+
var unloaded = false;
168+
169+
if (solution4 != null && TryGetProjectGuid(vsSolution, info.UniqueName, out projectGuid))
170+
{
171+
var hr = solution4.UnloadProject(ref projectGuid, (uint)_VSProjectUnloadStatus.UNLOADSTATUS_UnloadedByUser);
172+
unloaded = ErrorHandler.Succeeded(hr);
173+
}
128174

129-
foreach (var projectPath in referencingProjectPaths)
175+
try
130176
{
131-
UpdateReferencesInProject(projectPath, oldFileName, oldProjectPath, newProjectPath);
177+
UpdateReferencesInProject(info.FullPath, oldFileName, oldProjectPath, newProjectPath);
132178
}
179+
finally
180+
{
181+
if (unloaded)
182+
{
183+
solution4.ReloadProject(ref projectGuid);
184+
}
185+
}
186+
}
187+
188+
private static bool TryGetProjectGuid(IVsSolution vsSolution, string uniqueName, out System.Guid projectGuid)
189+
{
190+
ThreadHelper.ThrowIfNotOnUIThread();
191+
192+
projectGuid = System.Guid.Empty;
193+
194+
if (string.IsNullOrEmpty(uniqueName))
195+
{
196+
return false;
197+
}
198+
199+
var hr = vsSolution.GetProjectOfUniqueName(uniqueName, out var hierarchy);
200+
if (!ErrorHandler.Succeeded(hr) || hierarchy == null)
201+
{
202+
return false;
203+
}
204+
205+
hr = vsSolution.GetGuidOfProject(hierarchy, out projectGuid);
206+
return ErrorHandler.Succeeded(hr) && projectGuid != System.Guid.Empty;
133207
}
134208

135209
/// <summary>

0 commit comments

Comments
 (0)