最外层的 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 以供参考。

父实例: