Reputation: 1604
I have a Blazor server-side app, trying to understand the structure. Inside the MainLayout.razor page, I see the tag @Body, and this is where each page's content is rendered.
I am wondering, is it possible to add additional render element to the mainLayout page? For example, a @Header section. And I would prefer to define this section inside each individual page as well.
In other words, for each page, besides the main content, it also needs to define Header, Footer, or whatever render element I define in the MainLayout. This way, I can have customize Header/Footer elements that are unique for each page.
Thanks for any help.
Upvotes: 10
Views: 5882
Reputation: 449
Both answers given here do not reflect the way the Blazor documentation is currently advising to do it, at least in .NET8. They advise using sections. If we want to make a bootstrap card, create a layout as follows:
@using Microsoft.AspNetCore.Components.Sections
@inherits LayoutComponentBase
<div class="card" style="width: 18rem;">
<div class="card-header">
<SectionOutlet SectionName="header"/>
</div>
<div class="card-body">
@Body
</div>
<div class="card-footer">
<SectionOutlet SectionName="footer"/>
</div>
Then we can create a page:
<SectionContent SectionName="header">
<h3>An interesting title.</h3>
</SectionContent>
<p>This will be the body text of the card.</p>
<SectionContent SectionName="footer">
<p>Some footer content</p>
</SectionContent>
Good luck!
Upvotes: 1
Reputation: 14623
MainLayout.razor
@inherits LayoutComponentBase
<div class="sidebar">
<AppNavigation />
</div>
<div class="main">
<div class="top-row px-4">
<AppHeader />
</div>
<div class="content px-4">
@Body
</div>
<div class="bottom-row px-4">
<AppFooter />
</div>
</div>
AppHeader.razor
@implements IDisposable
@LayoutService.Header
@code {
[Inject]
public ILayoutService LayoutService { get; set; }
protected override void OnInitialized()
{
LayoutService.PropertyChanged += LayoutService_PropertyChanged;
base.OnInitialized();
}
private void LayoutService_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ILayoutService.Header))
{
StateHasChanged();
}
}
public void Dispose()
{
if (LayoutService != null)
{
LayoutService.PropertyChanged -= LayoutService_PropertyChanged;
}
}
}
ILayoutService.cs
public interface ILayoutService
{
RenderFragment Header { get; }
SetHeader HeaderSetter { get; set; }
event PropertyChangedEventHandler PropertyChanged;
void UpdateHeader();
}
LayoutService.cs
public class LayoutService : ILayoutService, INotifyPropertyChanged
{
public RenderFragment Header => HeaderSetter?.ChildContent;
public SetHeader HeaderSetter
{
get => headerSetter;
set
{
if (headerSetter == value) return;
headerSetter = value;
UpdateHeader();
}
}
public void UpdateHeader() => NotifyPropertyChanged(nameof(Header));
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
private SetHeader headerSetter;
}
SetHeader.cs
public class SetHeader : ComponentBase, IDisposable
{
[Inject]
private ILayoutService Layout { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
protected override void OnInitialized()
{
if (Layout != null)
{
Layout.HeaderSetter = this;
}
base.OnInitialized();
}
protected override bool ShouldRender()
{
var shouldRender = base.ShouldRender();
if (shouldRender)
{
Layout.UpdateHeader();
}
return shouldRender;
}
public void Dispose()
{
if (Layout != null)
{
Layout.HeaderSetter = null;
}
}
}
In my case Program.cs
builder.Services.AddScoped<ILayoutService, LayoutService>();
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
<SetHeader>
<p>Hello Current count: @currentCount</p>
</SetHeader>
<SetFooter>
<p>Goodbye Current count: @currentCount</p>
</SetFooter>
@code {
private int currentCount = 0;
private void IncrementCount() => currentCount++;
}
This also clears the values on navigation as I am using IDisposable
. The Setter does not have to be set (Null is ok). If more than one setter is used the latest takes precedence. I have not tested dynamically removing multiple setters on one page.
Here is a repo using WebAssembly 3.2.1
Upvotes: 3
Reputation: 14623
MainLayout.razor
Note: The use of a method to update the render fragment fields I deliberately made private which then calls StateHasChanged()
. Other methods could easily be created to clear or set other fields.
@inherits LayoutComponentBase
<CascadingValue Value="this">
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<div class="top-row px-4">
@header
</div>
<div class="content px-4">
@Body
@footer
</div>
</div>
</div>
</CascadingValue>
@code {
private RenderFragment header;
private RenderFragment footer;
public void SetHeaderAndFooter(RenderFragment header, RenderFragment footer)
{
this.header = header;
this.footer = footer;
StateHasChanged();
}
}
LayoutSetter.cs
public class LayoutSetter : ComponentBase
{
[CascadingParameter]
public MainLayout Layout { get; set; }
[Parameter]
public RenderFragment Header { get; set; }
[Parameter]
public RenderFragment Footer { get; set; }
protected override void OnInitialized()
{
Layout.SetHeaderAndFooter(Header, Footer);
}
}
On any page...
@page "/"
<h1>Hello, world!</h1>
<LayoutSetter>
<Header>
Hello
</Header>
<Footer>
Goodbye
</Footer>
</LayoutSetter>
Upvotes: 16