Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
11956ec
Add `NotFoundPage`.
ilonatommy Mar 18, 2025
216ec31
Fix rebase error.
ilonatommy Mar 19, 2025
3168993
Draft of template changes.
ilonatommy Mar 21, 2025
067cfd2
Typo errors.
ilonatommy Mar 21, 2025
6d22aad
NavigationManager is not needed for SSR.
ilonatommy Mar 21, 2025
6ea7082
Add BOM to new teamplate files.
ilonatommy Mar 24, 2025
25e8e66
Move instead of exclude.
ilonatommy Mar 24, 2025
1060b4d
Clean up, fix tests.
ilonatommy Mar 24, 2025
fc696c3
Fix
ilonatommy Mar 24, 2025
44e7d8e
Apply smallest possible changes to templates.
ilonatommy Mar 25, 2025
ae68011
Missing changes to baseline.
ilonatommy Mar 25, 2025
39deb2b
Prevent throwing.
ilonatommy Mar 25, 2025
6b620d5
Fix configurations without global router.
ilonatommy Mar 25, 2025
6876252
Merge branch 'main' into fix-58815
ilonatommy Mar 25, 2025
74e3eae
Fix "response started" scenarios.
ilonatommy Mar 26, 2025
e10b90c
Fix template tests.
ilonatommy Mar 26, 2025
b660d19
Fix baseline tests.
ilonatommy Mar 26, 2025
d566c4b
This is a draft of uneffective `UseStatusCodePagesWithReExecute`, cc …
ilonatommy Mar 26, 2025
d41bd5b
Update.
ilonatommy Mar 27, 2025
005f217
Fix reexecution mechanism.
ilonatommy Mar 31, 2025
8c7b6d2
Fix public API.
ilonatommy Mar 31, 2025
f876e4d
Args order.
ilonatommy Mar 31, 2025
8744e8b
Draft of test.
ilonatommy Mar 31, 2025
aee53a9
Per page interactivity test.
ilonatommy Apr 1, 2025
de91b4b
Revert unnecessary change.
ilonatommy Apr 1, 2025
7e7f1fe
Typo: we want to stop only if status pages are on.
ilonatommy Apr 2, 2025
6a10062
Remove comments.
ilonatommy Apr 2, 2025
5518812
Fix tests.
ilonatommy Apr 2, 2025
c8eb629
Feedback.
ilonatommy Apr 7, 2025
66b563c
Feedback.
ilonatommy Apr 7, 2025
1da92f9
Failing test - re-executed without a reason.
ilonatommy Apr 7, 2025
b7775ef
Add streaming test after response started.
ilonatommy Apr 8, 2025
cb32f94
Test SSR with no interactivity.
ilonatommy Apr 8, 2025
8273758
Stop the renderer regardless of `Response.HasStarted`.
ilonatommy Apr 8, 2025
c31812c
Feedback: not checking status code works as well.
ilonatommy Apr 9, 2025
b162946
Feedback: improve handling streaming-in-process case.
ilonatommy Apr 10, 2025
f57b0b7
Merge branch 'main' into fix-58815
ilonatommy Apr 10, 2025
3e77c62
Throw on NotFound without global router.
ilonatommy Apr 11, 2025
d612592
Use `IStatusCodeReExecuteFeature`.
ilonatommy Apr 11, 2025
cd5dffa
Unify "fallback" pages - check for titles only.
ilonatommy Apr 15, 2025
b416805
Fix early return condition.
ilonatommy Apr 17, 2025
fc63304
Merge branch 'main' into fix-58815
ilonatommy Apr 22, 2025
c0e7f86
Feedback.
ilonatommy Apr 22, 2025
124b470
Merge branch 'main' into fix-58815
ilonatommy Apr 22, 2025
ebdf9a9
No-op instead of exception.
ilonatommy Apr 23, 2025
fc49fe6
Solve merge conflict: hard stop in redirection and deferred stop in 404.
ilonatommy Apr 23, 2025
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
2 changes: 2 additions & 0 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#nullable enable
Microsoft.AspNetCore.Components.NavigationManager.NotFoundEvent -> System.EventHandler<System.EventArgs!>!
virtual Microsoft.AspNetCore.Components.NavigationManager.NotFound() -> void
Microsoft.AspNetCore.Components.Routing.Router.NotFoundPage.get -> System.Type!
Microsoft.AspNetCore.Components.Routing.Router.NotFoundPage.set -> void
Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.ComponentStatePersistenceManager(Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager!>! logger, System.IServiceProvider! serviceProvider) -> void
Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.SetPlatformRenderMode(Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> void
Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateServiceCollectionExtensions
Expand Down
42 changes: 41 additions & 1 deletion src/Components/Components/src/Routing/Router.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@

#nullable disable warnings

using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Reflection.Metadata;
using System.Runtime.ExceptionServices;
using Microsoft.AspNetCore.Components.HotReload;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;

Expand Down Expand Up @@ -70,6 +72,13 @@ static readonly IReadOnlyDictionary<string, object> _emptyParametersDictionary
[Parameter]
public RenderFragment NotFound { get; set; }

/// <summary>
/// Gets or sets the page content to display when no match is found for the requested route.
/// </summary>
[Parameter]
[DynamicallyAccessedMembers(LinkerFlags.Component)]
public Type NotFoundPage { get; set; } = default!;

/// <summary>
/// Gets or sets the content to display when a match is found for the requested route.
/// </summary>
Expand Down Expand Up @@ -132,6 +141,22 @@ public async Task SetParametersAsync(ParameterView parameters)
throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(Found)}.");
}

if (NotFoundPage != null)
{
if (!typeof(IComponent).IsAssignableFrom(NotFoundPage))
{
throw new InvalidOperationException($"The type {NotFoundPage.FullName} " +
$"does not implement {typeof(IComponent).FullName}.");
}

var routeAttributes = NotFoundPage.GetCustomAttributes(typeof(RouteAttribute), inherit: true);
if (routeAttributes.Length == 0)
{
throw new InvalidOperationException($"The type {NotFoundPage.FullName} " +
$"does not have a {typeof(RouteAttribute).FullName} applied to it.");
}
}

if (!_onNavigateCalled)
{
_onNavigateCalled = true;
Expand Down Expand Up @@ -327,7 +352,22 @@ private void OnNotFound(object sender, EventArgs args)
if (_renderHandle.IsInitialized)
{
Log.DisplayingNotFound(_logger);
_renderHandle.Render(NotFound ?? DefaultNotFoundContent);
_renderHandle.Render(builder =>
{
if (NotFoundPage != null)
{
builder.OpenComponent(0, NotFoundPage);
builder.CloseComponent();
}
else if (NotFound != null)
{
NotFound(builder);
}
else
{
DefaultNotFoundContent(builder);
}
});
}
}

Expand Down
26 changes: 19 additions & 7 deletions src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,16 @@ public class GlobalInteractivityTest(
{

[Theory]
[InlineData("server", true)]
[InlineData("webassembly", true)]
[InlineData("ssr", false)]
public void CanRenderNotFoundInteractive(string renderingMode, bool isInteractive)
[InlineData("server", true, false)]
[InlineData("webassembly", true, false)]
[InlineData("server", true, true)]
[InlineData("webassembly", true, true)]
[InlineData("ssr", false, true)]
[InlineData("ssr", false, false)]
public void CanRenderNotFoundPage(string renderingMode, bool isInteractive, bool useCustomNotFoundPage)
{
Navigate($"/subdir/render-not-found-{renderingMode}");
string query = useCustomNotFoundPage ? "?useCustomNotFoundPage=true" : "";
Navigate($"{ServerPathBase}/render-not-found-{renderingMode}{query}");

if (isInteractive)
{
Expand All @@ -36,8 +40,16 @@ public void CanRenderNotFoundInteractive(string renderingMode, bool isInteractiv
Browser.Exists(By.Id(buttonId)).Click();
}

var bodyText = Browser.FindElement(By.TagName("body")).Text;
Assert.Contains("There's nothing here", bodyText);
if (useCustomNotFoundPage)
{
var infoText = Browser.FindElement(By.Id("test-info")).Text;
Assert.Contains("Welcome On Custom Not Found Page", infoText);
}
else
{
var bodyText = Browser.FindElement(By.TagName("body")).Text;
Assert.Contains("There's nothing here", bodyText);
}
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@page "/render-custom-not-found-page"

<h3 id="test-info">Welcome On Custom Not Found Page</h3>
<p>Sorry, the page you are looking for does not exist.</p>
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
@using Microsoft.AspNetCore.Components.Routing
@using Components.WasmMinimal.Pages
@inject NavigationManager NavigationManager

<Router AppAssembly="@typeof(Program).Assembly">
@code {
[Parameter]
[SupplyParameterFromQuery(Name = "useCustomNotFoundPage")]
public string? UseCustomNotFoundPage { get; set; }

private Type? NotFoundPageType { get; set; }

protected override void OnParametersSet()
{
if (UseCustomNotFoundPage == "true")
{
NotFoundPageType = typeof(CustomNotFoundPage);
}
else
{
NotFoundPageType = null;
}
}
}

<Router AppAssembly="@typeof(Program).Assembly" NotFoundPage="NotFoundPageType">
<Found Context="routeData">
<RouteView RouteData="@routeData" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
@page "/weather/details/{date}"
@*#if (UseServer && !InteractiveAtRoot)
@rendermode InteractiveAuto
##elseif (!InteractiveAtRoot)
@rendermode InteractiveWebAssembly
##endif*@
@inject NavigationManager NavigationManager

@if (weatherDetails == null)
{
<p><em>Loading...</em></p>
}
else
{
<div>
<p><strong>Date:</strong> @weatherDetails.Date.ToShortDateString()</p>
<p><strong>Temperature (C):</strong> @weatherDetails.TemperatureC</p>
<p><strong>Temperature (F):</strong> @weatherDetails.TemperatureF</p>
<p><strong>Summary:</strong> @weatherDetails.Summary</p>
</div>
}

@code{
[Parameter]
public string? Date { get; set; }

private WeatherForecast? weatherDetails;

protected override async Task OnInitializedAsync()
{
weatherDetails = null;

// Simulate fetching data from a database
await Task.Delay(500);

// Simulate a scenario where the details are not found
NavigationManager.NotFound();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
public class WeatherForecast
{
public int Id { get; set; }
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@page "/not-found"

<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
@page "/weather"
@*#if (!InteractiveAtRoot) -->
@attribute [StreamRendering]
##else
@inject NavigationManager NavigationManager
##endif*@

<PageTitle>Weather</PageTitle>
Expand Down Expand Up @@ -32,6 +34,13 @@ else
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
<td>
@*#if (InteractiveAtRoot) -->
<button class="btn btn-primary" @onclick="() => NavigateToDetails(forecast.Id)">More info</button>
##else
<a href="@($"/weather/details/{forecast.Id}")">More info</a>
##endif*@
</td>
</tr>
}
</tbody>
Expand All @@ -54,17 +63,17 @@ else
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Id = index,
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}

private class WeatherForecast
@*#if (InteractiveAtRoot) -->
private void NavigateToDetails(int id)
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
NavigationManager.NavigateTo($"/weather/details/{id}");
}
##endif*@
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
@page "/weather/details/{id:int}"
@*#if (!InteractiveAtRoot) -->
@attribute [StreamRendering]
##endif*@
@inject NavigationManager NavigationManager

@if (weatherDetails == null)
{
<p><em>Loading...</em></p>
}
else
{
<div>
<p><strong>Date:</strong> @weatherDetails.Date.ToShortDateString()</p>
<p><strong>Temperature (C):</strong> @weatherDetails.TemperatureC</p>
<p><strong>Temperature (F):</strong> @weatherDetails.TemperatureF</p>
<p><strong>Summary:</strong> @weatherDetails.Summary</p>
</div>
}

@code{
[Parameter]
public int Id { get; set; }

private WeatherForecast? weatherDetails;

protected override async Task OnInitializedAsync()
{
weatherDetails = null;

// Simulate fetching data from a database
await Task.Delay(500);

// Simulate a scenario where the details are not found
NavigationManager.NotFound();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
@using BlazorWeb_CSharp.Components.Account.Shared
##endif*@
@*#if (UseWebAssembly && !InteractiveAtRoot)
<Router AppAssembly="typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(Client._Imports).Assembly }">
<Router AppAssembly="typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(Client._Imports).Assembly }" NotFoundPage="typeof(Pages.NotFound)">
##else
<Router AppAssembly="typeof(Program).Assembly">
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
##endif*@
<Found Context="routeData">
@*#if (IndividualLocalAuth)
Expand Down
34 changes: 34 additions & 0 deletions src/ProjectTemplates/scripts/startvs.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
@ECHO OFF
SETLOCAL

:: This command launches a Visual Studio solution with environment variables required to use a local version of the .NET Core SDK.

:: This tells .NET Core to use the same dotnet.exe that build scripts use
SET DOTNET_ROOT=%~dp0\.dotnet
SET DOTNET_ROOT(x86)=%~dp0\.dotnet\x86

:: This tells .NET Core not to go looking for .NET Core in other places
SET DOTNET_MULTILEVEL_LOOKUP=0

:: Put our local dotnet.exe on PATH first so Visual Studio knows which one to use
SET PATH=%DOTNET_ROOT%;%PATH%

SET sln=%~1

IF "%sln%"=="" (
echo Error^: Expected argument ^<SLN_FILE^>
echo Usage^: startvs.cmd ^<SLN_FILE^>

exit /b 1
)

IF NOT EXIST "%DOTNET_ROOT%\dotnet.exe" (
echo .NET Core has not yet been installed. Run `%~dp0restore.cmd` to install tools
exit /b 1
)

IF "%VSINSTALLDIR%" == "" (
start "" "%sln%"
) else (
"%VSINSTALLDIR%\Common7\IDE\devenv.com" "%sln%"
)
Loading
Loading