Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I modify the layout from a blazor page?

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:

  • Wrapped my layout in a CascadingParameter and implemented the parameter inside the breadcrumb component and my page. This didn't work because evidently cascading values cannot be changed in a child component.
  • A cleverly scoped service that could be implemented both the page and the breadcrumb component. But there is no scoping that for the "current page" level, so I'm concerned about weird artifacts if there were multiple browser tabs open, etc. It occurs to me as I write this that two tabs might yield two circuits, which is something to explore as that would be workable.
  • Give up and solve this with JS interop. This is the nuclear option. It's doable, just lame. :) There must be a better way, where fewer lives are lost.

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

like image 842
Brian MacKay Avatar asked Oct 22 '20 01:10

Brian MacKay


People also ask

How do I change my Blazor page?

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.

How do you structure a Blazor project?

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.

What design pattern does Blazor use?

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.


Video Answer


1 Answers

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.

like image 136
Brian MacKay Avatar answered Oct 16 '22 22:10

Brian MacKay