I want to modify the standard sidebar nav so it is collapsible and can contain submenus
The problem I encounter is that the start page flickers or reloads endlessly, probably due to something with rendermodes.
I have updated my project from .Net8 to .Net9 to make use of the ‘AcceptsInteractiveRouting()’ method in HttpContext. I have a hard time understanding rendermodes and feel like I’m punching blindly in the air.
However my impression is that the problem has something to do with that my project includes Identity and the use of HttpContext. It doesn’t seem like others experience the same problems without Identity
Here is what I’m at so far:
App.razor:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="@Assets["bootstrap/bootstrap.min.css"]" />
<link rel="stylesheet" href="@Assets["css/custom-bootstrap.css"]" />
<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["ePortal.styles.css"]" />
<link rel="icon" type="image/png" href="favicon.png" />
<ImportMap/>
<HeadOutlet @rendermode="@RenderModeForPage" />
</head>
<body>
<Routes @rendermode="@RenderModeForPage"/>
<script src="_framework/blazor.web.js"></script>
</body>
</html>
@code {
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
private IComponentRenderMode? RenderModeForPage => HttpContext.AcceptsInteractiveRouting() ? InteractiveServer : null;
}
NavMenu.razor:
@implements IDisposable
@inject NavigationManager NavigationManager
@inject IJSRuntime JSRuntime
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
@if (!@IconMenuActive)
{
<a class="navbar-brand" href="">Awexi eDelivery</a>
}
else
{
<a class="navbar-brand bi bi-window-fullscreen"></a>
}
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
<div class="@NavMenuCssClass">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span>
@if (!@IconMenuActive)
{
<span><label>Home</label></span>
}
</NavLink>
</div>
<AuthorizeView>
<Authorized>
<div class="nav-item px-3" @onclick="@(() => ToggleSubMenu("subMenu1"))">
<ul class="nav nav-pills flex-column">
<li class="nav-item">
<NavLink class="nav-link" href="ManageDomain/Dashboard"
Match="NavLinkMatch.All">Manage Domain Home</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="Companies/Index">Manage Companies</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="Employees/Index">Manage Employees</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="TransferMessages/Index">Monitor transfer messages</NavLink>
</li>
</ul>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="Account/Manage">
<span class="bi bi-person-fill-nav-menu" aria-hidden="true"></span> @context.User.Identity?.Name
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="ManageDomain/Dashboard">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span>
Manage Domain
</NavLink>
</div>
<div class="nav-item px-3">
<form action="Account/Logout" method="post">
<AntiforgeryToken />
<input type="hidden" name="ReturnUrl" value="@currentUrl" />
<button type="submit" class="nav-link">
<span class="bi bi-arrow-bar-left-nav-menu" aria-hidden="true"></span> Logout
</button>
</form>
</div>
</Authorized>
<NotAuthorized>
<div class="nav-item px-3">
<NavLink class="nav-link" href="Account/Login">
<span class="bi bi-person-badge-nav-menu" aria-hidden="true"></span> Login
</NavLink>
</div>
</NotAuthorized>
</AuthorizeView>
</nav>
</div>
@code {
[Parameter]
public EventCallback<bool> ShowIconMenu { get; set; }
private bool IconMenuActive { get; set; } = false;
private bool collapseNavMenu = true;
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private Dictionary<string, bool> subMenuStates = new Dictionary<string, bool>();
private string? currentUrl;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JSRuntime.InvokeVoidAsync("setResizeCallback", DotNetObjectReference.Create(this));
}
}
[JSInvokable]
public async Task OnResize(bool isBelowThreshold)
{
if (isBelowThreshold && IconMenuActive)
{
collapseNavMenu = true;
await ToggleIconMenu();
await ShowIconMenu.InvokeAsync(IconMenuActive);
await InvokeAsync(StateHasChanged);
}
}
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
private async Task ToggleIconMenu()
{
IconMenuActive = !IconMenuActive;
await ShowIconMenu.InvokeAsync(IconMenuActive);
}
protected override void OnInitialized()
{
currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
NavigationManager.LocationChanged += OnLocationChanged;
}
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
{
currentUrl = NavigationManager.ToBaseRelativePath(e.Location);
StateHasChanged();
}
public void Dispose()
{
NavigationManager.LocationChanged -= OnLocationChanged;
}
private void ToggleSubMenu(string subMenuKey)
{
if (subMenuStates.ContainsKey(subMenuKey) && subMenuStates[subMenuKey])
{
subMenuStates[subMenuKey] = false;
}
else
{
foreach (var key in subMenuStates.Keys.ToList())
{
subMenuStates[key] = false;
}
if (subMenuStates.ContainsKey(subMenuKey))
{
subMenuStates[subMenuKey] = !subMenuStates[subMenuKey];
}
else
{
subMenuStates[subMenuKey] = true;
}
}
}
private bool IsSubMenuOpen(string subMenuKey)
{
return subMenuStates.ContainsKey(subMenuKey) && subMenuStates[subMenuKey];
}
}
MainLayout.razor
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar" style="@IconMenuCssClass">
<NavMenu ShowIconMenu="ToggleIconMenu" />
</div>
<main>
<article class="content px-4">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
@code{
private bool _iconMenuActive { get; set; }
private string? IconMenuCssClass => _iconMenuActive ? "width: 80px;" : null;
protected void ToggleIconMenu(bool iconMenuActive)
{
_iconMenuActive = iconMenuActive;
}
}
program.cs:
using eDePo.Application;
using eDePo.Infrastructure;
using ePortal.Components;
using ePortal.Components.Account;
using ePortal.Components.Pages.Shared;
using Microsoft.AspNetCore.Components.Authorization;
using static ePortal.Components.Account.Pages.Login;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services
.AddApplication()
.AddInfrastructure(builder.Configuration);
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<IdentityUserAccessor>();
builder.Services.AddScoped<IdentityRedirectManager>();
builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
builder.Services.AddScoped<FormStateHandler>();
builder.Services.AddTransient<UserPwdUpd>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.MapStaticAssets();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
// Add additional endpoints required by the Identity /Account Razor components.
app.MapAdditionalIdentityEndpoints();
app.Run();
my research:
https://github.com/H3ALY/CollapsibleNavMenu/tree/master/Blazor_Server
https://learn.microsoft.com/en-us/aspnet/core/migration/80-90?view=aspnetcore-9.0&tabs=visual-studio
https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.excludefrominteractiveroutingattribute?view=aspnetcore-9.0
1