最外层的 CascadingValue 在页面刷新或直接丢失时丢失 link
Outermost CascadingValue is lost on page refresh or direct link
我有几个组件想在整个 Blazor 应用程序中共享。这些恰好是 SyncFusion 组件——一个是 SfToast,一个是 SfDialog。我认为一个简单的方法是将组件放在 MainLayout.razor 上,然后对每个组件使用 <CascadingValue>
将引用传递给所有子页面和组件。
只要通过 <NavLink>
元素或使用 NavigationManager.NavigateTo()
导航到页面,这就可以正常工作。然而,如果一个页面被深度 linked 或刷新,最外层的 <CascadingValue>
变为空。为了解决这个问题,我创建了一个额外的虚拟 <CascadingValue>
作为最外层,以确保我真正关心的值在刷新或直接 link 时被填充,但这感觉像是 hack。我想知道我这样做的方式是否存在固有错误,导致最外层的 <CascadingValue>
在刷新时变为 null。
下面是一些示例代码来说明问题。这是非常人为的,但这是我能想出创建一个显示问题的 minimal reproducible example 的唯一方法。
如果您 运行 项目并单击“转到示例页面”按钮,您将看到 CompOne 和 CompTwo 组件引用都设置为值,如预期的那样。但是,如果您随后刷新页面,您将看到 CompOne 引用(最外面的 <CascadingValue>
)现在为空。
项目的布局如下(基于默认的 Blazor Server 模板创建,因此我只显示了我进行修改的区域):
+ BlazorSample
| + Components (I added this folder)
| - ComponentOne.razor
| - ComponentTwo.razor
| + Pages
| - Index.razor
| - SamplePage.razor
| + Shared
| - MainLayout.razor
MainLayout.razor
@inherits LayoutComponentBase
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
</div>
<div class="content px-4">
<CascadingValue Name="CompOne" Value="ComponentOne">
<CascadingValue Name="CompTwo" Value="ComponentTwo">
@Body
</CascadingValue>
</CascadingValue>
</div>
<BlazorSample.Components.ComponentOne @ref="ComponentOne"></BlazorSample.Components.ComponentOne>
<BlazorSample.Components.ComponentTwo @ref="ComponentTwo"></BlazorSample.Components.ComponentTwo>
</div>
@code{
public BlazorSample.Components.ComponentOne ComponentOne;
public BlazorSample.Components.ComponentTwo ComponentTwo;
}
ComponentOne.razor
@code {
// this would normally contain something useful
public string ThisIsNotUseful = "This is just a sample";
}
ComponentTwo.razor
@code {
// this would normally contain something useful
public string ThisIsNotUsefulEither = "This is just a sample";
}
Index.razor
@page "/"
@inject NavigationManager NavigationManager
<button class="btn btn-primary"
@onclick="@(() => NavigationManager.NavigateTo("/samplepage"))">
Go to Sample Page
</button>
SamplePage.razor
@page "/samplepage"
<div class="row">
<div class="col-12">
CompOne is null: @(CompOne == null)
</div>
</div>
<div class="row">
<div class="col-12">
CompTwo is null: @(CompTwo == null)
</div>
</div>
@code{
[CascadingParameter(Name = "CompOne")]
public BlazorSample.Components.ComponentOne CompOne { get; set; }
[CascadingParameter(Name = "CompTwo")]
public BlazorSample.Components.ComponentTwo CompTwo { get; set; }
}
原因
正如引用的 HTML 元素直到渲染后才链接,引用的组件也不是。这是因为组件是作为 BuildRenderTree 的一部分创建的,此时引用已链接起来,就像这样。
<SurveyPrompt @ref=MySurveyPrompt Title="How is Blazor working for you?" />
@code
{
SurveyPrompt MySurveyPrompt;
}
将转译为后续 C#(查看 obj\Debug\net5.0\Razor\Pages
)
__builder.OpenComponent<BlazorApp58.Shared.SurveyPrompt>(1);
__builder.AddAttribute(2, "Title", "How is Blazor working for you?");
// This is the line that captures the reference
__builder.AddComponentReferenceCapture(3, (__value) => {
MySurveyPrompt = (BlazorApp58.Shared.SurveyPrompt)__value;
});
__builder.CloseComponent();
因此,第一次渲染(即直接导航)时,在渲染期间(即渲染 CascadingValue
时)没有参考 - 但是当您单击按钮或其他东西时 NavigationManager.NavigateTo
Blazor 将自动 re-render 检查更改。此时,您现在有要呈现的引用。
解决方案
- 首先,在您的布局中添加一个字段或 属性
bool HasRendered
。
- 然后在您的 UI 中,确保第一次呈现(在您有引用之前)不呈现除您引用的组件之外的任何子内容。
@if (HasRendered)
{
<CascadingValue Value=@ComponentOne>
<CascadingValue Value=@ComponentTwo>
... your markup here ...
</CascadingValue>
</CascadingValue>
}
<ComponentOne ...../>
<ComponentTwo ...../>
- 然后,在第一次呈现修复组件引用后,允许完全呈现消费者标记并告诉 Blazor re-render。
在 OnAfterPaint
中执行以下操作
if (firstRender)
{
HasRendered = true;
StateHasChanged();
}
额外
因为 CascadingValue 组件的值在任何消费者使用它们时都不会改变,您还可以添加 IsFixed=True
,这将提高渲染性能。
级联参数是向下执行的,页面刷新不考虑父组件实例,因此为null。检查以下 link 以供参考。
父实例:
我有几个组件想在整个 Blazor 应用程序中共享。这些恰好是 SyncFusion 组件——一个是 SfToast,一个是 SfDialog。我认为一个简单的方法是将组件放在 MainLayout.razor 上,然后对每个组件使用 <CascadingValue>
将引用传递给所有子页面和组件。
只要通过 <NavLink>
元素或使用 NavigationManager.NavigateTo()
导航到页面,这就可以正常工作。然而,如果一个页面被深度 linked 或刷新,最外层的 <CascadingValue>
变为空。为了解决这个问题,我创建了一个额外的虚拟 <CascadingValue>
作为最外层,以确保我真正关心的值在刷新或直接 link 时被填充,但这感觉像是 hack。我想知道我这样做的方式是否存在固有错误,导致最外层的 <CascadingValue>
在刷新时变为 null。
下面是一些示例代码来说明问题。这是非常人为的,但这是我能想出创建一个显示问题的 minimal reproducible example 的唯一方法。
如果您 运行 项目并单击“转到示例页面”按钮,您将看到 CompOne 和 CompTwo 组件引用都设置为值,如预期的那样。但是,如果您随后刷新页面,您将看到 CompOne 引用(最外面的 <CascadingValue>
)现在为空。
项目的布局如下(基于默认的 Blazor Server 模板创建,因此我只显示了我进行修改的区域):
+ BlazorSample | + Components (I added this folder) | - ComponentOne.razor | - ComponentTwo.razor | + Pages | - Index.razor | - SamplePage.razor | + Shared | - MainLayout.razor
MainLayout.razor
@inherits LayoutComponentBase
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
</div>
<div class="content px-4">
<CascadingValue Name="CompOne" Value="ComponentOne">
<CascadingValue Name="CompTwo" Value="ComponentTwo">
@Body
</CascadingValue>
</CascadingValue>
</div>
<BlazorSample.Components.ComponentOne @ref="ComponentOne"></BlazorSample.Components.ComponentOne>
<BlazorSample.Components.ComponentTwo @ref="ComponentTwo"></BlazorSample.Components.ComponentTwo>
</div>
@code{
public BlazorSample.Components.ComponentOne ComponentOne;
public BlazorSample.Components.ComponentTwo ComponentTwo;
}
ComponentOne.razor
@code {
// this would normally contain something useful
public string ThisIsNotUseful = "This is just a sample";
}
ComponentTwo.razor
@code {
// this would normally contain something useful
public string ThisIsNotUsefulEither = "This is just a sample";
}
Index.razor
@page "/"
@inject NavigationManager NavigationManager
<button class="btn btn-primary"
@onclick="@(() => NavigationManager.NavigateTo("/samplepage"))">
Go to Sample Page
</button>
SamplePage.razor
@page "/samplepage"
<div class="row">
<div class="col-12">
CompOne is null: @(CompOne == null)
</div>
</div>
<div class="row">
<div class="col-12">
CompTwo is null: @(CompTwo == null)
</div>
</div>
@code{
[CascadingParameter(Name = "CompOne")]
public BlazorSample.Components.ComponentOne CompOne { get; set; }
[CascadingParameter(Name = "CompTwo")]
public BlazorSample.Components.ComponentTwo CompTwo { get; set; }
}
原因
正如引用的 HTML 元素直到渲染后才链接,引用的组件也不是。这是因为组件是作为 BuildRenderTree 的一部分创建的,此时引用已链接起来,就像这样。
<SurveyPrompt @ref=MySurveyPrompt Title="How is Blazor working for you?" />
@code
{
SurveyPrompt MySurveyPrompt;
}
将转译为后续 C#(查看 obj\Debug\net5.0\Razor\Pages
)
__builder.OpenComponent<BlazorApp58.Shared.SurveyPrompt>(1);
__builder.AddAttribute(2, "Title", "How is Blazor working for you?");
// This is the line that captures the reference
__builder.AddComponentReferenceCapture(3, (__value) => {
MySurveyPrompt = (BlazorApp58.Shared.SurveyPrompt)__value;
});
__builder.CloseComponent();
因此,第一次渲染(即直接导航)时,在渲染期间(即渲染 CascadingValue
时)没有参考 - 但是当您单击按钮或其他东西时 NavigationManager.NavigateTo
Blazor 将自动 re-render 检查更改。此时,您现在有要呈现的引用。
解决方案
- 首先,在您的布局中添加一个字段或 属性
bool HasRendered
。 - 然后在您的 UI 中,确保第一次呈现(在您有引用之前)不呈现除您引用的组件之外的任何子内容。
@if (HasRendered)
{
<CascadingValue Value=@ComponentOne>
<CascadingValue Value=@ComponentTwo>
... your markup here ...
</CascadingValue>
</CascadingValue>
}
<ComponentOne ...../>
<ComponentTwo ...../>
- 然后,在第一次呈现修复组件引用后,允许完全呈现消费者标记并告诉 Blazor re-render。
在 OnAfterPaint
中执行以下操作
if (firstRender)
{
HasRendered = true;
StateHasChanged();
}
额外
因为 CascadingValue 组件的值在任何消费者使用它们时都不会改变,您还可以添加 IsFixed=True
,这将提高渲染性能。
级联参数是向下执行的,页面刷新不考虑父组件实例,因此为null。检查以下 link 以供参考。
父实例: