I created this BreadcrumbService
so that I can use it to inject breadcrumbs and update them globally.
public class BreadcrumbService
{
private List<BreadcrumbItem> _breadcrumbItems = new List<BreadcrumbItem>();
public List<BreadcrumbItem> BreadcrumbItems
{
get => _breadcrumbItems;
set
{
_breadcrumbItems = value;
NotifyStateChanged();
}
}
public event Func<Task>? OnBreadcrumbsChanged;
public void SetBreadcrumbs(IEnumerable<BreadcrumbItem>? items)
{
_breadcrumbItems = items?.ToList() ?? new List<BreadcrumbItem>();
NotifyStateChanged();
}
private void NotifyStateChanged() => OnBreadcrumbsChanged?.Invoke();
}
And I have injected the breadcrumbs into the MainLayout as such:
<MudMainContent>
<MudContainer MaxWidth="MaxWidth.Large" Style="height:max-content">
<MudBreadcrumbs Items="BreadcrumbService.BreadcrumbItems"></MudBreadcrumbs>
<MudPaper Elevation="3" Style="height:100dvh;">
@Body
</MudPaper>
</MudContainer>
</MudMainContent>
And in AppLayout
, I am initializing the breadcrumbs:
protected override async Task OnInitializedAsync()
{
BreadcrumbService.OnBreadcrumbsChanged += BreadcrumbsService_OnChange;
}
public void Dispose()
{
BreadcrumbService.OnBreadcrumbsChanged -= BreadcrumbsService_OnChange;
}
private async Task BreadcrumbsService_OnChange()
{
Breadcrumbs = BreadcrumbService.BreadcrumbItems;
await InvokeAsync(StateHasChanged);
}
And finally I am using it as such:
@inject BreadcrumbService BreadcrumbService
protected override async Task OnInitializedAsync()
{
BreadcrumbService.SetBreadcrumbs(new List<BreadcrumbItem>
{
new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home)
});
StateHasChanged();
}
.
.
.
// In other component
BreadcrumbService.SetBreadcrumbs(new List<BreadcrumbItem>
{
new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home),
new BreadcrumbItem("Blogs", href: "/blogs"),
new BreadcrumbItem($"Blog {Id}", href: $"/blogs/{ChapId}")
});
My problem is that, it takes two clicks to update the breadcrumbs, and their behaviour is non deterministic, could you help me to fix this bug so that it updates the breadcrumbs on initialized a component?
I created this BreadcrumbService
so that I can use it to inject breadcrumbs and update them globally.
public class BreadcrumbService
{
private List<BreadcrumbItem> _breadcrumbItems = new List<BreadcrumbItem>();
public List<BreadcrumbItem> BreadcrumbItems
{
get => _breadcrumbItems;
set
{
_breadcrumbItems = value;
NotifyStateChanged();
}
}
public event Func<Task>? OnBreadcrumbsChanged;
public void SetBreadcrumbs(IEnumerable<BreadcrumbItem>? items)
{
_breadcrumbItems = items?.ToList() ?? new List<BreadcrumbItem>();
NotifyStateChanged();
}
private void NotifyStateChanged() => OnBreadcrumbsChanged?.Invoke();
}
And I have injected the breadcrumbs into the MainLayout as such:
<MudMainContent>
<MudContainer MaxWidth="MaxWidth.Large" Style="height:max-content">
<MudBreadcrumbs Items="BreadcrumbService.BreadcrumbItems"></MudBreadcrumbs>
<MudPaper Elevation="3" Style="height:100dvh;">
@Body
</MudPaper>
</MudContainer>
</MudMainContent>
And in AppLayout
, I am initializing the breadcrumbs:
protected override async Task OnInitializedAsync()
{
BreadcrumbService.OnBreadcrumbsChanged += BreadcrumbsService_OnChange;
}
public void Dispose()
{
BreadcrumbService.OnBreadcrumbsChanged -= BreadcrumbsService_OnChange;
}
private async Task BreadcrumbsService_OnChange()
{
Breadcrumbs = BreadcrumbService.BreadcrumbItems;
await InvokeAsync(StateHasChanged);
}
And finally I am using it as such:
@inject BreadcrumbService BreadcrumbService
protected override async Task OnInitializedAsync()
{
BreadcrumbService.SetBreadcrumbs(new List<BreadcrumbItem>
{
new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home)
});
StateHasChanged();
}
.
.
.
// In other component
BreadcrumbService.SetBreadcrumbs(new List<BreadcrumbItem>
{
new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home),
new BreadcrumbItem("Blogs", href: "/blogs"),
new BreadcrumbItem($"Blog {Id}", href: $"/blogs/{ChapId}")
});
My problem is that, it takes two clicks to update the breadcrumbs, and their behaviour is non deterministic, could you help me to fix this bug so that it updates the breadcrumbs on initialized a component?
Your problem is in the page update logic. You can simplify the code and logic like this.
BreadcrumbService
The only way to set the breadcrumbs is through the SetBreadcrumbs
method.
public class BreadcrumbService
{
private List<BreadcrumbItem> _breadcrumbItems = new List<BreadcrumbItem>();
public IReadOnlyList<BreadcrumbItem> BreadcrumbItems => _breadcrumbItems.AsReadOnly();
public event EventHandler? OnBreadcrumbsChanged;
public void SetBreadcrumbs(IEnumerable<BreadcrumbItem>? items, object? sender = null)
{
_breadcrumbItems = items?.ToList() ?? new List<BreadcrumbItem>();
NotifyStateChanged(sender);
}
private void NotifyStateChanged(object? sender = null) => OnBreadcrumbsChanged?.Invoke(sender, EventArgs.Empty);
}
}
BreadcrumbComponent.razor
Component to encapsulate the Breadcrumb and update on change.
@inject BreadcrumbService breadcrumbService
@implements IDisposable
<MudBreadcrumbs Items="breadcrumbService.BreadcrumbItems"></MudBreadcrumbs>
@code {
protected override void OnInitialized()
{
breadcrumbService.OnBreadcrumbsChanged += BreadcrumbsService_OnChange;
}
private void BreadcrumbsService_OnChange(object? sender, EventArgs e)
{
if (sender != this)
this.InvokeAsync(StateHasChanged);
}
public void Dispose()
{
breadcrumbService.OnBreadcrumbsChanged -= BreadcrumbsService_OnChange;
}
}
MainLayout.razor
<MudMainContent Class="mt-16 pa-4">
<BreadcrumbComponent />
@Body
</MudMainContent>
And Home.razor
@page "/"
@inject BreadcrumbService BreadcrumbService
//...
@code
{
protected override void OnInitialized()
{
BreadcrumbService.SetBreadcrumbs(new List<BreadcrumbItem>
{
new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home)
});
}
}
Make use of the built-in components called SectionOutlet
SectionContent
.
Somewhere in your app where you want your breadcrumbs displayed.
Mainlayout.razor
@using Microsoft.AspNetCore.Components.Sections
...
<div class="top-row px-4">
<SectionOutlet SectionName="Breadcrumbs" /> <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
Breadcrumbs.razor
@using Microsoft.AspNetCore.Components.Sections
<SectionContent SectionName="Breadcrumbs">
@ChildContent
</SectionContent>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
}
On any page:
<Breadcrumbs>
<NavLink class="nav-link" href="" >
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
Item2
</Breadcrumbs>
Of course, Breadcrumbs.razor
does not have to capture the render fragment. You could use a parameter and build a display.
I like this way as you can still make use of the razor syntax to construct custom breadcrumbs for a particular page. It makes it clearer and easier to understand when reading the page or subcomponent for the page.