tracks form changes and warns users before navigation

This commit is contained in:
2025-12-25 23:55:04 -05:00
parent 059a16b958
commit 77b5683804
8 changed files with 156 additions and 8 deletions
+12 -1
View File
@@ -11,6 +11,7 @@
BackButtonUrl="/events" />
<EditForm id="create-event-form" Model="EventDefinition" OnValidSubmit="OnValidSubmit" Enhance>
<FormChangeTracker @ref="_formChangeTracker" />
<AntiforgeryToken />
<DataAnnotationsValidator />
<MudGrid>
@@ -48,17 +49,27 @@
<FormActions>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Form="create-event-form">Create</MudButton>
<MudButton Href="/events" Variant="Variant.Text">Cancel</MudButton>
<MudButton OnClick="HandleCancel" Variant="Variant.Text">Cancel</MudButton>
</FormActions>
@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");
}
}
+10 -1
View File
@@ -18,6 +18,7 @@
BackButtonUrl="/events" />
<EditForm id="edit-event-form" Model="EventDefinition" OnValidSubmit="OnValidSubmit" Enhance>
<FormChangeTracker @ref="_formChangeTracker" />
<AntiforgeryToken />
<DataAnnotationsValidator />
<MudGrid>
@@ -55,7 +56,7 @@
<FormActions>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Form="edit-event-form">Save</MudButton>
<MudButton Href="/events" Variant="Variant.Text">Cancel</MudButton>
<MudButton OnClick="HandleCancel" Variant="Variant.Text">Cancel</MudButton>
</FormActions>
@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");
}
@@ -11,6 +11,7 @@
BackButtonUrl="/students" />
<EditForm id="create-student-form" Model="Student" OnValidSubmit="OnValidSubmit" Enhance>
<FormChangeTracker @ref="_formChangeTracker" />
<AntiforgeryToken />
<DataAnnotationsValidator />
<MudGrid>
@@ -29,18 +30,27 @@
<FormActions>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Form="create-student-form">Create</MudButton>
<MudButton Href="/students" Variant="Variant.Text">Cancel</MudButton>
<MudButton OnClick="HandleCancel" Variant="Variant.Text">Cancel</MudButton>
</FormActions>
@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");
}
}
+11 -2
View File
@@ -20,6 +20,7 @@
@* https://www.mudblazor.com/components/form *@
@* https://medium.com/@husainalbar/applying-mudblazor-for-crud-operations-in-our-blazor-project-a343037a52ef *@
<EditForm method="post" Model="Student" OnValidSubmit="UpdateStudent" FormName="edit" Enhance>
<FormChangeTracker @ref="_formChangeTracker" />
<AntiforgeryToken />
<DataAnnotationsValidator/>
<MudGrid>
@@ -49,7 +50,7 @@
<FormActions>
<MudButton OnClick="UpdateStudent" Variant="Variant.Filled" Color="Color.Primary">Save</MudButton>
<MudButton Href="@(ReturnUrl ?? "/students")" Variant="Variant.Text">Cancel</MudButton>
<MudButton OnClick="HandleCancel" Variant="Variant.Text">Cancel</MudButton>
</FormActions>
@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");
}
+10 -1
View File
@@ -18,6 +18,7 @@
BackButtonUrl="/teams" />
<EditForm method="post" Model="Team" OnValidSubmit="AddTeam" FormName="create" Enhance>
<FormChangeTracker @ref="_formChangeTracker" />
<AntiforgeryToken />
<DataAnnotationsValidator />
<MudGrid>
@@ -67,13 +68,14 @@
<FormActions>
<MudButton OnClick="AddTeam" Variant="Variant.Filled" Color="Color.Primary">Add</MudButton>
<MudButton Href="/teams" Variant="Variant.Text">Cancel</MudButton>
<MudButton OnClick="HandleCancel" Variant="Variant.Text">Cancel</MudButton>
</FormActions>
@code {
[SupplyParameterFromForm]
private Team Team { get; set; } = new();
private FormChangeTracker? _formChangeTracker;
private List<EventDefinition>? _events;
private List<Student> _students = [];
private IEnumerable<Student> _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");
}
}
+9 -1
View File
@@ -17,6 +17,7 @@
BackButtonUrl="/teams" />
<EditForm method="post" Model="Team" OnValidSubmit="UpdateTeam" FormName="edit" Enhance>
<FormChangeTracker @ref="_formChangeTracker" />
<AntiforgeryToken />
<DataAnnotationsValidator/>
<MudGrid>
@@ -38,7 +39,7 @@
<FormActions>
<MudButton OnClick="UpdateTeam" Variant="Variant.Filled" Color="Color.Primary">Save</MudButton>
<MudButton Href="/teams" Variant="Variant.Text">Cancel</MudButton>
<MudButton OnClick="HandleCancel" Variant="Variant.Text">Cancel</MudButton>
</FormActions>
@code {
@@ -48,6 +49,7 @@
[SupplyParameterFromForm]
private Team? Team { get; set; }
private FormChangeTracker? _formChangeTracker;
private IEnumerable<Student>? _selectedStudents = [];
private List<Student> _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");
}
@@ -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();
}
}
/// <summary>
/// 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.
/// </summary>
public void AllowNavigation()
{
_allowNavigation = true;
}
/// <summary>
/// Reset the dirty state of the form. This marks the form as clean and resets the navigation flag.
/// </summary>
public void MarkClean()
{
_isDirty = false;
_allowNavigation = false;
}
public void Dispose()
{
var contextToUse = ExplicitEditContext ?? EditContext;
if (contextToUse != null)
{
contextToUse.OnFieldChanged -= HandleFieldChanged;
}
_navigationRegistration?.Dispose();
}
}
@@ -1,5 +1,4 @@
@namespace WebApp.Components.Shared.Components
@using MudBlazor
<PageTitle>@GetPageTitle()</PageTitle>