Telerik blogs

No one wants to see their work go up in smoke. .NET 7’s new location handling logic makes it easy to check with your users before they depart for another site and lose all their changes.

We’ve all been there—you spend ages typing information into a web app only to accidentally navigate away from the page. In horror, you watch as a new page appears. You hit the back button, desperately hoping your work will reappear, but alas, when the page finally loads your work is gone and you’ve no option but to start from scratch.

When you’re building a Blazor web app, you probably want to save your users from such a fate.

A common requirement is to prompt the user for confirmation when they attempt to leave a page with unsaved changes. This was a non-trivial task in .NET 6 and below, but .NET 7 introduces two new related features which make it much easier to detect when navigation is about to occur and run any logic you see fit (including canceling the navigation entirely).

The New LocationChanging Event

The new LocationChanging event runs just before Blazor navigates to a new “page.”

To use it, you need an instance of NavigationManager.

@inject NavigationManager NavigationManager
@implements IDisposable

You can then register an event handler for LocationChanging by overriding OnAfterRender or its async equivalent OnAfterRenderAsync and calling RegisterLocationChangingHandler on NavigationManager.

@code {

    private IDisposable registration;

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            registration = NavigationManager.RegisterLocationChangingHandler(LocationChangingHandler);
        }
    }

    private ValueTask LocationChangingHandler(LocationChangingContext arg)
    {
        Console.WriteLine("Location is changing...");
        return ValueTask.CompletedTask;
    }

    public void Dispose()
    {
        registration?.Dispose();
    }
}

There are two important details here:

  • We only want to register the handler once, so we check if firstRender is true before registering our handler.
  • We implement @IDisposable and use the Dispose method to ensure we dispose of the event registration when our component is disposed.

This last step is important. It ensures we don’t inadvertently create a memory leak in our application by leaving orphaned/unused event handlers lying around in memory.

With this, we’ll see a message in the browser’s console (if running Blazor WASM) or our application logs (if Blazor Server) every time we attempt to navigate anywhere within our Blazor app.

In our handler for LocationChanging, we can then choose to cancel the navigation using the PreventNavigation method on the passed in LocationChangingContext.

private ValueTask LocationChangingHandler(LocationChangingContext arg)
{  
    Console.WriteLine("Location is changing...");
    
    arg.PreventNavigation();
    
    return ValueTask.CompletedTask;
}

This is where we’d likely want to use a boolean value to track whether the user has unsaved changes.

We could then check the boolean here in our LocationChangingHandler, cancel the navigation as needed, and show something in the UI to let the user know what’s going on (more on this shortly).

It’s worth knowing the limitations of the LocationChanging event—the main one being that it doesn’t always fire when you might want/need it to.

The LocationChanging event will fire for:

  • Any navigation triggered within your Blazor application to another “page” within your Blazor application
  • Any navigation which is initiated via NavigationManager (using its NavigateTo method), including links to external sites

LocationChanging will not fire:

  • When the user enters an address directly into the Navigation bar of their browser
  • When your users follow direct links to external sites (which don’t go via NavigationManager)

But fear not—if you want those last two scenarios handled, there is another new .NET 7 feature you can lean on.

With the NavigationLock component, you can block your users from leaving your application when they have unsaved changes for both internal and external links.

It uses LocationChanging under the hood, plus a little bit of JavaScript to catch every feasible scenario where your users could be leaving your page (or site).

Here’s how to use it on your page/component.

<NavigationLock ConfirmExternalNavigation="@changed"/>

With this, any attempt by the user to head to an external URL (an address outside your application) will be blocked if thechanged boolean is true.

Unlike LocationChanging, this will kick in when the user:

  • Directly changes the address in their browser’s address bar
  • Follows a link to an external site

For example, let’s say we have a contact form and want to warn users if they attempt to navigate away when the form has unsubmitted changes:

ContactForm.razor

@page "/NavigationScenarios"
@implements IDisposable

<NavigationLock ConfirmExternalNavigation="@changed"/>

<EditForm EditContext="editContext" OnValidSubmit="Submit" class="container w-50">
    <div class="row">
        <label class="form-label">
            Email:
            <InputText @bind-Value="contactFormModel.Email" class="form-control"/>
        </label>
    </div>
    <div class="row">
        <label class="form-label">
            Message:
            <InputTextArea @bind-Value="contactFormModel.Message" class="form-control"/>
        </label>
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
    
    <!-- a handy link we can use to check the lock is working -->
    <div class="row mt-2">
        <a href="https://microsoft.com">Check out Microsoft.com</a>
    </div>
</EditForm>

This a standard Blazor EditForm with a little Bootstrap styling to make it look respectable in the browser.

Because we’re using an EditContext to drive the form, we can wire up a handler to fire when any field is modified:

@code {
    
    private bool changed = false;

    private ContactForm contactFormModel = new();
    private EditContext? editContext;

    protected override void OnInitialized()
    {
        editContext = new EditContext(contactFormModel);
        editContext.OnFieldChanged += EditContextOnOnFieldChanged;        
    }
    
    private void EditContextOnOnFieldChanged(object sender, FieldChangedEventArgs e)
    {
        changed = true;
    }    

    public void Dispose()
    {
        editContext.OnFieldChanged -= EditContextOnOnFieldChanged;
    }
    
    ...
    
}

As ever, we need to be careful to detach the handler when our component is disposed.

In this EditContextOnFieldChanged handler changed is set to true whenever the user interacts with any of the form’s fields.

Finally we can reset changed back to false when the form is successfully submitted.

@code {
    
    ...
    
    private void Submit()
    {
        changed = false;
    }
    
}

With this in place, if the user attempts to navigate directly to https://microsoft.com after entering values into the form, NavigationLock will kick in and warn them using a standard browser dialog:

A dialog window with the title

The same goes if they attempt to refresh the page:

A dialog window with the title

It’s worth noting we don’t have any control over the appearance or wording of these messages; they’re baked into the browser.

Navigation Lock can also be used to prevent internal navigation.

Because external and internal navigation is handled differently (within Blazor and the browser) we have to explicitly wire up a handler for internal navigation.

<NavigationLock OnBeforeInternalNavigation="BeforeInternalNavigation"/>

In the handler we can accept a parameter of type LocationChangingContext:

@code {
    
    private bool changed = false;
    
    private void BeforeInternalNavigation(LocationChangingContext context)
    {
        if(changed)
            context.PreventNavigation();
    }
    
}

We can then use this context and call PreventNavigation whenever we want to stop the navigation from taking place.

This will silently abandon the navigation, but chances are we want to let the user know why they’re being blocked from leaving the page.

Custom Confirmation UI

Whether we’re hooking up a handler to Navigation Manager’s LocationChanging event or assigning a handler to a Navigation Lock component’s OnBeforeInternalNavigation parameter, the chances are we want a way to display a message when we prevent navigation.

There are a few ways to implement this:

  • We can build our own UI to prompt the user for confirmation.
  • We can “cheat” and invoke the standard JavaScript confirmation dialog:
private async Task BeforeInternalNavigation(LocationChangingContext context)
{
    if (changed)
    {
		var proceed =  await JsRuntime.InvokeAsync<bool>("confirm", "Are you sure you want to continue?");

    	if(!proceed)
    		context.PreventNavigation();
    }
}

Even better, if we’re using a component library, we can use built-in functionality—such as Telerik’s Dialogs feature:

@code {
    
    [CascadingParameter]
    public DialogFactory Dialogs { get; set; }
    
    private async Task BeforeInternalNavigation(LocationChangingContext context)
	{   
        if (changed)
    	{
            var proceed = await Dialogs.ConfirmAsync("You have unsaved changes, are you sure you want to leave this page?");
            
            if(!proceed)
    			context.PreventNavigation();
        }    	
    }
    
}

In Summary

The new LocationChanging event and NavigationLock component make it much easier to intercept both internal and external navigation events in order to run custom business logic.

You can decide whether to cancel navigation and offer users a choice whether to proceed or not, according to your application’s needs.

LocationChanging is limited to internal navigation and external navigation that originates from NavigationManager (will not fire for direct links to external sites, nor when the user directly enters a new address in their browser’s address bar).

NavigationLock can handle both internal and external links, effectively blocking any attempt by the user to navigate to another URL.

When blocking external navigation using NavigationLock, you’re limited to the browser’s own “lock” UI. When blocking internal navigation, you can implement custom UI to warn the user and offer them a choice whether to proceed.


Jon Hilton
About the Author

Jon Hilton

Jon spends his days building applications using Microsoft technologies (plus, whisper it quietly, a little bit of JavaScript) and his spare time helping developers level up their skills and knowledge via his blog, courses and books. He's especially passionate about enabling developers to build better web applications by mastering the tools available to them. Follow him on Twitter here.

Related Posts

Comments

Comments are disabled in preview mode.