I'm trying to implement breadcrumbs in Blazor. There doesn't seem to be any mechanism for passing data upstream to the layout level, so I'm wondering how people are managing this.
This is a simplified version of my layout:
<header>
<Toolbar />
<Breadcrumbs></Breadcrumbs>
</header>
<main class="mt-5">
@Body
</main>
The idea is that on any given Blazor page, I want to be able to set breadcrumbs. But my pages cannot interact with the layout or the breadcrumb component.
Things I've considered/tried:
In this ancient GitHub issue, Steve Sanderson replies to a similar need, but his workaround is invasive. I'm hoping there's something better now: https://github.com/dotnet/aspnetcore/issues/16009
This was interesting but ultimately not helpful: Blazor: Access parameter from layout
You can redirect to a page in Blazor using the Navigation Manager's NavigateTo method. In the following code snippet, it will redirect to the home page when this page gets loaded. Similarly, you can call NavigateTo() method from NavigationManager class anywhere to redirect to another page.
The Blazor Server app's entry point is defined in the Program. cs file, as you would see in a Console app. When the app executes, it creates and runs a web host instance using defaults specific to web apps. The web host manages the Blazor Server app's lifecycle and sets up host-level services.
An Architectural Approach In the MVVM design pattern you wrap all of your Blazor code into a single class (the ViewModel). Instead of binding HTML elements to some random collection of fields, you bind your elements to properties on the ViewModel.
State Container Pattern
I ended up solving this using the State Container pattern.
If you don't know what that is, these links are helpful:
https://chrissainty.com/3-ways-to-communicate-between-components-in-blazor/ (last section) https://www.youtube.com/watch?v=BB4lK2kfKf0&feature=youtu.be (deeper dive with additional options)
Overview
I made a Scoped service just for handling nav. It's injected into my Navbar component, where it is used to manage breadcrumbs. The nav service also has an event that refreshes the navbar UI whenever the breadcrumbs change.
Nav options can be configured on a page-by-page basis.
To make things easier for myself, I also created a base page that inherits from ComponentBase.
I've simplified my code somewhat. In my actual project I am managing more than just breadcrumbs here.
MainLayout
Note the Navbar component in the header.
<header>
<Navbar />
</header>
<main>
@Body
</main>
Navbar Component
This uses the NavState component to build our breadcrumbs and handle visibility. This example uses mdbootstrap 4. In the code block at the end, we sign up for the OnChange event, and we use it to re-render the component. We also implement Dispose to drop that event binding, otherwise we might have memory leaks.
@inject NavState Nav
@implements IDisposable
<div class="subnav clearfix @(Nav.Visible ? "" : "invisible")">
@*BREADCRUMBS*@
<div class="float-left">
<ol class="breadcrumb">
@foreach (var item in Nav.Breadcrumbs)
{
if (item.Active)
{
<li class="breadcrumb-item active">@item.Text</li>
}
else
{
<li class="breadcrumb-item"><a href="@item.Link">@item.Text</a></li>
}
}
</ol>
</div>
</div>
@code {
protected override void OnInitialized()
{
Nav.OnChange += StateHasChanged;
}
public void Dispose()
{
Nav.OnChange -= StateHasChanged;
}
}
NavState Service
Injected as a scoped service. In server-side Blazor, scoped services exist for the lifespan of the Blazor connection, so we have to be careful to reset this when a new page loads.
Also worth noting: if you open multiple tabs, each tab has its own connection, so there is no chance of corruption due to one user having multiple tabs open.
public class NavState : IScopedService
{
public List<Breadcrumb> Breadcrumbs { get; set; } = new List<Breadcrumb>();
public bool Visible { get; set; } = false;
public event Action OnChange;
public void SetVisible(bool isVisible)
{
Visible = isVisible;
NotifyStateChanged();
}
public void Reset()
{
Breadcrumbs = new List<Breadcrumb>();
Visible = false;
NotifyStateChanged();
}
public void SetBreadcrumbs(List<Breadcrumb> breadcrumbs)
{
Breadcrumbs = breadcrumbs;
Visible = true;
NotifyStateChanged();
}
private void NotifyStateChanged() => OnChange?.Invoke();
}
}
The breadcrumb itself is simple:
public class Breadcrumb
{
public string Text { get; set; }
public string Link { get; set; }
public bool Active { get; set; }
}
Base Page
public class MyPageBase : ComponentBase
{
[Inject] protected NavState Nav { get; set; }
protected override void OnInitialized()
{
// NavState (breadcrumbs, etc) is Scoped, so it lives as long as our connection lives.
// So when a new page is visited, we need to clear navigation to prevent breadcrumbs from bleed-over, etc.
// This also makes the navbar invisible by default.
Nav.Reset();
}
}
Pages
With all that out of your way, on your pages, if you do nothing at all the navbar will be invisible. Or you can add breadcrumbs:
protected override async Task OnInitializedAsync()
{
...
Nav.SetBreadcrumbs(new List<Breadcrumb>()
{ new Breadcrumb(Text: "Test", Link: "https://google.com" }
);
...
}
In my real-world implementation, I also created a fluent builder to make working with breadcrumbs a bit less clunky, but I don't want to overwhelm people.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With