Skip to content

Unify the UX of template projects on navigation to non-existing page #62067

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

ilonatommy
Copy link
Member

@ilonatommy ilonatommy commented May 22, 2025

Templates use re-execution and NotFound behavior gets unified

Before this PR navigating away to non-existing page by:

  • clicking a link
  • programmatic navigation NavigationManager.NavigateTo(nonExistingPath)
  • browser search bar navigation

was inconsistent with Navigation.NotFound() behavior. See the "before" video:

before.mp4

Now, all of them are behaving the same way, see "after" video:

After.mp4

Description

  1. Adding more re-execution tests: for interactive (server & wasm) and SSR (streaming and non-streaming) scenarios.
  2. Setting up proper re-execution in startup files: no-interactive test did not have it at all and interactive had only partial configuration for re-executed paths in comparison to what was used for non-re-executed paths.
  3. Re-organizing files connected to NotFound tests. There is enough of them to pack them to a separate directory.
  4. Adding re-execution in templates to unify the experience in case of calling Navigation.NotFound(). Previously added NotFound.razor page was not used in the template. In case the user started calling new NotFound method, they saw a different behavior than in the cases of navigating away to non-existing url. It was reported as inconsistent UX by @danroth27.
  5. Standalone WASM template did not have NotFound.razor. We are adding it and adjusting app's Router accordingly.
  6. We used to render NotFound fragment or DefaultNotFoundContnet in Refresh method, skipping NotFoundPage. Now, we follow the same logic there as when handling NotFound event. In practice, it means that e.g. in standalone WASM application, mistyping the url (triggers Refresh method) behaves the same as calling NavigationManager.NotFound() (triggers NotFound event).
  7. NotFoundPage uses layout declared with
@layout NotFoundLayout

To make it work in templates, imports were updated in the following way:

  • standalone WASM always imports Layout - no changes
  • blazor interactivity None -> InteractiveAtRoot=false, UseServer=false, UseWebAssembly=false - Components.Layout added
  • blazor interactivity Server -ai -> InteractiveAtRoot=true, UseServer=true, UseWebAssembly=false - no changes
  • blazor interactivity Server -> InteractiveAtRoot=false, UseServer=true, UseWebAssembly=false - no changes
  • blazor interactivity Wasm -ai -> InteractiveAtRoot=true, UseServer=false, UseWebAssembly=true - Client.Layout added
  • blazor interactivity Wasm -> InteractiveAtRoot=false, UseServer=false, UseWebAssembly=true - Components.Layout added
  • blazor interactivity Auto -ai -> InteractiveAtRoot=true, UseServer=true, UseWebAssembly=true - no changes
  • blazor interactivity Auto -> InteractiveAtRoot=false, UseServer=true, UseWebAssembly=true - no changes.

Router component reads the layout from NotFoundPage attributes and renders NotFoundPage contents in layout's Body. In case there's no layout defined, we keep rendering pure NotFoundPage.

@ilonatommy ilonatommy added this to the 10.0-preview6 milestone May 22, 2025
@ilonatommy ilonatommy requested review from danroth27 and javiercn May 22, 2025 10:32
@ilonatommy ilonatommy self-assigned this May 22, 2025
@ilonatommy ilonatommy requested a review from a team as a code owner May 22, 2025 10:32
@ilonatommy ilonatommy added the area-blazor Includes: Blazor, Razor Components label May 22, 2025
@ilonatommy ilonatommy requested a review from Copilot May 22, 2025 10:35
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

The PR ensures consistent re-execution behavior for navigation to non-existing pages across Blazor templates by configuring status code middleware and adding end-to-end tests.

  • Added UseStatusCodePagesWithReExecute middleware in both Program entrypoints to route 404s to /not-found.
  • Introduced shared test components and organized NotFound tests for interactive, SSR, and streaming scenarios.
  • Refactored startup classes to extract endpoint configuration and added re-execution branches in non-interactive pipelines.

Reviewed Changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/ProjectTemplates/Web.ProjectTemplates/.../Program.cs Added status code pages middleware for 404 handling
src/ProjectTemplates/Web.ProjectTemplates/.../Program.Main.cs Duplicated middleware call in Main variant
src/Components/test/testassets/Components.WasmMinimal/RenderModeHelper.cs Removed namespace, leaving helper in the global namespace
src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs Made ConfigureSubdirPipeline private and extracted ConfigureEndpoints
src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsNoInteractivityStartup.cs Added mapped re-execution branch under /subdir/reexecution
src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs & InteractivityTest.cs New E2E theories for programmatic, link, and browser navigation
Comments suppressed due to low confidence (3)

src/Components/test/testassets/Components.WasmMinimal/RenderModeHelper.cs:8

  • RenderModeHelper is now in the global namespace after removing the original namespace; consider adding an appropriate namespace to keep test helpers organized.
public static class RenderModeHelper

src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs:89

  • Changing ConfigureSubdirPipeline from protected virtual to private removes the ability for subclasses to override it; consider retaining protected visibility or providing a new protected hook.
private void ConfigureSubdirPipeline(IApplicationBuilder app, IWebHostEnvironment env)

src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsNoInteractivityStartup.cs:66

  • It looks like a closing brace for the inner app.Map block was removed, which may lead to mismatched braces and a compile error. Please verify block boundaries and restore the missing brace.
}

@danroth27
Copy link
Member

Instead of requiring re-executing the pipeline, could the Router component handle rendering the Not Found page instead for these cases similar to how things work when the Router is interactive?

@ilonatommy
Copy link
Member Author

ilonatommy commented May 22, 2025

Do template tests operate on older version of framework?

\MyBlazorApp\Program.cs(18,51): error CS1739: The best overload for 'UseStatusCodePagesWithReExecute' does not have a parameter named 'createScopeForErrors'

log


static Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePagesWithReExecute(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, string! pathFormat, bool createScopeForErrors, string? queryFormat = null) -> Microsoft.AspNetCore.Builder.IApplicationBuilder!

edit: We have that API in p5 only, merge with main should help.

@javiercn
Copy link
Member

Instead of requiring re-executing the pipeline, could the Router component handle rendering the Not Found page instead for these cases similar to how things work when the Router is interactive?

You don't get to even execute Blazor if you have a 404 because someone introduced the wrong URL in the address bar or click a link to an inexistent location.

The router will only work for cases where you explicitly invoke NotFound()

@ilonatommy
Copy link
Member Author

ilonatommy commented May 28, 2025

We decided that the missing change to this PR is unified layout of NotFoundPage.

  1. Adding @layout MainLayout directive to template's NotFound.razor does not work:
    we're rendering the layout by simple
builder.OpenComponent(0, NotFoundPage);
builder.CloseComponent();

that does not respect that directive.

As a workaround, we could add a new parameterto the router,

[Parameter] public Type Layout { get; set; }

and then change the rendering code to

builder.OpenComponent<LayoutView>(0);
builder.AddAttribute(1, nameof(LayoutView.Layout), Layout);
builder.AddAttribute(2, nameof(LayoutView.ChildContent), (RenderFragment)(b =>
{
    b.OpenComponent(3, NotFoundPage);
    b.CloseComponent();
}));
builder.CloseComponent();

but then we are forcing the layout to support ChildContnet which is not always the case. Using layout that does not support it, causes serious runtime issues:

System.InvalidOperationException: Object of type 'project.Components.Layout.MainLayout' does not have a property matching the name 'ChildContent'.
  1. Leverage NotFound fragment, maybe even remove NotFoundPage parameter from Router.
<Router AppAssembly="@typeof(Program).Assembly">
  <Found Context="routeData">
    <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
  </Found>
  <NotFound>
    <LayoutView Layout="@typeof(MainLayout)">
      <NotFoundPage />
    </LayoutView>
  </NotFound>
</Router>

We would suggest that this is the proper way of using the NotFoundPage in blazor. Blazor pages that re-executed keep the Found fragment layout in this case, so if NotFound has a different layout, it will not be unified.

edit:
3) Use reflection to get LayoutAttribute of NotFoundPage in SetParametersAsync where we already do a similar thing to check if route parameter is added. I am investigating how far this will get us.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants