From 7e443de2a08536a7c36eaacf097700d8903b2c7d Mon Sep 17 00:00:00 2001 From: "Calvin A. Allen" Date: Sat, 20 Dec 2025 11:52:22 -0500 Subject: [PATCH] feat(ui): add progress dialog with step-by-step indication - Add RenameProgressDialog WPF dialog showing all steps - Display checkmarks as each step completes - Show current step with play icon, completed with checkmark - Progress bar tracks overall completion - UI remains responsive during operation Closes #12 --- .../CodingWithCalvin.ProjectRenamifier.csproj | 7 + .../Commands/RenamifyProjectCommand.cs | 118 ++++++++++++--- .../Dialogs/RenameProgressDialog.xaml | 54 +++++++ .../Dialogs/RenameProgressDialog.xaml.cs | 140 ++++++++++++++++++ 4 files changed, 298 insertions(+), 21 deletions(-) create mode 100644 src/CodingWithCalvin.ProjectRenamifier/Dialogs/RenameProgressDialog.xaml create mode 100644 src/CodingWithCalvin.ProjectRenamifier/Dialogs/RenameProgressDialog.xaml.cs diff --git a/src/CodingWithCalvin.ProjectRenamifier/CodingWithCalvin.ProjectRenamifier.csproj b/src/CodingWithCalvin.ProjectRenamifier/CodingWithCalvin.ProjectRenamifier.csproj index e5e3a35..6b11371 100644 --- a/src/CodingWithCalvin.ProjectRenamifier/CodingWithCalvin.ProjectRenamifier.csproj +++ b/src/CodingWithCalvin.ProjectRenamifier/CodingWithCalvin.ProjectRenamifier.csproj @@ -69,6 +69,9 @@ RenameProjectDialog.xaml + + RenameProgressDialog.xaml + @@ -90,6 +93,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + diff --git a/src/CodingWithCalvin.ProjectRenamifier/Commands/RenamifyProjectCommand.cs b/src/CodingWithCalvin.ProjectRenamifier/Commands/RenamifyProjectCommand.cs index 9434166..3d59534 100644 --- a/src/CodingWithCalvin.ProjectRenamifier/Commands/RenamifyProjectCommand.cs +++ b/src/CodingWithCalvin.ProjectRenamifier/Commands/RenamifyProjectCommand.cs @@ -1,6 +1,7 @@ using System.ComponentModel.Design; using System.IO; using System.Windows.Interop; +using System.Windows.Threading; using CodingWithCalvin.ProjectRenamifier.Dialogs; using CodingWithCalvin.ProjectRenamifier.Services; using EnvDTE; @@ -80,41 +81,116 @@ private void RenameProject(Project project, DTE2 dte) var newName = dialog.NewProjectName; var projectFilePath = project.FullName; - // Collect projects that reference this project before removal - var referencingProjects = ProjectReferenceService.FindProjectsReferencingTarget(dte.Solution, projectFilePath); + // Show progress dialog + var progressDialog = new RenameProgressDialog(currentName); + var progressHelper = new WindowInteropHelper(progressDialog) + { + Owner = dte.MainWindow.HWnd + }; + progressDialog.Show(); + + var stepIndex = 0; + + // Step 1: Collect projects that reference this project before removal + ExecuteStep(progressDialog, stepIndex++, () => + { + return ProjectReferenceService.FindProjectsReferencingTarget(dte.Solution, projectFilePath); + }, out var referencingProjects); var oldProjectFilePath = projectFilePath; - // Capture the parent solution folder before removal - var parentSolutionFolder = SolutionFolderService.GetParentSolutionFolder(project); + // Step 2: Capture the parent solution folder before removal + ExecuteStep(progressDialog, stepIndex++, () => + { + return SolutionFolderService.GetParentSolutionFolder(project); + }, out var parentSolutionFolder); - // Remove project from solution before file operations - dte.Solution.Remove(project); + // Step 3: Remove project from solution before file operations + ExecuteStep(progressDialog, stepIndex++, () => + { + dte.Solution.Remove(project); + }); + + // Step 4: Update RootNamespace and AssemblyName in .csproj + ExecuteStep(progressDialog, stepIndex++, () => + { + ProjectFileService.UpdateProjectFile(projectFilePath, currentName, newName); + }); - // Update RootNamespace and AssemblyName in .csproj - ProjectFileService.UpdateProjectFile(projectFilePath, currentName, newName); + // Step 5: Update namespace declarations in source files + ExecuteStep(progressDialog, stepIndex++, () => + { + SourceFileService.UpdateNamespacesInProject(projectFilePath, currentName, newName); + }); - // Update namespace declarations in source files - SourceFileService.UpdateNamespacesInProject(projectFilePath, currentName, newName); + // Step 6: Rename the project file on disk + ExecuteStep(progressDialog, stepIndex++, () => + { + return ProjectFileService.RenameProjectFile(projectFilePath, newName); + }, out projectFilePath); - // Rename the project file on disk - projectFilePath = ProjectFileService.RenameProjectFile(projectFilePath, newName); + // Step 7: Rename parent directory if it matches the old project name + ExecuteStep(progressDialog, stepIndex++, () => + { + return ProjectFileService.RenameParentDirectoryIfMatches(projectFilePath, currentName, newName); + }, out projectFilePath); - // Rename parent directory if it matches the old project name - projectFilePath = ProjectFileService.RenameParentDirectoryIfMatches(projectFilePath, currentName, newName); + // Step 8: Update references in projects that referenced this project + ExecuteStep(progressDialog, stepIndex++, () => + { + ProjectReferenceService.UpdateProjectReferences(referencingProjects, oldProjectFilePath, projectFilePath); + }); - // Update references in projects that referenced this project - 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); + }); - // Re-add project to solution, preserving solution folder location - SolutionFolderService.AddProjectToSolution(dte.Solution, projectFilePath, parentSolutionFolder); + // Step 10: Update using statements across the entire solution + ExecuteStep(progressDialog, stepIndex++, () => + { + SourceFileService.UpdateUsingStatementsInSolution(dte.Solution, currentName, newName); + }); - // Update using statements across the entire solution - 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(); // TODO: Implement remaining rename operations // See open issues for requirements: - // - #12: Progress indication // - #13: Error handling and rollback } + + private void ExecuteStep(RenameProgressDialog dialog, int stepIndex, Action action) + { + dialog.StartStep(stepIndex); + DoEvents(); + action(); + dialog.CompleteStep(stepIndex); + DoEvents(); + } + + private void ExecuteStep(RenameProgressDialog dialog, int stepIndex, Func func, out T result) + { + dialog.StartStep(stepIndex); + DoEvents(); + result = func(); + dialog.CompleteStep(stepIndex); + DoEvents(); + } + + private void DoEvents() + { + var frame = new DispatcherFrame(); + Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, + new DispatcherOperationCallback(obj => + { + ((DispatcherFrame)obj).Continue = false; + return null; + }), frame); + Dispatcher.PushFrame(frame); + } } } diff --git a/src/CodingWithCalvin.ProjectRenamifier/Dialogs/RenameProgressDialog.xaml b/src/CodingWithCalvin.ProjectRenamifier/Dialogs/RenameProgressDialog.xaml new file mode 100644 index 0000000..7c94dc3 --- /dev/null +++ b/src/CodingWithCalvin.ProjectRenamifier/Dialogs/RenameProgressDialog.xaml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/CodingWithCalvin.ProjectRenamifier/Dialogs/RenameProgressDialog.xaml.cs b/src/CodingWithCalvin.ProjectRenamifier/Dialogs/RenameProgressDialog.xaml.cs new file mode 100644 index 0000000..3613c3e --- /dev/null +++ b/src/CodingWithCalvin.ProjectRenamifier/Dialogs/RenameProgressDialog.xaml.cs @@ -0,0 +1,140 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Windows; +using System.Windows.Media; + +namespace CodingWithCalvin.ProjectRenamifier.Dialogs +{ + public partial class RenameProgressDialog : Window + { + public ObservableCollection Steps { get; } + + public RenameProgressDialog(string projectName) + { + InitializeComponent(); + + HeaderText.Text = $"Renaming '{projectName}'..."; + + Steps = new ObservableCollection + { + new ProgressStep("Analyzing project references"), + new ProgressStep("Capturing solution structure"), + new ProgressStep("Removing project from solution"), + new ProgressStep("Updating project file"), + new ProgressStep("Updating namespace declarations"), + new ProgressStep("Renaming project file"), + new ProgressStep("Renaming project directory"), + new ProgressStep("Updating project references"), + new ProgressStep("Re-adding project to solution"), + new ProgressStep("Updating using statements") + }; + + StepsList.ItemsSource = Steps; + } + + public void StartStep(int stepIndex) + { + if (stepIndex >= 0 && stepIndex < Steps.Count) + { + Steps[stepIndex].Status = StepStatus.InProgress; + UpdateProgress(stepIndex); + } + } + + public void CompleteStep(int stepIndex) + { + if (stepIndex >= 0 && stepIndex < Steps.Count) + { + Steps[stepIndex].Status = StepStatus.Completed; + UpdateProgress(stepIndex + 1); + } + } + + public void FailStep(int stepIndex, string error) + { + if (stepIndex >= 0 && stepIndex < Steps.Count) + { + Steps[stepIndex].Status = StepStatus.Failed; + Steps[stepIndex].Description += $" - {error}"; + } + } + + public void Complete() + { + HeaderText.Text = "Rename completed successfully!"; + ProgressBar.Value = 100; + } + + private void UpdateProgress(int completedSteps) + { + ProgressBar.Value = (completedSteps * 100.0) / Steps.Count; + } + } + + public enum StepStatus + { + Pending, + InProgress, + Completed, + Failed + } + + public class ProgressStep : INotifyPropertyChanged + { + private string _description; + private StepStatus _status; + + public ProgressStep(string description) + { + _description = description; + _status = StepStatus.Pending; + } + + public string Description + { + get => _description; + set + { + _description = value; + OnPropertyChanged(nameof(Description)); + } + } + + public StepStatus Status + { + get => _status; + set + { + _status = value; + OnPropertyChanged(nameof(Status)); + OnPropertyChanged(nameof(StatusIcon)); + OnPropertyChanged(nameof(TextColor)); + } + } + + public string StatusIcon => _status switch + { + StepStatus.Pending => "\u2022", // Bullet + StepStatus.InProgress => "\u25B6", // Play arrow + StepStatus.Completed => "\u2714", // Check mark + StepStatus.Failed => "\u2716", // X mark + _ => "\u2022" + }; + + public Brush TextColor => _status switch + { + StepStatus.Pending => Brushes.Gray, + StepStatus.InProgress => Brushes.Black, + StepStatus.Completed => Brushes.Green, + StepStatus.Failed => Brushes.Red, + _ => Brushes.Gray + }; + + public event PropertyChangedEventHandler PropertyChanged; + + protected void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +}