I have a list of pages, each using the same Blazor page component, of which the content is generated dynamically based on the page name. The content is split across different tabs. Due to some Blazor rendering issue, when navigating between pages, not everything is updated, resulting in the update of my tab content, but my tab headers are not.
Everything is done in .NET Core 3.1 LTS, as I am not able to upgrade to .NET 5 yet due to various other constraints.
Given, as an example, the following page content:
pageContent.Add("page-1", new Dictionary<string, string>
{
{ "tab a", "This is page 1, tab a" },
{ "tab b", "This is page 1, tab b" },
{ "tab c", "This is page 1, tab c" }
});
pageContent.Add("page-2", new Dictionary<string, string>
{
{ "tab d", "This is page 2, tab d" },
{ "tab e", "This is page 2, tab e" }
});
The result is the following:
As you can see, on page 2 the content is updated, but the tab headers are not. However, if I move back from page 2 to page 1, the content of page 1 is displayed, but now with the tab headers of page 2 are shown.
My tab control is based on various samples found online, and although it contains a lot more functionality, I tried to reduce it to a very basic working example which reproduces the problem.
PageContentService.cs
public Dictionary<string, string> GetPageContent(string pageName)
=> pageContent.ContainsKey(pageName) ? pageContent[pageName] : new Dictionary<string, string>();
DynamicPage.razor
@page "/dynamic/{PageName}"
<MyDynamicContent PageName="@PageName"/>
@code {
[Parameter]
public string PageName { get; set; }
}
MyDynamicContent.razor
@if (isLoading)
{
<p>Loading...</p>
}
else
{
<MyTabs SelectedTab="@selectedTabTitle" OnSelectedTabChanged="OnSelectedTabChanged">
@foreach (var (tabId, tabContent) in currentPage)
{
<MyTabItem Title="@tabId">
@tabContent
</MyTabItem>
}
</MyTabs>
}
@code {
private bool isLoading;
private string selectedTabTitle;
private Dictionary<string, string> currentPage;
[Parameter]
public string PageName { get; set; }
[Inject]
public IPageContentService PageContentService { get; set; }
protected override void OnParametersSet()
{
base.OnParametersSet();
isLoading = true;
currentPage = PageContentService.GetPageContent(PageName);
if (currentPage != null && currentPage.Count > 0)
{
selectedTabTitle = currentPage.First().Key;
}
isLoading = false;
}
public void OnSelectedTabChanged(string title)
=> selectedTabTitle = title;
}
MyTabs.razor
<CascadingValue Value="@(this)" IsFixed="true">
<CascadingValue Value="@selectedTabName">
<div>
<ul class="nav nav-tabs">
@foreach (var item in tabItems)
{
var aCss = "nav-link";
if (item.Active)
{
aCss += " active show";
}
<li class="nav-item">
<a class="@aCss" tabindex="0" @onclick="async () => await item.OnTabClicked()">
@item.Title
</a>
</li>
}
</ul>
<div class="tab-content">
@ChildContent
</div>
</div>
</CascadingValue>
</CascadingValue>
@code {
private readonly List<MyTabItem> tabItems = new List<MyTabItem>();
private string selectedTabName;
[Parameter]
public string SelectedTab
{
get => selectedTabName;
set
{
if (selectedTabName != value)
{
selectedTabName = value;
OnSelectedTabChanged.InvokeAsync(value);
}
}
}
[Parameter]
public EventCallback<string> OnSelectedTabChanged { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
public IReadOnlyList<MyTabItem> TabItems
=> tabItems;
public async Task ChangeSelectedTab(string title)
{
SelectedTab = title;
await InvokeAsync(StateHasChanged);
}
public async Task AddTab(MyTabItem tabItem)
{
if (tabItems.All(x => x.Title != tabItem.Title))
{
tabItems.Add(tabItem);
await InvokeAsync(StateHasChanged);
}
}
public async Task RemoveTab(MyTabItem tabItem)
{
if (tabItems.Any(x => x.Title == tabItem.Title))
{
tabItems.Remove(tabItem);
await InvokeAsync(StateHasChanged);
}
}
}
MyTabItem.razor
@implements IAsyncDisposable
@{
var css = "tab-pane";
if (Active)
{
css += " active show";
}
}
<div class="@css">
@if (Active)
{
@ChildContent
}
</div>
@code {
[Parameter]
public string Title { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
[CascadingParameter]
public MyTabs ParentTabs { get; set; }
public bool Active
=> ParentTabs.SelectedTab == Title;
public async Task OnTabClicked()
{
if (ParentTabs != null)
{
await ParentTabs.ChangeSelectedTab(Title);
}
}
protected override async Task OnInitializedAsync()
{
if (ParentTabs != null)
{
await ParentTabs.AddTab(this);
}
await base.OnInitializedAsync();
}
public ValueTask DisposeAsync()
=> ParentTabs != null ? new ValueTask(ParentTabs.RemoveTab(this)) : new ValueTask(Task.CompletedTask);
}
My question is very simple: Why is my tab content updating, but my tab headers are not? And how do I get them to update when navigating between pages?


@keyinside loops - it allows Blazor to track your components/elements and is likely to be a factor here.