// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable warnings
using OSS.Application.WebServices;
using System.Reflection;
using System.Reflection.Metadata;
using System.Runtime.ExceptionServices;
using Microsoft.AspNetCore.Components.HotReload;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
namespace OSS.Blazor.Components.Routing;
// NOTE: the 'region' sections contain code added to support the TreeItem/NavMenu type route decoder
///
/// A component that supplies route data corresponding to the current navigation state.
///
public partial class OSSRouter : IComponent, IHandleAfterRender, IDisposable
{
// Dictionary is intentionally used instead of ReadOnlyDictionary to reduce Blazor size
static readonly IReadOnlyDictionary _emptyParametersDictionary
= new Dictionary();
RenderHandle _renderHandle;
string _baseUri;
string _locationAbsolute;
bool _navigationInterceptionEnabled;
ILogger _logger;
private CancellationTokenSource _onNavigateCts;
private Task _previousOnNavigateTask = Task.CompletedTask;
private RouteKey _routeTableLastBuiltForRouteKey;
private bool _onNavigateCalled;
#region Custom Route Suport
[Parameter] public int RouteContextOptions { get; set; } = 1; // <-- options when constructing a RouteContext class model
[Parameter] public Action CustomRoute { get; set; } // <-- custom user-specific router delegate
[Parameter] public int CustomRouteOrder { get; set; } = 1; // <-- for custom user-specific type routing
[Parameter] public int TemplateRouteOrder { get; set; } = 1; // <-- for @page or RouteAttribute type routing
#endregion
[Inject] private NavigationManager NavigationManager { get; set; }
[Inject] private INavigationInterception NavigationInterception { get; set; }
[Inject] private ILoggerFactory LoggerFactory { get; set; }
///
/// Gets or sets the assembly that should be searched for components matching the URI.
///
[Parameter]
[EditorRequired]
public Assembly AppAssembly { get; set; }
///
/// Gets or sets a collection of additional assemblies that should be searched for components
/// that can match URIs.
///
[Parameter] public IEnumerable AdditionalAssemblies { get; set; }
///
/// Gets or sets the content to display when no match is found for the requested route.
///
[Parameter]
[EditorRequired]
public RenderFragment NotFound { get; set; }
///
/// Gets or sets the content to display when a match is found for the requested route.
///
[Parameter]
[EditorRequired]
public RenderFragment Found { get; set; }
///
/// Get or sets the content to display when asynchronous navigation is in progress.
///
[Parameter] public RenderFragment? Navigating { get; set; }
///
/// Gets or sets a handler that should be called before navigating to a new page.
///
[Parameter] public EventCallback OnNavigateAsync { get; set; }
///
/// Gets or sets a flag to indicate whether route matching should prefer exact matches
/// over wildcards.
/// This property is obsolete and configuring it does nothing.
///
[Parameter] public bool PreferExactMatches { get; set; }
private RouteTable Routes { get; set; }
///
public void Attach(RenderHandle renderHandle)
{
_logger = LoggerFactory.CreateLogger();
_renderHandle = renderHandle;
_baseUri = NavigationManager.BaseUri;
_locationAbsolute = NavigationManager.Uri;
NavigationManager.LocationChanged += OnLocationChanged;
if (HotReloadManager.Default.MetadataUpdateSupported)
{
HotReloadManager.Default.OnDeltaApplied += ClearRouteCaches;
}
}
///
public async Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
if (AppAssembly == null)
{
throw new InvalidOperationException($"The {nameof(OSSRouter)} component requires a value for the parameter {nameof(AppAssembly)}.");
}
// Found content is mandatory, because even though we could use something like as a
// reasonable default, if it's not declared explicitly in the template then people will have no way
// to discover how to customize this (e.g., to add authorization).
if (Found == null)
{
throw new InvalidOperationException($"The {nameof(OSSRouter)} component requires a value for the parameter {nameof(Found)}.");
}
// NotFound content is mandatory, because even though we could display a default message like "Not found",
// it has to be specified explicitly so that it can also be wrapped in a specific layout
if (NotFound == null)
{
throw new InvalidOperationException($"The {nameof(OSSRouter)} component requires a value for the parameter {nameof(NotFound)}.");
}
if (!_onNavigateCalled)
{
_onNavigateCalled = true;
await RunOnNavigateAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute), isNavigationIntercepted: false);
}
Refresh(isNavigationIntercepted: false);
}
///
public void Dispose()
{
NavigationManager.LocationChanged -= OnLocationChanged;
if (HotReloadManager.Default.MetadataUpdateSupported)
{
HotReloadManager.Default.OnDeltaApplied -= ClearRouteCaches;
}
}
private static string TrimQueryOrHash(string str)
{
var firstIndex = str.AsSpan().IndexOfAny('?', '#');
return firstIndex < 0
? str
: str.Substring(0, firstIndex);
}
private void RefreshRouteTable()
{
var routeKey = new RouteKey(AppAssembly, AdditionalAssemblies);
if (!routeKey.Equals(_routeTableLastBuiltForRouteKey))
{
Routes = RouteTableFactory.Create(routeKey);
_routeTableLastBuiltForRouteKey = routeKey;
}
}
private void ClearRouteCaches()
{
RouteTableFactory.ClearCaches();
_routeTableLastBuiltForRouteKey = default;
}
internal virtual void Refresh(bool isNavigationIntercepted)
{
// If an `OnNavigateAsync` task is currently in progress, then wait
// for it to complete before rendering. Note: because _previousOnNavigateTask
// is initialized to a CompletedTask on initialization, this will still
// allow first-render to complete successfully.
if (_previousOnNavigateTask.Status != TaskStatus.RanToCompletion)
{
if (Navigating != null)
{
_renderHandle.Render(Navigating);
}
return;
}
RefreshRouteTable();
#region Custom Route Suport
//*** RouteContext class has been enhanced ***
var baseRelativePath = NavigationManager.ToBaseRelativePath(_locationAbsolute); // <-- (get PathAndQuery)
var context = new RouteContext(baseRelativePath, isNavigationIntercepted, RouteContextOptions);
//-------------------------------------------------------------------
Action templateRoute = Routes.Route;
if (CustomRouteOrder > 0 && CustomRouteOrder >= TemplateRouteOrder)
{
CustomRoute?.Invoke(context);
if (TemplateRouteOrder > 0 && context.Handler == null && !context.EndRouting)
{
templateRoute?.Invoke(context);
}
}
else if (TemplateRouteOrder > 0 && TemplateRouteOrder >= CustomRouteOrder)
{
templateRoute?.Invoke(context);
if (CustomRouteOrder > 0 && context.Handler == null && !context.EndRouting)
{
CustomRoute?.Invoke(context);
}
}
else
{
// TODO - error. must have at least one context route to invoke
}
#endregion
if (context.Handler != null)
{
if (!typeof(IComponent).IsAssignableFrom(context.Handler))
{
throw new InvalidOperationException($"The type {context.Handler.FullName} " +
$"does not implement {typeof(IComponent).FullName}.");
}
// Log.NavigatingToComponent(_logger, context.Handler, locationPath, _baseUri);
var routeData = new Microsoft.AspNetCore.Components.RouteData(
context.Handler,
context.Parameters ?? _emptyParametersDictionary);
_renderHandle.Render(Found(routeData));
}
else
{
if (!isNavigationIntercepted)
{
// Log.DisplayingNotFound(_logger, locationPath, _baseUri);
// We did not find a Component that matches the route.
// Only show the NotFound content if the application developer programatically got us here i.e we did not
// intercept the navigation. In all other cases, force a browser navigation since this could be non-Blazor content.
_renderHandle.Render(NotFound);
}
else
{
// Log.NavigatingToExternalUri(_logger, _locationAbsolute, locationPath, _baseUri);
NavigationManager.NavigateTo(_locationAbsolute, forceLoad: true);
}
}
}
internal async ValueTask RunOnNavigateAsync(string path, bool isNavigationIntercepted)
{
// Cancel the CTS instead of disposing it, since disposing does not
// actually cancel and can cause unintended Object Disposed Exceptions.
// This effectively cancels the previously running task and completes it.
_onNavigateCts?.Cancel();
// Then make sure that the task has been completely cancelled or completed
// before starting the next one. This avoid race conditions where the cancellation
// for the previous task was set but not fully completed by the time we get to this
// invocation.
await _previousOnNavigateTask;
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
_previousOnNavigateTask = tcs.Task;
if (!OnNavigateAsync.HasDelegate)
{
Refresh(isNavigationIntercepted);
}
_onNavigateCts = new CancellationTokenSource();
var navigateContext = new NavigationContext(path, _onNavigateCts.Token);
var cancellationTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
navigateContext.CancellationToken.Register(state =>
((TaskCompletionSource)state).SetResult(), cancellationTcs);
try
{
// Task.WhenAny returns a Task so we need to await twice to unwrap the exception
var task = await Task.WhenAny(OnNavigateAsync.InvokeAsync(navigateContext), cancellationTcs.Task);
await task;
tcs.SetResult();
Refresh(isNavigationIntercepted);
}
catch (Exception e)
{
_renderHandle.Render(builder => ExceptionDispatchInfo.Throw(e));
}
}
private void OnLocationChanged(object sender, LocationChangedEventArgs args)
{
_locationAbsolute = args.Location;
#region Custom Route Suport
// original code: if (_renderHandle.IsInitialized && Routes != null)
// revised code: if (_renderHandle.IsInitialized && (Routes != null || CustomRoute != null)) <-- not sure if this is needed
#endregion
if (_renderHandle.IsInitialized && (Routes != null || CustomRoute != null)) // <-- not sure if including " || CustomRoute != null" is needed
{
_ = RunOnNavigateAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute), args.IsNavigationIntercepted).Preserve();
}
}
Task IHandleAfterRender.OnAfterRenderAsync()
{
if (!_navigationInterceptionEnabled)
{
_navigationInterceptionEnabled = true;
return NavigationInterception.EnableNavigationInterceptionAsync();
}
return Task.CompletedTask;
}
//private static partial class Log
//{
// [LoggerMessage(1, LogLevel.Debug, $"Displaying {nameof(NotFound)} because path '{{Path}}' with base URI '{{BaseUri}}' does not match any component route", EventName = "DisplayingNotFound")]
// internal static partial void DisplayingNotFound(ILogger logger, string path, string baseUri);
// [LoggerMessage(2, LogLevel.Debug, "Navigating to component {ComponentType} in response to path '{Path}' with base URI '{BaseUri}'", EventName = "NavigatingToComponent")]
// internal static partial void NavigatingToComponent(ILogger logger, Type componentType, string path, string baseUri);
// [LoggerMessage(3, LogLevel.Debug, "Navigating to non-component URI '{ExternalUri}' in response to path '{Path}' with base URI '{BaseUri}'", EventName = "NavigatingToExternalUri")]
// internal static partial void NavigatingToExternalUri(ILogger logger, string externalUri, string path, string baseUri);
//}
}