Home > Net >  Notify MainLayout/App of Property Change from Child Component
Notify MainLayout/App of Property Change from Child Component

Time:04-27

I have an application, which consists of a simple layout with a sidebar and header. The header has a text that displays some custom information (e.g., a username, date or simply the name of the current view).

<Layout Sider="true">
    <LayoutSider>
        <LayoutSiderContent>
            <NavMenu />
        </LayoutSiderContent>
    </LayoutSider>
    <Layout>
        <LayoutHeader Fixed="true">
            <Header Title="@Title"></Header> <!-- CUSTOM HEADER COMPONENT -->
        </LayoutHeader>
        <LayoutContent>
            <CascadingAuthenticationState>
                <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> <!-- VIEW ROUTER-->
                    <Found Context="routeData">
                        <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                            <NotAuthorized>
                                You are not Authorized to Access this App. Please make sure, that you logged in with your company account.
                            </NotAuthorized>
                        </AuthorizeRouteView>
                    </Found>
                    <NotFound>
                        <LayoutView Layout="@typeof(MainLayout)">
                            <p>Sorry, there's nothing at this address.</p>
                        </LayoutView>
                    </NotFound>
                </Router>
            </CascadingAuthenticationState>
        </LayoutContent>
    </Layout>
</Layout>

@code 
{
    private string Title { get; set; }
}

Note: Header is a simple component I wrote, which just outputs a text with a little bit CSS. I additionally use Blazorise, which wraps the basic HTML objects into Blazor components.

I did not put this code into my MainLayout.razor because I want the sidebar and header to be displayed when a page is not found or the user does not have permission for a specific page.

The issue I have is, that I cannot figure out a clean solution to update my Title. Currently I did it like the following, but for me it looks like a very "hacky" solution and it does not update the title without a call of StateHasChanged.

I made a class called ViewBase.cs, from which all of my pages inherit from and takes the title as a cascading parameter.

<Layout Sider="true">
    <CascadingValue Value="@Title">
        <Layout>
            <LayoutHeader Fixed="true">
                ...
            </LayoutHeader>
            <LayoutContent>
                ...
            </LayoutContent>
        </Layout>
    </CascadingValue>
</Layout>

^ Adjusted App.cshtml with Cascading Value

@inherits LayoutComponentBase

<CascadingValue Value="@Title">
    @Body
</CascadingValue>

@code 
{
    private string title;

    [CascadingParameter]
    public string Title
    {
        get => this.title;
        set
        {
            this.title = value;
            this.StateHasChanged();
        }
    }    
}

^ MainLayout.razor

public class ViewBase : ComponentBase
{
    private string title;

    [CascadingParameter]
    public string Title
    {
        get => this.title;
        set
        {
            this.title = value;
            this.StateHasChanged();
        }
    }
}

^ ViewBase.cs

This allows me to set the title programmatically in my views, by writing this.Title = "My custom Title!". This implements the basic functionality, but I additionally want to control the title directly in the Blazor view. Therefore, I created a simple component HeaderControl.

@inherits Project.Shared.BaseComponents.ViewBase

@code {
    [Parameter]
    public string PTitle
    {
        get => this.Title;
        set => this.Title = value;
    }
}

I can then use this component in my views like this:

@page "/"
@inherits Project.Shared.BaseComponents.ViewBase

<HeaderControl PTitle="Hello World!"/>

<Div Class="w-100 h-100 d-flex flex-column align-items-center justify-content-between">
    <!-- Page Content -->
</Div>

As I said, this somewhat works, but the StateHasChanged is a little hustle, especially, if you need to update the Title a couple of times per second (which is a requirement). Is there some sort of mechanism I have overseen? I think it would be nice, if this can be handled via an event, where the new value gets passed up the hierarchy (View.razor -> MainLayout.razor -> App.cshtml), but I cannot think of a way on how to do that. Another solution I can think of is, that I have a static oldscool C# event in my Header component, which gets invoked by my HeaderControl component.

CodePudding user response:

You need to use a Service with a Notification event. Here's a very simple implementation to demo the principle.

A Header Service

public class HeaderService
{
    public string Header { get; set; } = "Starting Header";

    public event EventHandler? HeaderChanged;

    public void SetHeader(string header)
    {
        Header = header;
        HeaderChanged?.Invoke(this, EventArgs.Empty);
    }

    public void NotifyHeaderChanged()
        => HeaderChanged?.Invoke(this, EventArgs.Empty);
}

Register in Program:

builder.Services.AddScoped<HeaderService>();

A Header Component

@inject HeaderService headerService
@implements IDisposable
<h3>@this.headerService.Header</h3>

@code {
    protected override void OnInitialized()
        => this.headerService.HeaderChanged  = this.OnChange;

    private void OnChange(object? sender, EventArgs e)
     => this.InvokeAsync(StateHasChanged);

    public void Dispose()
    => this.headerService.HeaderChanged -= this.OnChange;
}

App:

<MyHeader />
<Router AppAssembly="@typeof(App).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
    <NotFound>
        <PageTitle>Not found</PageTitle>
        <LayoutView Layout="@typeof(MainLayout)">
            <p role="alert">Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

Demo Page:

@page "/"
@inject HeaderService headerService

<PageTitle>Index</PageTitle>

<div >
    <button  @onclick=UpdateHeader>Say Hello</button>
</div>
Welcome to your new app.

@code {

    private void UpdateHeader()
      => this.headerService.SetHeader($"Clicked at {DateTime.Now.ToLongTimeString()}");

}
  • Related