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()}");
}