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?
Share Improve this question asked Feb 3 at 21:37 SachihiroSachihiro 1,7934 gold badges27 silver badges56 bronze badges 3- Just use Blazor Sections much easier than all this. learn.microsoft/en-us/aspnet/core/blazor/components/… – Brian Parker Commented Feb 4 at 1:22
- I accepted the answer from Shaun Curtis, but can you explain, how can sections be used in this context? – Sachihiro Commented Feb 4 at 7:10
- Sure, see below. Shaun's answer is perfectly valid. Just an alternative way for the community. – Brian Parker Commented Feb 4 at 15:41
2 Answers
Reset to default 0Your 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/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.