动态生成的 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);
}
所以异步方法似乎真的改变了渲染管道的行为方式,并且看起来乱序。我想这有点道理,也可能有一些好处,但这不是我在这种情况下需要的。
我有一个页面列表,每个页面都使用相同的 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);
}
所以异步方法似乎真的改变了渲染管道的行为方式,并且看起来乱序。我想这有点道理,也可能有一些好处,但这不是我在这种情况下需要的。