Skip to content
IzzyBlazor 0.1.0-alpha.17

Enter all results ↑ ↓ move Esc close

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 NavigateTo still 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.
Rename.razor
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 BlazorDisableThrowNavigationException to true. NavigateTo records 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), NavigateTo throws a NavigationException that Blazor turns into the redirect. A catch (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:

Rename.razor
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

RedirectAfterSave.razor
@using System.ComponentModel.DataAnnotations
@inject 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>

@code {
    [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; }
    }
}

Off: The post is answered with a redirect to ?saved=, so reloading doesn't post again.

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.