diff --git a/WebApp/Components/Features/Events/Create.razor b/WebApp/Components/Features/Events/Create.razor index 88c4e23..183e9f6 100644 --- a/WebApp/Components/Features/Events/Create.razor +++ b/WebApp/Components/Features/Events/Create.razor @@ -11,6 +11,7 @@ BackButtonUrl="/events" /> + @@ -48,17 +49,27 @@ Create - Cancel + Cancel @code { [SupplyParameterFromForm] private EventDefinition EventDefinition { get; set; } = new(); + private FormChangeTracker? _formChangeTracker; + private void OnValidSubmit() { + _formChangeTracker?.AllowNavigation(); + context.Events.Add(EventDefinition); context.SaveChanges(); NavigationManager.NavigateTo("/events"); } + + private void HandleCancel() + { + _formChangeTracker?.AllowNavigation(); + NavigationManager.NavigateTo("/events"); + } } diff --git a/WebApp/Components/Features/Events/Edit.razor b/WebApp/Components/Features/Events/Edit.razor index a7e218f..09b2dcc 100644 --- a/WebApp/Components/Features/Events/Edit.razor +++ b/WebApp/Components/Features/Events/Edit.razor @@ -18,6 +18,7 @@ BackButtonUrl="/events" /> + @@ -55,7 +56,7 @@ Save - Cancel + Cancel @code { @@ -65,6 +66,8 @@ [SupplyParameterFromForm] private EventDefinition? EventDefinition { get; set; } + private FormChangeTracker? _formChangeTracker; + protected override async Task OnInitializedAsync() { EventDefinition ??= await context.Events.FirstOrDefaultAsync(m => m.Id == Id); @@ -84,6 +87,8 @@ try { context.SaveChangesAsync(); + _formChangeTracker?.AllowNavigation(); + NavigationManager.NavigateTo("/events"); } catch (DbUpdateConcurrencyException) { @@ -96,7 +101,11 @@ throw; } } + } + private void HandleCancel() + { + _formChangeTracker?.AllowNavigation(); NavigationManager.NavigateTo("/events"); } diff --git a/WebApp/Components/Features/Students/Create.razor b/WebApp/Components/Features/Students/Create.razor index eee8bd3..84b9c23 100644 --- a/WebApp/Components/Features/Students/Create.razor +++ b/WebApp/Components/Features/Students/Create.razor @@ -11,6 +11,7 @@ BackButtonUrl="/students" /> + @@ -29,18 +30,27 @@ Create - Cancel + Cancel @code { [SupplyParameterFromForm] private Student Student { get; set; } = new() { TsaYear = 1 }; + private FormChangeTracker? _formChangeTracker; + private void OnValidSubmit(EditContext context) { + _formChangeTracker?.AllowNavigation(); Context.Students.Add(Student); Context.SaveChanges(); NavigationManager.NavigateTo("/students"); } + + private void HandleCancel() + { + _formChangeTracker?.AllowNavigation(); + NavigationManager.NavigateTo("/students"); + } } diff --git a/WebApp/Components/Features/Students/Edit.razor b/WebApp/Components/Features/Students/Edit.razor index 42152e4..b160650 100644 --- a/WebApp/Components/Features/Students/Edit.razor +++ b/WebApp/Components/Features/Students/Edit.razor @@ -20,6 +20,7 @@ @* https://www.mudblazor.com/components/form *@ @* https://medium.com/@husainalbar/applying-mudblazor-for-crud-operations-in-our-blazor-project-a343037a52ef *@ + @@ -49,7 +50,7 @@ Save - Cancel + Cancel @code { @@ -62,6 +63,8 @@ [SupplyParameterFromForm] private Student? Student { get; set; } + private FormChangeTracker? _formChangeTracker; + protected override async Task OnInitializedAsync() { Student ??= await Context.Students.FirstOrDefaultAsync(m => m.Id == Id); @@ -75,7 +78,7 @@ // To protect from overposting attacks, enable the specific properties you want to bind to. // For more information, see https://learn.microsoft.com/aspnet/core/blazor/forms/#mitigate-overposting-attacks. private async Task UpdateStudent() - { + { if (Student?.OfficerRole == 0) Student.OfficerRole = null; @@ -84,6 +87,8 @@ try { await Context.SaveChangesAsync(); + _formChangeTracker?.AllowNavigation(); + NavigationManager.NavigateTo(ReturnUrl ?? "/students"); } catch (DbUpdateConcurrencyException) { @@ -96,7 +101,11 @@ throw; } } + } + private void HandleCancel() + { + _formChangeTracker?.AllowNavigation(); NavigationManager.NavigateTo(ReturnUrl ?? "/students"); } diff --git a/WebApp/Components/Features/Teams/Create.razor b/WebApp/Components/Features/Teams/Create.razor index e9bd952..c057f0e 100644 --- a/WebApp/Components/Features/Teams/Create.razor +++ b/WebApp/Components/Features/Teams/Create.razor @@ -18,6 +18,7 @@ BackButtonUrl="/teams" /> + @@ -67,13 +68,14 @@ Add - Cancel + Cancel @code { [SupplyParameterFromForm] private Team Team { get; set; } = new(); + private FormChangeTracker? _formChangeTracker; private List? _events; private List _students = []; private IEnumerable _selectedStudents = []; @@ -159,6 +161,13 @@ Context.Teams.Add(Team); await Context.SaveChangesAsync(); + _formChangeTracker?.AllowNavigation(); + NavigationManager.NavigateTo("/teams"); + } + + private void HandleCancel() + { + _formChangeTracker?.AllowNavigation(); NavigationManager.NavigateTo("/teams"); } } \ No newline at end of file diff --git a/WebApp/Components/Features/Teams/Edit.razor b/WebApp/Components/Features/Teams/Edit.razor index ceafc2f..396eec8 100644 --- a/WebApp/Components/Features/Teams/Edit.razor +++ b/WebApp/Components/Features/Teams/Edit.razor @@ -17,6 +17,7 @@ BackButtonUrl="/teams" /> + @@ -38,7 +39,7 @@ Save - Cancel + Cancel @code { @@ -48,6 +49,7 @@ [SupplyParameterFromForm] private Team? Team { get; set; } + private FormChangeTracker? _formChangeTracker; private IEnumerable? _selectedStudents = []; private List _students = []; @@ -100,6 +102,8 @@ try { await Context.SaveChangesAsync(); + _formChangeTracker?.AllowNavigation(); + NavigationManager.NavigateTo("/teams"); } catch (DbUpdateConcurrencyException) { @@ -112,7 +116,11 @@ throw; } } + } + private void HandleCancel() + { + _formChangeTracker?.AllowNavigation(); NavigationManager.NavigateTo("/teams"); } diff --git a/WebApp/Components/Shared/Components/FormChangeTracker.razor b/WebApp/Components/Shared/Components/FormChangeTracker.razor new file mode 100644 index 0000000..94c9c00 --- /dev/null +++ b/WebApp/Components/Shared/Components/FormChangeTracker.razor @@ -0,0 +1,93 @@ +@namespace WebApp.Components.Shared.Components +@implements IDisposable +@inject IDialogService DialogService +@inject NavigationManager NavigationManager + +@code { + [CascadingParameter] + private EditContext? EditContext { get; set; } + + [Parameter] + public EditContext? ExplicitEditContext { get; set; } + + [Parameter] + public bool Enabled { get; set; } = true; + + private bool _isDirty = false; + private bool _allowNavigation = false; + private IDisposable? _navigationRegistration; + + protected override void OnInitialized() + { + var contextToUse = ExplicitEditContext ?? EditContext; + + if (contextToUse == null) + { + throw new InvalidOperationException("FormChangeTracker requires an EditContext. Either provide it as a cascading parameter or through the ExplicitEditContext parameter."); + } + + // Subscribe to field changes to detect when form becomes dirty + contextToUse.OnFieldChanged += HandleFieldChanged; + + // Register navigation handler to intercept navigation attempts + _navigationRegistration = NavigationManager.RegisterLocationChangingHandler(OnLocationChanging); + } + + private void HandleFieldChanged(object? sender, FieldChangedEventArgs e) + { + if (Enabled) + { + _isDirty = true; + } + } + + private async ValueTask OnLocationChanging(LocationChangingContext context) + { + // Allow navigation if form isn't dirty, navigation is explicitly allowed, or tracking is disabled + if (!_isDirty || _allowNavigation || !Enabled) + { + return; + } + + // Show confirmation dialog + var result = await DialogService.ShowMessageBox( + "Unsaved Changes", + "You have unsaved changes. Are you sure you want to leave this page?", + yesText: "Leave", + cancelText: "Stay"); + + // If user chooses to stay (result is null or false), prevent navigation + if (result != true) + { + context.PreventNavigation(); + } + } + + /// + /// Call this method before programmatic navigation to bypass the unsaved changes warning. + /// Use this when the form has been successfully saved or when the user explicitly cancels. + /// + public void AllowNavigation() + { + _allowNavigation = true; + } + + /// + /// Reset the dirty state of the form. This marks the form as clean and resets the navigation flag. + /// + public void MarkClean() + { + _isDirty = false; + _allowNavigation = false; + } + + public void Dispose() + { + var contextToUse = ExplicitEditContext ?? EditContext; + if (contextToUse != null) + { + contextToUse.OnFieldChanged -= HandleFieldChanged; + } + _navigationRegistration?.Dispose(); + } +} diff --git a/WebApp/Components/Shared/Components/PageHeader.razor b/WebApp/Components/Shared/Components/PageHeader.razor index bfcdb68..4ac909b 100644 --- a/WebApp/Components/Shared/Components/PageHeader.razor +++ b/WebApp/Components/Shared/Components/PageHeader.razor @@ -1,5 +1,4 @@ @namespace WebApp.Components.Shared.Components -@using MudBlazor @GetPageTitle()