From 4ae5338f3ad7ac099a1b05c1dd36530faa3d7349 Mon Sep 17 00:00:00 2001 From: "Calvin A. Allen" Date: Sat, 20 Dec 2025 12:01:53 -0500 Subject: [PATCH] feat(error): add error handling and rollback support - Wrap rename operation in try-catch - Track state to know if project was removed from solution - On failure, attempt to re-add project to solution at current path - Show failed step in progress dialog with error details - Display error message box with explanation Closes #13 --- .../Commands/RenamifyProjectCommand.cs | 154 +++++++++++------- 1 file changed, 97 insertions(+), 57 deletions(-) diff --git a/src/CodingWithCalvin.ProjectRenamifier/Commands/RenamifyProjectCommand.cs b/src/CodingWithCalvin.ProjectRenamifier/Commands/RenamifyProjectCommand.cs index 3d59534..e8ea9e6 100644 --- a/src/CodingWithCalvin.ProjectRenamifier/Commands/RenamifyProjectCommand.cs +++ b/src/CodingWithCalvin.ProjectRenamifier/Commands/RenamifyProjectCommand.cs @@ -80,6 +80,7 @@ private void RenameProject(Project project, DTE2 dte) var newName = dialog.NewProjectName; var projectFilePath = project.FullName; + var originalProjectFilePath = projectFilePath; // Show progress dialog var progressDialog = new RenameProgressDialog(currentName); @@ -90,77 +91,116 @@ private void RenameProject(Project project, DTE2 dte) progressDialog.Show(); var stepIndex = 0; + var projectRemovedFromSolution = false; + var projectReaddedToSolution = false; + System.Collections.Generic.List referencingProjects = null; + Project parentSolutionFolder = null; - // Step 1: Collect projects that reference this project before removal - ExecuteStep(progressDialog, stepIndex++, () => + try { - return ProjectReferenceService.FindProjectsReferencingTarget(dte.Solution, projectFilePath); - }, out var referencingProjects); - var oldProjectFilePath = projectFilePath; + // Step 1: Collect projects that reference this project before removal + ExecuteStep(progressDialog, stepIndex++, () => + { + return ProjectReferenceService.FindProjectsReferencingTarget(dte.Solution, projectFilePath); + }, out referencingProjects); + var oldProjectFilePath = projectFilePath; - // Step 2: Capture the parent solution folder before removal - ExecuteStep(progressDialog, stepIndex++, () => - { - return SolutionFolderService.GetParentSolutionFolder(project); - }, out var parentSolutionFolder); + // Step 2: Capture the parent solution folder before removal + ExecuteStep(progressDialog, stepIndex++, () => + { + return SolutionFolderService.GetParentSolutionFolder(project); + }, out parentSolutionFolder); - // Step 3: Remove project from solution before file operations - ExecuteStep(progressDialog, stepIndex++, () => - { - dte.Solution.Remove(project); - }); + // Step 3: Remove project from solution before file operations + ExecuteStep(progressDialog, stepIndex++, () => + { + dte.Solution.Remove(project); + projectRemovedFromSolution = true; + }); - // Step 4: Update RootNamespace and AssemblyName in .csproj - ExecuteStep(progressDialog, stepIndex++, () => - { - ProjectFileService.UpdateProjectFile(projectFilePath, currentName, newName); - }); + // Step 4: Update RootNamespace and AssemblyName in .csproj + ExecuteStep(progressDialog, stepIndex++, () => + { + ProjectFileService.UpdateProjectFile(projectFilePath, currentName, newName); + }); - // Step 5: Update namespace declarations in source files - ExecuteStep(progressDialog, stepIndex++, () => - { - SourceFileService.UpdateNamespacesInProject(projectFilePath, currentName, newName); - }); + // Step 5: Update namespace declarations in source files + ExecuteStep(progressDialog, stepIndex++, () => + { + SourceFileService.UpdateNamespacesInProject(projectFilePath, currentName, newName); + }); - // Step 6: Rename the project file on disk - ExecuteStep(progressDialog, stepIndex++, () => - { - return ProjectFileService.RenameProjectFile(projectFilePath, newName); - }, out projectFilePath); + // Step 6: Rename the project file on disk + ExecuteStep(progressDialog, stepIndex++, () => + { + return ProjectFileService.RenameProjectFile(projectFilePath, newName); + }, out projectFilePath); - // Step 7: Rename parent directory if it matches the old project name - ExecuteStep(progressDialog, stepIndex++, () => - { - return ProjectFileService.RenameParentDirectoryIfMatches(projectFilePath, currentName, newName); - }, out projectFilePath); + // Step 7: Rename parent directory if it matches the old project name + ExecuteStep(progressDialog, stepIndex++, () => + { + return ProjectFileService.RenameParentDirectoryIfMatches(projectFilePath, currentName, newName); + }, out projectFilePath); - // Step 8: Update references in projects that referenced this project - ExecuteStep(progressDialog, stepIndex++, () => - { - ProjectReferenceService.UpdateProjectReferences(referencingProjects, oldProjectFilePath, projectFilePath); - }); + // Step 8: Update references in projects that referenced this project + ExecuteStep(progressDialog, stepIndex++, () => + { + ProjectReferenceService.UpdateProjectReferences(referencingProjects, oldProjectFilePath, projectFilePath); + }); - // Step 9: Re-add project to solution, preserving solution folder location - ExecuteStep(progressDialog, stepIndex++, () => - { - SolutionFolderService.AddProjectToSolution(dte.Solution, projectFilePath, parentSolutionFolder); - }); + // Step 9: Re-add project to solution, preserving solution folder location + ExecuteStep(progressDialog, stepIndex++, () => + { + SolutionFolderService.AddProjectToSolution(dte.Solution, projectFilePath, parentSolutionFolder); + projectReaddedToSolution = true; + }); - // Step 10: Update using statements across the entire solution - ExecuteStep(progressDialog, stepIndex++, () => + // Step 10: Update using statements across the entire solution + ExecuteStep(progressDialog, stepIndex++, () => + { + SourceFileService.UpdateUsingStatementsInSolution(dte.Solution, currentName, newName); + }); + + // Mark as complete and close after a brief delay + progressDialog.Complete(); + DoEvents(); + System.Threading.Thread.Sleep(500); + progressDialog.Close(); + } + catch (Exception ex) { - SourceFileService.UpdateUsingStatementsInSolution(dte.Solution, currentName, newName); - }); + // Mark the current step as failed + progressDialog.FailStep(stepIndex, ex.Message); + DoEvents(); - // Mark as complete and close after a brief delay - progressDialog.Complete(); - DoEvents(); - System.Threading.Thread.Sleep(500); - progressDialog.Close(); + // Attempt rollback if project was removed but not re-added + if (projectRemovedFromSolution && !projectReaddedToSolution) + { + try + { + // Try to re-add the project at its current location + var currentProjectPath = File.Exists(projectFilePath) ? projectFilePath : originalProjectFilePath; + if (File.Exists(currentProjectPath)) + { + SolutionFolderService.AddProjectToSolution(dte.Solution, currentProjectPath, parentSolutionFolder); + } + } + catch + { + // Rollback failed, nothing more we can do + } + } + + // Show error message + System.Windows.MessageBox.Show( + $"An error occurred while renaming the project:\n\n{ex.Message}\n\n" + + "The operation has been aborted. The project may be in a partially renamed state.", + "Rename Failed", + System.Windows.MessageBoxButton.OK, + System.Windows.MessageBoxImage.Error); - // TODO: Implement remaining rename operations - // See open issues for requirements: - // - #13: Error handling and rollback + progressDialog.Close(); + } } private void ExecuteStep(RenameProgressDialog dialog, int stepIndex, Action action)