File: Routing\NavigationLock.cs
Web Access
Project: src\src\Components\Web\src\Microsoft.AspNetCore.Components.Web.csproj (Microsoft.AspNetCore.Components.Web)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Globalization;
using Microsoft.JSInterop;
 
namespace Microsoft.AspNetCore.Components.Routing;
 
/// <summary>
/// A component that can be used to intercept navigation events. 
/// </summary>
public sealed class NavigationLock : IComponent, IHandleAfterRender, IAsyncDisposable
{
    private readonly string _id = Guid.NewGuid().ToString("D", CultureInfo.InvariantCulture);
 
    private RenderHandle _renderHandle;
    private IDisposable? _locationChangingRegistration;
    private bool _hasLocationChangingHandler;
    private bool _confirmExternalNavigation;
 
    private bool HasLocationChangingHandler => OnBeforeInternalNavigation.HasDelegate;
 
    [Inject]
    private IJSRuntime JSRuntime { get; set; } = default!;
 
    [Inject]
    private NavigationManager NavigationManager { get; set; } = default!;
 
    /// <summary>
    /// Gets or sets a callback to be invoked when an internal navigation event occurs.
    /// </summary>
    [Parameter]
    public EventCallback<LocationChangingContext> OnBeforeInternalNavigation { get; set; }
 
    /// <summary>
    /// Gets or sets whether a browser dialog should prompt the user to either confirm or cancel
    /// external navigations.
    /// </summary>
    [Parameter]
    public bool ConfirmExternalNavigation { get; set; }
 
    void IComponent.Attach(RenderHandle renderHandle)
    {
        _renderHandle = renderHandle;
    }
 
    Task IComponent.SetParametersAsync(ParameterView parameters)
    {
        foreach (var parameter in parameters)
        {
            if (parameter.Name.Equals(nameof(OnBeforeInternalNavigation), StringComparison.OrdinalIgnoreCase))
            {
                OnBeforeInternalNavigation = (EventCallback<LocationChangingContext>)parameter.Value;
            }
            else if (parameter.Name.Equals(nameof(ConfirmExternalNavigation), StringComparison.OrdinalIgnoreCase))
            {
                ConfirmExternalNavigation = (bool)parameter.Value;
            }
            else
            {
                throw new ArgumentException($"The component '{nameof(NavigationLock)}' does not accept a parameter with the name '{parameter.Name}'.");
            }
        }
 
        if (_hasLocationChangingHandler != HasLocationChangingHandler ||
            _confirmExternalNavigation != ConfirmExternalNavigation)
        {
            _renderHandle.Render(static builder => { });
        }
 
        return Task.CompletedTask;
    }
 
    async Task IHandleAfterRender.OnAfterRenderAsync()
    {
        if (_hasLocationChangingHandler != HasLocationChangingHandler)
        {
            _hasLocationChangingHandler = HasLocationChangingHandler;
            _locationChangingRegistration?.Dispose();
            _locationChangingRegistration = _hasLocationChangingHandler
                ? NavigationManager.RegisterLocationChangingHandler(OnLocationChanging)
                : null;
        }
 
        if (_confirmExternalNavigation != ConfirmExternalNavigation)
        {
            _confirmExternalNavigation = ConfirmExternalNavigation;
            if (_confirmExternalNavigation)
            {
                await JSRuntime.InvokeVoidAsync(NavigationLockInterop.EnableNavigationPrompt, _id);
            }
            else
            {
                await JSRuntime.InvokeVoidAsync(NavigationLockInterop.DisableNavigationPrompt, _id);
            }
        }
    }
 
    private async ValueTask OnLocationChanging(LocationChangingContext context)
    {
        await OnBeforeInternalNavigation.InvokeAsync(context);
    }
 
    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        _locationChangingRegistration?.Dispose();
 
        if (_confirmExternalNavigation)
        {
            try
            {
                await JSRuntime.InvokeVoidAsync(NavigationLockInterop.DisableNavigationPrompt, _id);
            }
            catch (JSDisconnectedException)
            {
                // If the browser is gone, we don't need it to clean up any browser-side state
            }
        }
    }
}