File: Circuits\RemoteNavigationManager.cs
Web Access
Project: src\src\Components\Server\src\Microsoft.AspNetCore.Components.Server.csproj (Microsoft.AspNetCore.Components.Server)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
using Interop = Microsoft.AspNetCore.Components.Web.BrowserNavigationManagerInterop;
 
namespace Microsoft.AspNetCore.Components.Server.Circuits;
 
/// <summary>
/// A Server-Side Blazor implementation of <see cref="NavigationManager"/>.
/// </summary>
internal sealed partial class RemoteNavigationManager : NavigationManager, IHostEnvironmentNavigationManager
{
    private readonly ILogger<RemoteNavigationManager> _logger;
    private IJSRuntime _jsRuntime;
    private bool? _navigationLockStateBeforeJsRuntimeAttached;
 
    public event EventHandler<Exception>? UnhandledException;
 
    /// <summary>
    /// Creates a new <see cref="RemoteNavigationManager"/> instance.
    /// </summary>
    /// <param name="logger">The <see cref="ILogger{TCategoryName}"/>.</param>
    public RemoteNavigationManager(ILogger<RemoteNavigationManager> logger)
    {
        _logger = logger;
    }
 
    /// <summary>
    /// Gets or sets whether the circuit has an attached <see cref="IJSRuntime"/>.
    /// </summary>
    public bool HasAttachedJSRuntime => _jsRuntime != null;
 
    /// <summary>
    /// Initializes the <see cref="NavigationManager" />.
    /// </summary>
    /// <param name="baseUri">The base URI.</param>
    /// <param name="uri">The absolute URI.</param>
    public new void Initialize(string baseUri, string uri)
    {
        base.Initialize(baseUri, uri);
        NotifyLocationChanged(isInterceptedLink: false);
    }
 
    /// <summary>
    /// Initializes the <see cref="RemoteNavigationManager"/>.
    /// </summary>
    /// <param name="jsRuntime">The <see cref="IJSRuntime"/> to use for interoperability.</param>
    public void AttachJsRuntime(IJSRuntime jsRuntime)
    {
        if (_jsRuntime != null)
        {
            throw new InvalidOperationException("JavaScript runtime already initialized.");
        }
 
        _jsRuntime = jsRuntime;
 
        if (_navigationLockStateBeforeJsRuntimeAttached.HasValue)
        {
            _ = SetHasLocationChangingListenersAsync(_navigationLockStateBeforeJsRuntimeAttached.Value);
            _navigationLockStateBeforeJsRuntimeAttached = null;
        }
    }
 
    public void NotifyLocationChanged(string uri, string state, bool intercepted)
    {
        Log.ReceivedLocationChangedNotification(_logger, uri, intercepted);
 
        Uri = uri;
        HistoryEntryState = state;
        NotifyLocationChanged(intercepted);
    }
 
    public async ValueTask<bool> HandleLocationChangingAsync(string uri, string? state, bool intercepted)
    {
        return await NotifyLocationChangingAsync(uri, state, intercepted);
    }
 
    /// <inheritdoc />
    [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(NavigationOptions))]
    protected override void NavigateToCore(string uri, NavigationOptions options)
    {
        Log.RequestingNavigation(_logger, uri, options);
 
        if (_jsRuntime == null)
        {
            var absoluteUriString = ToAbsoluteUri(uri).AbsoluteUri;
            throw new NavigationException(absoluteUriString);
        }
 
        _ = PerformNavigationAsync();
 
        async Task PerformNavigationAsync()
        {
            try
            {
                var shouldContinueNavigation = await NotifyLocationChangingAsync(uri, options.HistoryEntryState, false);
 
                if (!shouldContinueNavigation)
                {
                    Log.NavigationCanceled(_logger, uri);
                    return;
                }
 
                await _jsRuntime.InvokeVoidAsync(Interop.NavigateTo, uri, options);
                Log.NavigationCompleted(_logger, uri);
            }
            catch (TaskCanceledException)
            when (_jsRuntime is RemoteJSRuntime remoteRuntime && remoteRuntime.IsPermanentlyDisconnected)
            {
                Log.NavigationStoppedSessionEnded(_logger, uri);
            }
            catch (Exception ex)
            {
                // We shouldn't ever reach this since exceptions thrown from handlers are handled in HandleLocationChangingHandlerException.
                // But if some other exception gets thrown, we still want to know about it.
                Log.NavigationFailed(_logger, uri, ex);
                UnhandledException?.Invoke(this, ex);
            }
        }
    }
 
    /// <inheritdoc />
    public override void Refresh(bool forceReload = false)
    {
        if (_jsRuntime == null)
        {
            var absoluteUriString = ToAbsoluteUri(Uri).AbsoluteUri;
            throw new NavigationException(absoluteUriString);
        }
 
        _ = RefreshAsync();
 
        async Task RefreshAsync()
        {
            try
            {
                await _jsRuntime.InvokeVoidAsync(Interop.Refresh, forceReload);
            }
            catch (Exception ex)
            {
                Log.RefreshFailed(_logger, ex);
                UnhandledException?.Invoke(this, ex);
            }
        }
    }
 
    protected override void HandleLocationChangingHandlerException(Exception ex, LocationChangingContext context)
    {
        Log.NavigationFailed(_logger, context.TargetLocation, ex);
        UnhandledException?.Invoke(this, ex);
    }
 
    protected override void SetNavigationLockState(bool value)
    {
        if (_jsRuntime is null)
        {
            _navigationLockStateBeforeJsRuntimeAttached = value;
            return;
        }
 
        _ = SetHasLocationChangingListenersAsync(value);
    }
 
    private async Task SetHasLocationChangingListenersAsync(bool value)
    {
        try
        {
            await _jsRuntime.InvokeVoidAsync(Interop.SetHasLocationChangingListeners, WebRendererId.Server, value);
        }
        catch (JSDisconnectedException)
        {
            // If the browser is gone, we don't need it to clean up any browser-side state
        }
    }
 
    private static partial class Log
    {
        [LoggerMessage(1, LogLevel.Debug, "Requesting navigation to URI {Uri} with forceLoad={ForceLoad}, replace={Replace}", EventName = "RequestingNavigation")]
        private static partial void RequestingNavigation(ILogger logger, string uri, bool forceLoad, bool replace);
 
        public static void RequestingNavigation(ILogger logger, string uri, NavigationOptions options)
            => RequestingNavigation(logger, uri, options.ForceLoad, options.ReplaceHistoryEntry);
 
        [LoggerMessage(2, LogLevel.Debug, "Received notification that the URI has changed to {Uri} with isIntercepted={IsIntercepted}", EventName = "ReceivedLocationChangedNotification")]
        public static partial void ReceivedLocationChangedNotification(ILogger logger, string uri, bool isIntercepted);
 
        [LoggerMessage(3, LogLevel.Debug, "Navigation canceled when changing the location to {Uri}", EventName = "NavigationCanceled")]
        public static partial void NavigationCanceled(ILogger logger, string uri);
 
        [LoggerMessage(4, LogLevel.Error, "Navigation failed when changing the location to {Uri}", EventName = "NavigationFailed")]
        public static partial void NavigationFailed(ILogger logger, string uri, Exception exception);
 
        [LoggerMessage(5, LogLevel.Error, "Failed to refresh", EventName = "RefreshFailed")]
        public static partial void RefreshFailed(ILogger logger, Exception exception);
 
        [LoggerMessage(6, LogLevel.Debug, "Navigation completed when changing the location to {Uri}", EventName = "NavigationCompleted")]
        public static partial void NavigationCompleted(ILogger logger, string uri);
 
        [LoggerMessage(7, LogLevel.Debug, "Navigation stopped because the session ended when navigating to {Uri}", EventName = "NavigationStoppedSessionEnded")]
        public static partial void NavigationStoppedSessionEnded(ILogger logger, string uri);
    }
}