diff --git a/src/Components/Components/src/NavigationManager.cs b/src/Components/Components/src/NavigationManager.cs index 25d6da65b94e..987482ece5a7 100644 --- a/src/Components/Components/src/NavigationManager.cs +++ b/src/Components/Components/src/NavigationManager.cs @@ -54,8 +54,6 @@ public event EventHandler OnNotFound private EventHandler? _notFound; - private static readonly NotFoundEventArgs _notFoundEventArgs = new NotFoundEventArgs(); - // 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; @@ -63,6 +61,8 @@ public event EventHandler OnNotFound // The URI. Always represented an absolute URI. private string? _uri; private bool _isInitialized; + internal string NotFoundPageRoute { get; set; } = string.Empty; + internal Type NotFoundPageType { get; set; } = default!; /// /// Gets or sets the current base URI. The is always represented as an absolute URI in string form with trailing slash. @@ -212,7 +212,7 @@ private void NotFoundCore() } else { - _notFound.Invoke(this, _notFoundEventArgs); + _notFound.Invoke(this, new NotFoundEventArgs(NotFoundPageRoute, NotFoundPageType)); // ToDo: sending only the type would be enough and then we would get the route later } } diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 5eb52d2c330e..bb9da6e52ead 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -6,7 +6,9 @@ Microsoft.AspNetCore.Components.NavigationManager.OnNotFound -> System.EventHand Microsoft.AspNetCore.Components.NavigationManager.NotFound() -> void Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager.Initialize(string! baseUri, string! uri, System.Func! onNavigateTo) -> void Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs -Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.NotFoundEventArgs() -> void +Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.NotFoundEventArgs(string! url, System.Type! notFoundPageType) -> void +Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.NotFoundPageType.get -> System.Type! +Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.Path.get -> string! Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.ComponentStatePersistenceManager(Microsoft.Extensions.Logging.ILogger! logger, System.IServiceProvider! serviceProvider) -> void Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.SetPlatformRenderMode(Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> void Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateServiceCollectionExtensions diff --git a/src/Components/Components/src/Routing/NotFoundEventArgs.cs b/src/Components/Components/src/Routing/NotFoundEventArgs.cs index 637bfd442b70..07eca1a9c906 100644 --- a/src/Components/Components/src/Routing/NotFoundEventArgs.cs +++ b/src/Components/Components/src/Routing/NotFoundEventArgs.cs @@ -8,9 +8,23 @@ namespace Microsoft.AspNetCore.Components.Routing; /// public sealed class NotFoundEventArgs : EventArgs { + /// + /// Gets the path of NotFoundPage. + /// + public string Path { get; } + + /// + /// Gets the type of NotFoundPage component. + /// + public Type NotFoundPageType { get; } + /// /// Initializes a new instance of . /// - public NotFoundEventArgs() - { } + public NotFoundEventArgs(string url, Type notFoundPageType) + { + Path = url; + NotFoundPageType = notFoundPageType; + } + } diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index 7a73ead53ea9..0f815973a561 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -18,6 +18,7 @@ namespace Microsoft.AspNetCore.Components.Routing; /// /// A component that supplies route data corresponding to the current navigation state. /// +[StreamRendering] public partial class Router : IComponent, IHandleAfterRender, IDisposable { // Dictionary is intentionally used instead of ReadOnlyDictionary to reduce Blazor size diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index a52f6e274410..8e5338f54788 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -111,13 +111,6 @@ await _renderer.InitializeStandardComponentServicesAsync( ParameterView.Empty, waitForQuiescence: result.IsPost || isErrorHandlerOrReExecuted); - bool avoidStartingResponse = hasStatusCodePage && !isReExecuted && context.Response.StatusCode == StatusCodes.Status404NotFound; - if (avoidStartingResponse) - { - // the request is going to be re-executed, we should avoid writing to the response - return; - } - Task quiesceTask; if (!result.IsPost) { diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index 6af020fc847a..8c60a17f00cf 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Components.Endpoints.Rendering; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; @@ -77,21 +78,36 @@ private Task ReturnErrorResponse(string detailedMessage) : Task.CompletedTask; } - private async Task SetNotFoundResponseAsync(string baseUri) + private async Task SetNotFoundResponseAsync(string baseUri, NotFoundEventArgs args) { if (_httpContext.Response.HasStarted) { + if (args.NotFoundPageType == null || string.IsNullOrEmpty(args.Path)) + { + throw new InvalidOperationException("The NotFoundPageType and Path must be specified in the NotFoundEventArgs to render NotFoundPage when the response has started."); + } + var instance = Activator.CreateInstance(args.NotFoundPageType) as IComponent; + if (instance == null) + { + throw new InvalidOperationException($"The type {args.NotFoundPageType.FullName} does not implement IComponent."); + } + if (_notFoundComponentId == -1) + { + _notFoundComponentId = AssignRootComponentId(instance); + } + if (string.IsNullOrEmpty(_notFoundUrl)) + { + _notFoundUrl = $"{baseUri}{args.Path.TrimStart('/')}"; + } var defaultBufferSize = 16 * 1024; await using var writer = new HttpResponseStreamWriter(_httpContext.Response.Body, Encoding.UTF8, defaultBufferSize, ArrayPool.Shared, ArrayPool.Shared); using var bufferWriter = new BufferedTextWriter(writer); - var notFoundUri = $"{baseUri}not-found"; - HandleNavigationAfterResponseStarted(bufferWriter, _httpContext, notFoundUri); + HandleNotFoundAfterResponseStarted(bufferWriter, _httpContext, _notFoundUrl, _notFoundComponentId); await bufferWriter.FlushAsync(); } else { _httpContext.Response.StatusCode = StatusCodes.Status404NotFound; - _httpContext.Response.ContentType = null; } // When the application triggers a NotFound event, we continue rendering the current batch. diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs index c17f7cd53555..f76b8c873f55 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs @@ -226,6 +226,15 @@ private static void HandleExceptionAfterResponseStarted(HttpContext httpContext, writer.Write(""); } + private static void HandleNotFoundAfterResponseStarted(TextWriter writer, HttpContext httpContext, string notFoundUrl, int componentId) + { + writer.Write(""); + } + private static void HandleNavigationAfterResponseStarted(TextWriter writer, HttpContext httpContext, string destinationUrl) { writer.Write("