Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/Components/Components/src/IEndpointHtmlRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Components;

/// <summary>
/// An interface for EndpointHtmlRenderer implementations that allows NavigationManagers call renderer's methods
/// </summary>
public interface IEndpointHtmlRenderer
{
/// <summary>
/// Sets the html response to 404 Not Found.
/// </summary>
void SetNotFoundResponse();
}
192 changes: 192 additions & 0 deletions src/Components/Components/src/NavigationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,29 @@ public event EventHandler<LocationChangedEventArgs> LocationChanged

private CancellationTokenSource? _locationChangingCts;

/// <summary>
/// An event that fires when the page is not found.
/// </summary>
public event EventHandler<EventArgs> NotFoundEvent
{
add
{
AssertInitialized();
_notFound += value;
}
remove
{
AssertInitialized();
_notFound -= value;
}
}

private EventHandler<EventArgs>? _notFound;

private readonly List<Func<NotFoundContext, ValueTask>> _notFoundHandlers = new();

private CancellationTokenSource? _notFoundCts;

// For the baseUri it's worth storing as a System.Uri so we can do operations
// on that type. System.Uri gives us access to the original string anyway.
private Uri? _baseUri;
Expand Down Expand Up @@ -177,6 +200,16 @@ protected virtual void NavigateToCore([StringSyntax(StringSyntaxAttribute.Uri)]
public virtual void Refresh(bool forceReload = false)
=> NavigateTo(Uri, forceLoad: true, replace: true);

/// <summary>
/// Handles setting the NotFound state.
/// </summary>
public virtual void NotFound() => NotFoundCore();

/// <summary>
/// Handles setting the NotFound state.
/// </summary>
protected virtual void NotFoundCore() => throw new NotImplementedException();

/// <summary>
/// Called to initialize BaseURI and current URI before these values are used for the first time.
/// Override <see cref="EnsureInitialized" /> and call this method to dynamically calculate these values.
Expand Down Expand Up @@ -308,6 +341,21 @@ protected void NotifyLocationChanged(bool isInterceptedLink)
}
}

/// <summary>
/// Triggers the <see cref="NotFound"/> event with the current URI value.
/// </summary>
protected void NotifyNotFound()
{
try
{
_notFound?.Invoke(this, new EventArgs());
}
catch (Exception ex)
{
throw new NotFoundRenderingException("An exception occurred while dispatching a NotFound event.", ex);
}
}

/// <summary>
/// Notifies the registered handlers of the current location change.
/// </summary>
Expand Down Expand Up @@ -439,6 +487,126 @@ protected async ValueTask<bool> NotifyLocationChangingAsync(string uri, string?
}
}

/// <summary>
/// Notifies the registered handlers of the current ot found event.
/// </summary>
/// <returns>A <see cref="ValueTask{TResult}"/> representing the completion of the operation. If the result is <see langword="true"/>, the navigation should continue.</returns>
protected async ValueTask<bool> NotifyNotFoundAsync()
{
_notFoundCts?.Cancel();
_notFoundCts = null;

var handlerCount = _notFoundHandlers.Count;

if (handlerCount == 0)
{
return true;
}

var cts = new CancellationTokenSource();

_notFoundCts = cts;

var cancellationToken = cts.Token;
var context = new NotFoundContext
{
CancellationToken = cancellationToken,
};

try
{
if (handlerCount == 1)
{
var handlerTask = InvokeNotFoundHandlerAsync(_notFoundHandlers[0], context);

if (handlerTask.IsFaulted)
{
await handlerTask;
return false; // Unreachable because the previous line will throw.
}

if (context.DidPreventRendering)
{
return false;
}

if (!handlerTask.IsCompletedSuccessfully)
{
await handlerTask.AsTask().WaitAsync(cancellationToken);
}
}
else
{
var notFoundHandlersCopy = ArrayPool<Func<NotFoundContext, ValueTask>>.Shared.Rent(handlerCount);

try
{
_notFoundHandlers.CopyTo(notFoundHandlersCopy);

var notFoundTasks = new HashSet<Task>();

for (var i = 0; i < handlerCount; i++)
{
var handlerTask = InvokeNotFoundHandlerAsync(notFoundHandlersCopy[i], context);

if (handlerTask.IsFaulted)
{
await handlerTask;
return false; // Unreachable because the previous line will throw.
}

if (context.DidPreventRendering)
{
return false;
}

notFoundTasks.Add(handlerTask.AsTask());
}

while (notFoundTasks.Count != 0)
{
var completedHandlerTask = await Task.WhenAny(notFoundTasks).WaitAsync(cancellationToken);

if (completedHandlerTask.IsFaulted)
{
await completedHandlerTask;
return false; // Unreachable because the previous line will throw.
}

notFoundTasks.Remove(completedHandlerTask);
}
}
finally
{
ArrayPool<Func<NotFoundContext, ValueTask>>.Shared.Return(notFoundHandlersCopy);
}
}

return !context.DidPreventRendering;
}
catch (TaskCanceledException ex)
{
if (ex.CancellationToken == cancellationToken)
{
// This navigation was in progress when a successive navigation occurred.
// We treat this as a canceled navigation.
return false;
}

throw;
}
finally
{
cts.Cancel();
cts.Dispose();

if (_notFoundCts == cts)
{
_notFoundCts = null;
}
}
}

private async ValueTask InvokeLocationChangingHandlerAsync(Func<LocationChangingContext, ValueTask> handler, LocationChangingContext context)
{
try
Expand All @@ -455,6 +623,22 @@ private async ValueTask InvokeLocationChangingHandlerAsync(Func<LocationChanging
}
}

private async ValueTask InvokeNotFoundHandlerAsync(Func<NotFoundContext, ValueTask> handler, NotFoundContext context)
{
try
{
await handler(context);
}
catch (OperationCanceledException)
{
// Ignore exceptions caused by cancellations.
}
catch (Exception ex)
{
HandleNotFoundHandlerException(ex, context);
}
}

/// <summary>
/// Handles exceptions thrown in location changing handlers.
/// </summary>
Expand All @@ -463,6 +647,14 @@ private async ValueTask InvokeLocationChangingHandlerAsync(Func<LocationChanging
protected virtual void HandleLocationChangingHandlerException(Exception ex, LocationChangingContext context)
=> throw new InvalidOperationException($"To support navigation locks, {GetType().Name} must override {nameof(HandleLocationChangingHandlerException)}");

/// <summary>
/// Handles exceptions thrown in NotFound rendering handlers.
/// </summary>
/// <param name="ex">The exception to handle.</param>
/// <param name="context">The context passed to the handler.</param>
protected virtual void HandleNotFoundHandlerException(Exception ex, NotFoundContext context)
=> throw new InvalidOperationException($"To support not found rendering locks, {GetType().Name} must override {nameof(HandleNotFoundHandlerException)}");

/// <summary>
/// Sets whether navigation is currently locked. If it is, then implementations should not update <see cref="Uri"/> and call
/// <see cref="NotifyLocationChanged(bool)"/> until they have first confirmed the navigation by calling
Expand Down
20 changes: 20 additions & 0 deletions src/Components/Components/src/NotFoundRenderingException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Components;

/// <summary>
/// Exception thrown when an <see cref="NavigationManager"/> is not able to render not found page.
/// </summary>
public class NotFoundRenderingException : Exception
{
/// <summary>
/// Creates a new instance of <see cref="NotFoundRenderingException"/>.
/// </summary>
/// <param name="message">The exception message.</param>
/// <param name="innerException">The inner exception.</param>
public NotFoundRenderingException(string message, Exception innerException)
: base(message, innerException)
{
}
}
16 changes: 16 additions & 0 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
#nullable enable
Microsoft.AspNetCore.Components.IEndpointHtmlRenderer
Microsoft.AspNetCore.Components.IEndpointHtmlRenderer.SetNotFoundResponse() -> void
Microsoft.AspNetCore.Components.NavigationManager.NotFoundEvent -> System.EventHandler<System.EventArgs!>!
Microsoft.AspNetCore.Components.NavigationManager.NotifyNotFound() -> void
Microsoft.AspNetCore.Components.NavigationManager.NotifyNotFoundAsync() -> System.Threading.Tasks.ValueTask<bool>
Microsoft.AspNetCore.Components.NotFoundRenderingException
Microsoft.AspNetCore.Components.NotFoundRenderingException.NotFoundRenderingException(string! message, System.Exception! innerException) -> void
Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager.Initialize(string! baseUri, string! uri, Microsoft.AspNetCore.Components.IEndpointHtmlRenderer! renderer) -> void
Microsoft.AspNetCore.Components.Routing.NotFoundContext
Microsoft.AspNetCore.Components.Routing.NotFoundContext.CancellationToken.get -> System.Threading.CancellationToken
Microsoft.AspNetCore.Components.Routing.NotFoundContext.CancellationToken.init -> void
Microsoft.AspNetCore.Components.Routing.NotFoundContext.NotFoundContext() -> void
Microsoft.AspNetCore.Components.Routing.NotFoundContext.PreventRendering() -> void
virtual Microsoft.AspNetCore.Components.NavigationManager.HandleNotFoundHandlerException(System.Exception! ex, Microsoft.AspNetCore.Components.Routing.NotFoundContext! context) -> void
virtual Microsoft.AspNetCore.Components.NavigationManager.NotFound() -> void
virtual Microsoft.AspNetCore.Components.NavigationManager.NotFoundCore() -> void
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,12 @@ public interface IHostEnvironmentNavigationManager
/// <param name="baseUri">The base URI.</param>
/// <param name="uri">The absolute URI.</param>
void Initialize(string baseUri, string uri);

/// <summary>
/// Initializes the <see cref="NavigationManager" />.
/// </summary>
/// <param name="baseUri">The base URI.</param>
/// <param name="uri">The absolute URI.</param>
/// <param name="renderer">The renderer.</param>
void Initialize(string baseUri, string uri, IEndpointHtmlRenderer renderer);
}
27 changes: 27 additions & 0 deletions src/Components/Components/src/Routing/NotFoundContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Components.Routing;

/// <summary>
/// Contains context for a change to the browser's current location.
/// </summary>
public sealed class NotFoundContext
{
internal bool DidPreventRendering { get; private set; }

/// <summary>
/// Gets a <see cref="System.Threading.CancellationToken"/> that can be used to determine if this navigation was canceled
/// (for example, because the user has triggered a different navigation).
/// </summary>
public CancellationToken CancellationToken { get; init; }

/// <summary>
/// Prevents this navigation from continuing.
/// </summary>
public void PreventRendering()
{
DidPreventRendering = true;
}

}
14 changes: 14 additions & 0 deletions src/Components/Components/src/Routing/Router.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ public void Attach(RenderHandle renderHandle)
_baseUri = NavigationManager.BaseUri;
_locationAbsolute = NavigationManager.Uri;
NavigationManager.LocationChanged += OnLocationChanged;
NavigationManager.NotFoundEvent += OnNotFound;
RoutingStateProvider = ServiceProvider.GetService<IRoutingStateProvider>();

if (HotReloadManager.Default.MetadataUpdateSupported)
Expand Down Expand Up @@ -146,6 +147,7 @@ public async Task SetParametersAsync(ParameterView parameters)
public void Dispose()
{
NavigationManager.LocationChanged -= OnLocationChanged;
NavigationManager.NotFoundEvent -= OnNotFound;
if (HotReloadManager.Default.MetadataUpdateSupported)
{
HotReloadManager.Default.OnDeltaApplied -= ClearRouteCaches;
Expand Down Expand Up @@ -320,6 +322,15 @@ private void OnLocationChanged(object sender, LocationChangedEventArgs args)
}
}

private void OnNotFound(object sender, EventArgs args)
{
if (_renderHandle.IsInitialized)
{
Log.DisplayingNotFound(_logger);
_renderHandle.Render(NotFound ?? DefaultNotFoundContent);
}
}

async Task IHandleAfterRender.OnAfterRenderAsync()
{
if (!_navigationInterceptionEnabled)
Expand All @@ -340,6 +351,9 @@ 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(1, LogLevel.Debug, $"Displaying {nameof(NotFound)} on request", EventName = "DisplayingNotFoundOnRequest")]
internal static partial void DisplayingNotFound(ILogger logger);

[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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,24 @@ namespace Microsoft.AspNetCore.Components.Endpoints;

internal sealed class HttpNavigationManager : NavigationManager, IHostEnvironmentNavigationManager
{
private IEndpointHtmlRenderer? _renderer;

void IHostEnvironmentNavigationManager.Initialize(string baseUri, string uri) => Initialize(baseUri, uri);

void IHostEnvironmentNavigationManager.Initialize(string baseUri, string uri, IEndpointHtmlRenderer? renderer)
{
Initialize(baseUri, uri);
_renderer = renderer;
}

protected override void NavigateToCore(string uri, NavigationOptions options)
{
var absoluteUriString = ToAbsoluteUri(uri).AbsoluteUri;
throw new NavigationException(absoluteUriString);
}

protected override void NotFoundCore()
{
_renderer?.SetNotFoundResponse();
}
}
Loading
Loading