0

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:

page 1 page 2

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?

1
  • Start by learning about using @key inside loops - it allows Blazor to track your components/elements and is likely to be a factor here. Commented Oct 21, 2021 at 23:18

1 Answer 1

0

Although adding the @key property to each list isn't a bad idea, as @MisterMagoo suggested in the comments, it wasn't wat ultimately fixed it.

The trick was to make every async method synchronous.

MyTabs.razor

    public void ChangeSelectedTab(string title)
    {
        SelectedTab = title;
        StateHasChanged;
    }

    public void AddTab(MyTabItem tabItem)
    {
        if (tabItems.All(x => x.Title != tabItem.Title))
        {
            tabItems.Add(tabItem);
            StateHasChanged;
        }
    }

    public void RemoveTab(MyTabItem tabItem)
    {
        if (tabItems.Any(x => x.Title == tabItem.Title))
        {
            tabItems.Remove(tabItem);
            StateHasChanged;
        }
    }

MyTabItem.razor

    public void OnTabClicked()
    {
        ParentTabs?.ChangeSelectedTab(Title);
    }

    protected override void OnInitialized()
    {
        ParentTabs?.AddTab(this);
        base.OnInitialized();
    }

    public void Dispose()
    {
        ParentTabs?.RemoveTab(this);
    }

So the async methods seem to really change how the render pipeline behaves, and do things seemingly out of order. I guess that makes a bit sense, and might have some benefits too it, but it isn't what I needed in this scenario.

Sign up to request clarification or add additional context in comments.

1 Comment

I don't think async methods were the ptoblem. You were not awaiting in the Dispose() methods. But there wasn't anything requiring async, so keep it this way.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.