Code runs after NavigateTo, or the post doesn't redirect
NavigateTo doesn't always stop your code. Make it the last thing the handler does, or
return right after it, and keep it out of try/catch.
Problem
After saving a form, the handler calls NavigateTo so that reloading the page doesn't post the form
again. One of two things goes wrong:
- The browser is redirected, but code after
NavigateTostill runs, such as a save that should have been skipped. - On a static page, it doesn't redirect, and shows your error message although the save worked.
private async Task SaveAsync()
{
if (!Model!.Changed)
{
// With the setting, this returns, and the save below runs.
Navigation.NavigateTo("projects");
}
try
{
await Projects.RenameAsync(Model.Id, Model.Name);
// Without it, this throws, and the catch takes the exception.
Navigation.NavigateTo($"projects/{Model.Id}?saved=1");
}
catch (Exception)
{
_error = "Couldn't save the project.";
}
}Cause
On a static page, the post's response hasn't started when your handler runs, so NavigateTo turns
it into a redirect. Whether it stops your code depends on a project setting:
- The .NET 10 Blazor Web App template sets
BlazorDisableThrowNavigationExceptiontotrue.NavigateTorecords the redirect and returns, so the code after it runs. - Without that setting (an app from an older template, or one that added Blazor to an existing
project),
NavigateTothrows aNavigationExceptionthat Blazor turns into the redirect. Acatch (Exception)around it catches that too, and the redirect is lost.
On an interactive page, NavigateTo never throws: it starts the navigation and returns, so the code
after it runs there too, whatever the setting.
Fix
Call NavigateTo last, after the try/catch that guards your work.
return right after any NavigateTo that isn't the last line, and on any path that
shouldn't redirect. That works in every mode, with the setting on or off:
private async Task SaveAsync()
{
if (!Model!.Changed)
{
Navigation.NavigateTo("projects");
return;
}
try
{
await Projects.RenameAsync(Model.Id, Model.Name);
}
catch (DbUpdateException)
{
_error = "Couldn't save the project.";
return;
}
Navigation.NavigateTo($"projects/{Model.Id}?saved=1");
}
Rename the project, then reload: the page shows the saved name from ?saved= and doesn't post
again.
Preview
Code
System.ComponentModel.DataAnnotations
NavigationManager Navigation
<Stack Gap="4" MaxWidth="28.Rem" Grow>
if (Saved is not null)
{
<Alert Status="Status.Success">Renamed to "Saved". Reloading the page doesn't post it again.</Alert>
}
<Form Model="Model" FormName="rename-project" OnValidSubmit="Save">
<TextField @bind-Value="Model!.Name" />
<Button Submit>Rename</Button>
</Form>
</Stack>
{
[SupplyParameterFromForm(FormName = "rename-project")]
private Project? Model { get; set; }
[SupplyParameterFromQuery(Name = "saved")]
private string? Saved { get; set; }
protected override void OnInitialized() => Model ??= new() { Name = Saved ?? "Website refresh" };
private void Save()
{
// Save Model here, then answer with a redirect, as the last thing the handler does.
Navigation.NavigateTo(Navigation.GetUriWithQueryParameter("saved", Model!.Name));
}
public sealed class Project
{
[Required, StringLength(40), Display(Name = "Project name")]
public string? Name { get; set; }
}
}Live: NavigateTo changes the address without a page load.
GetUriWithQueryParameter keeps the rest of the address. A message that shouldn't be in the address
can go in a cookie or in your own store instead, read on the next request.
Related
- Form: posting and submitting.
- A value resets on the next request: why the saved name goes in the address.