动态生成的 Blazor 页面仅在页面导航之间部分更新

Dynamically generated Blazor pages are only partially updated between page navigation

我有一个页面列表,每个页面都使用相同的 Blazor 页面组件,其中的内容是根据页面名称动态生成的。内容分为不同的选项卡。由于某些 Blazor 渲染问题,在页面之间导航时,并非所有内容都会更新,导致我的选项卡内容更新,但我的选项卡 headers 没有。

一切都在 .NET Core 3.1 LTS 中完成,因为由于各种其他限制,我还不能升级到 .NET 5。

例如,给定以下页面内容:

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

结果如下:

如您所见,第 2 页的内容已更新,但选项卡 headers 未更新。但是,如果我从第 2 页返回到第 1 页,则显示第 1 页的内容,但现在显示第 2 页的选项卡 headers。

我的选项卡控件基于在网上找到的各种示例,虽然它包含更多功能,但我试图将其简化为一个非常基本的工作示例,从而重现了该问题。

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);

}

我的问题很简单:为什么我的标签页内容在更新,而我的标签页 headers 却没有?在页面之间导航时如何让它们更新?

虽然在每个列表中添加 @key 属性 并不是一个坏主意,正如@MisterMagoo 在评论中所建议的那样,但最终并没有解决它。

诀窍是让每个异步方法都同步。

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

所以异步方法似乎真的改变了渲染管道的行为方式,并且看起来乱序。我想这有点道理,也可能有一些好处,但这不是我在这种情况下需要的。