I created my custom component in Blazor. It's Tabs. I need to remove re-render Tab re-rendering. This's necessary so that when switching tabs again, the components aren't reinitialized, i.e. "Dispose" and "OnInitialized" methods weren't called.
TabPage.razor.cs
partial class TabPage
{
[CascadingParameter]
public Tabs Tabs { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public string Text { get; set; }
[Parameter]
public string Value { get; set; }
[Parameter]
public bool Enabled { get; set; } = true;
protected override void OnInitialized()
{
if(Tabs is null)
{
throw new ArgumentNullException(nameof(Tabs), "TabPage must exist within Tabs");
}
base.OnInitialized();
if (!Tabs.Tabs.Any())
Tabs.ActiveTab = this;
Tabs.AddPage(this);
}
TabPage.razor.cs
@if(Tabs.ActiveTab == this)
{
@ChildContent
}
Tabs.razor.cs
partial class Tabs
{
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public EventCallback<TabPage> OnTabChanged { get; set; }
public TabPage ActiveTab { get; set; }
public List<TabPage> Tabs = new List<TabPage>();
internal void AddPage(TabPage tabPage)
{
Tabs.Add(tabPage);
if(Tabs.Count() == 1)
{
ActiveTab = tabPage;
}
StateHasChanged();
}
private async Task activateTabAsync(TabPage tab)
{
if(tab.Enabled)
{
ActiveTab = tab;
await OnTabChanged.InvokeAsync(tab);
}
}
private string setStyleTab(TabPage tab)
{
if (ActiveTab.Value == tab.Value)
return "show active";
else
return "";
}
}
Tabs.razor.cs
<CascadingValue Value="this">
<ul class="nav nav-tabs">
@foreach(PacsManTabPage tab in Tabs)
{
<li class="nav-item">
<button class="nav-link @setStyleTab(tab)" @onclick=@(async() => await activateTabAsync(tab))>
@tab.Text
</button>
</li>
}
</ul>
<div class="tab-content">
<div class="tab-pane show active">
@ChildContent
</div>
</div>
</CascadingValue>
Usage
<Tabs OnTabChanged="@OnSelectedTabChanged">
<TabPage Text="Storage Details 1" Value="details1">
//Content 1
</TabPage>
<TabPage Text="Storage Details 2" Value="details2">
//Content 2
</TabPage>
</Tabs>
@code {
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
}
private Task OnSelectedTabChanged(TabPage selectedTab)
{
SelectedTab = selectedTab.Value;
return Task.CompletedTask;
}
}
I tried using RenderFragment as below, but in StorageDetailsTable component, which is associated with _storageDetailsContent2, "Dispose" and "OnInitialized" methods are still called. Usage
@implements IDisposable
<Tabs OnTabChanged="@OnSelectedTabChanged">
<TabPage Text="Storage Details 1" Value="details1">
//Content 1
</TabPage>
<TabPage Text="Storage Details 2" Value="details2">
@_storageDetailsContent2
</TabPage>
</Tabs>
@code {
private RenderFragment? _storageDetailsContent2 = null;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
}
private Task OnSelectedTabChanged(TabPage selectedTab)
{
SelectedTab = selectedTab.Value;
if (SelectedTab == "details2" && _storageDetailsContent2 == null)
_storageDetailsContent2 = StorageDetailsFragment ;
return Task.CompletedTask;
}
public void Dispose()
{
_storageDetailsContent2 = null;
}
protected RenderFragment StorageDetailsFragment => (RenderTreeBuilder builder) =>
{
builder.OpenComponent<StorageDetailsTable>(0);
builder.CloseComponent();
};
}
I'm guessing that your tabs are related i.e. use the same services, such as a wizard that steps through creating an order or some such multi-part operation.
If you use components for each Tab, it's hard to manage services and content.
One approach to this is to use renderfragments rather that separate components for each tab. The form now manages the services, so there's no creation/disposal going on when you shift tabs.
Here's a very basic implementation that demonstrates the principal:
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<ul class="nav nav-pills">
<li class="nav-item">
<a class="nav-link @_isActiveCss(Tab1)" aria-current="page" @onclick="() => SetActiveTab(Tab1)">Tab 1</a>
</li>
<li class="nav-item">
<a class="nav-link @_isActiveCss(Tab2)" aria-current="page" @onclick="() => SetActiveTab(Tab2)">Tab 2</a>
</li>
<li class="nav-item">
<a class="nav-link @_isActiveCss(Tab3)" aria-current="page" @onclick="() => SetActiveTab(Tab3)">Tab 3</a>
</li>
</ul>
@_activeTab
@code {
private RenderFragment Tab1 => (__builder) =>
{
<div class="alert alert-primary m-2 p-2">
Hello from Tab 1
</div>
};
private RenderFragment Tab2 => (__builder) =>
{
<div class="alert alert-primary m-2 p-2">
Hello from Tab 2
</div>
};
private RenderFragment Tab3 => (__builder) =>
{
<div class="alert alert-primary m-2 p-2">
Hello from Tab 3
</div>
};
private string _isActiveCss(RenderFragment tab) => _activeTab == tab ? "active" : string.Empty;
private RenderFragment _activeTab = default!;
protected override void OnInitialized()
{
_activeTab = Tab1;
}
private void SetActiveTab(RenderFragment tab)
{
_activeTab = tab;
}
}
If you need to use components, then you need to use a cascaded object that each Tab component captures and uses for it's services and state. I can provide an example, if the above approach doesn't work.
UPDATE - Using Components:
Here's an example using a mix of markup code and components. I've used FetchData
and Counter
. Everything is loaded in the page. You just turn on and off what is displayed with Css.
@page "/Demo"
<PageTitle>Index</PageTitle>
<ul class="nav nav-pills">
<li class="nav-item">
<a class="nav-link @_isActiveCss(1)" aria-current="page" @onclick="() => SetActiveTab(1)">Hello</a>
</li>
<li class="nav-item">
<a class="nav-link @_isActiveCss(2)" aria-current="page" @onclick="() => SetActiveTab(2)">Counter</a>
</li>
<li class="nav-item">
<a class="nav-link @_isActiveCss(3)" aria-current="page" @onclick="() => SetActiveTab(3)">Weather</a>
</li>
</ul>
<div class="@_isActiveTabCss(1)">
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
</div>
<div class="@_isActiveTabCss(2)">
<Counter />
</div>
<div class="@_isActiveTabCss(3)">
<FetchData />
</div>
@* Normally Put this is a scoped Css file *@
<style>
div.active-tab {
display: block;
}
div.inactive-tab {
display: none;
}
</style>
@code {
private string _isActiveCss(int tab) => _activeTab == tab ? "active" : string.Empty;
private string _isActiveTabCss(int tab) => _activeTab == tab ? "active-tab" : "inactive-tab";
private int _activeTab = 1;
private void SetActiveTab(int tab)
=>_activeTab = tab;
}