ASP.NET Core Blazor 级联值和参数

注意

此版本不是本文的最新版本。 有关当前版本,请参阅本文.NET 9 版本。

警告

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 有关当前版本,请参阅本文.NET 9 版本。

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

有关当前版本,请参阅本文.NET 9 版本。

本文介绍如何将数据从上级 Razor 组件流向下级组件。

级联值和参数提供了一种方便的方法,可将数据沿组件层次结构从祖先组件向下流向任意数量的后代组件。 不同于组件参数,级联值和参数不需要对使用数据的每个后代组件分配特性。 级联值和参数还允许组件在组件层次结构中相互协调。

注意

本文中的代码示例采用在 .NET 6 或更高版本的 ASP.NET Core 中支持的可为空的引用类型 (NRT) 和 .NET 编译器 Null 状态静态分析。 面向 ASP.NET Core 5.0 或更早版本时,请从文章示例中的 CascadingType?@ActiveTab?RenderFragment?ITab?TabSet?string? 类型中删除 NULL 类型指定 (?)。

根级别级联值

可以为整个组件层次结构注册根级别级联值。 支持用于更新通知的命名级联值和订阅。

本部分的示例中使用了以下类。

Dalek.cs:

// "Dalek" ©Terry Nation https://www.imdb.com/name/nm0622334/
// "Doctor Who" ©BBC https://www.bbc.co.uk/programmes/b006q2x0

namespace BlazorSample;

public class Dalek
{
    public int Units { get; set; }
}
// "Dalek" ©Terry Nation https://www.imdb.com/name/nm0622334/
// "Doctor Who" ©BBC https://www.bbc.co.uk/programmes/b006q2x0

namespace BlazorSample;

public class Dalek
{
    public int Units { get; set; }
}

在应用的 Program 文件中使用 AddCascadingValue 进行了以下注册:

  • 具有 Units 属性值的 Dalek 注册为固定级联值。
  • 具有不同 Units 属性值的第二个 Dalek 注册命名为“AlphaGroup”。
builder.Services.AddCascadingValue(sp => new Dalek { Units = 123 });
builder.Services.AddCascadingValue("AlphaGroup", sp => new Dalek { Units = 456 });

以下 Daleks 组件显示级联值。

Daleks.razor:

@page "/daleks"

<PageTitle>Daleks</PageTitle>

<h1>Root-level Cascading Value Example</h1>

<ul>
    <li>Dalek Units: @Dalek?.Units</li>
    <li>Alpha Group Dalek Units: @AlphaGroupDalek?.Units</li>
</ul>

<p>
    Dalek© <a href="https://www.imdb.com/name/nm0622334/">Terry Nation</a><br>
    Doctor Who© <a href="https://www.bbc.co.uk/programmes/b006q2x0">BBC</a>
</p>

@code {
    [CascadingParameter]
    public Dalek? Dalek { get; set; }

    [CascadingParameter(Name = "AlphaGroup")]
    public Dalek? AlphaGroupDalek { get; set; }
}
@page "/daleks"

<PageTitle>Daleks</PageTitle>

<h1>Root-level Cascading Value Example</h1>

<ul>
    <li>Dalek Units: @Dalek?.Units</li>
    <li>Alpha Group Dalek Units: @AlphaGroupDalek?.Units</li>
</ul>

<p>
    Dalek© <a href="https://www.imdb.com/name/nm0622334/">Terry Nation</a><br>
    Doctor Who© <a href="https://www.bbc.co.uk/programmes/b006q2x0">BBC</a>
</p>

@code {
    [CascadingParameter]
    public Dalek? Dalek { get; set; }

    [CascadingParameter(Name = "AlphaGroup")]
    public Dalek? AlphaGroupDalek { get; set; }
}

在以下示例中,Dalek 使用 CascadingValueSource<T> 注册为级联值,其中 <T> 为类型。 isFixed 标志指示值是否固定。 如果为 false,则所有接收方都会订阅更新通知,这些通知通过调用 NotifyChangedAsync 发出。 订阅会产生开销并降低性能,因此,如果值不变,请将 isFixed 设置为 true

builder.Services.AddCascadingValue(sp =>
{
    var dalek = new Dalek { Units = 789 };
    var source = new CascadingValueSource<Dalek>(dalek, isFixed: false);

    return source;
});

警告

将组件类型注册为根级级联值不会为该类型注册其他服务或允许在组件中激活服务。

将所需服务与级联值分开处理,并将它们与级联类型分开注册。

避免使用 AddCascadingValue 将组件类型注册为级联值。 相反,使用组建将 <Router>...</Router> 包装在 Routes 组件 (Components/Routes.razor) 中,并采用全局交互式服务器端呈现(交互式 SSR)。 有关示例,请参阅 CascadingValue 组件 部分。

CascadingValue 组件

祖先组件使用 Blazor 框架的 CascadingValue 组件提供级联值,该组件包装组件层次结构的子树,并向其子树中的所有组件提供单个值。

下面的示例演示了主题信息沿组件层次结构向下流动,以便为子组件中的按钮提供 CSS 样式的类。

以下 ThemeInfo C# 类会指定主题信息。

注意

对于本部分中的示例,应用的命名空间为 BlazorSample。 在自己的示例应用中试验代码时,请将应用的命名空间更改为示例应用的命名空间。

ThemeInfo.cs:

namespace BlazorSample;

public class ThemeInfo
{
    public string? ButtonClass { get; set; }
}
namespace BlazorSample;

public class ThemeInfo
{
    public string? ButtonClass { get; set; }
}
namespace BlazorSample.UIThemeClasses;

public class ThemeInfo
{
    public string? ButtonClass { get; set; }
}
namespace BlazorSample.UIThemeClasses;

public class ThemeInfo
{
    public string? ButtonClass { get; set; }
}
namespace BlazorSample.UIThemeClasses
{
    public class ThemeInfo
    {
        public string ButtonClass { get; set; }
    }
}
namespace BlazorSample.UIThemeClasses
{
    public class ThemeInfo
    {
        public string ButtonClass { get; set; }
    }
}

下面的布局组件将主题信息 (ThemeInfo) 指定为构成 Body 属性布局主体的所有组件的级联值。 ButtonClass 分配有值 btn-success,这是一种启动按钮样式。 组件层次结构中的任何后代组件都可以通过 ThemeInfo 级联值来使用 ButtonClass 属性。

MainLayout.razor:

@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
        </div>

        <CascadingValue Value="theme">
            <article class="content px-4">
                @Body
            </article>
        </CascadingValue>
    </main>
</div>

<div id="blazor-error-ui" data-nosnippet>
    An unhandled error has occurred.
    <a href="." class="reload">Reload</a>
    <span class="dismiss">🗙</span>
</div>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
        </div>

        <CascadingValue Value="theme">
            <article class="content px-4">
                @Body
            </article>
        </CascadingValue>
    </main>
</div>

<div id="blazor-error-ui" data-nosnippet>
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
        </div>

        <CascadingValue Value="@theme">
            <article class="content px-4">
                @Body
            </article>
        </CascadingValue>
    </main>
</div>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <CascadingValue Value="@theme">
            <div class="content px-4">
                @Body
            </div>
        </CascadingValue>
    </main>
</div>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <div class="main">
        <CascadingValue Value="@theme">
            <div class="content px-4">
                @Body
            </div>
        </CascadingValue>
    </div>
</div>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses

<div class="sidebar">
    <NavMenu />
</div>

<div class="main">
    <CascadingValue Value="theme">
        <div class="content px-4">
            @Body
        </div>
    </CascadingValue>
</div>

@code {
    private ThemeInfo theme = new ThemeInfo { ButtonClass = "btn-success" };
}

Blazor Web App 为级联值提供了替代方法,与通过单个布局文件提供这些值相比,这种方法可以更广泛地应用于应用:

  • Routes 组件的标记包装在 CascadingValue 组件中,以指定数据作为应用所有组件的级联值。

    以下示例从 Routes 组件级联 ThemeInfo 数据。

    Routes.razor:

    <CascadingValue Value="theme">
        <Router ...>
            ...
        </Router>
    </CascadingValue>
    
    @code {
        private ThemeInfo theme = new() { ButtonClass = "btn-success" };
    }
    

    注意

    不支持使用 CascadingValue 组件将 Routes 组件实例包装在 App 组件(Components/App.razor)中。

  • 通过在服务集合生成器上调用 AddCascadingValue 扩展方法,将根级别级联值指定为服务。

    以下示例级联 Program 文件中的数据 ThemeInfo

    Program.cs

    builder.Services.AddCascadingValue(sp => 
        new ThemeInfo() { ButtonClass = "btn-primary" });
    

有关详细信息,请参阅本文以下各节:

[CascadingParameter] 特性

为了使用级联值,后代组件使用 [CascadingParameter] 特性来声明级联参数。 级联值按类型绑定到级联参数。 本文后面的级联多个值部分中介绍了相同类型的级联多个值。

以下组件将 ThemeInfo 级联值绑定到级联参数,并且可以选择使用相同的名称 ThemeInfo。 该参数用于设置 Increment Counter (Themed) 按钮的 CSS 类。

ThemedCounter.razor:

@page "/themed-counter"

<PageTitle>Themed Counter</PageTitle>

<h1>Themed Counter Example</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button 
        class="btn @(ThemeInfo is not null ? ThemeInfo.ButtonClass : string.Empty)" 
        @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    protected ThemeInfo? ThemeInfo { get; set; }

    private void IncrementCount() => currentCount++;
}
@page "/themed-counter"

<PageTitle>Themed Counter</PageTitle>

<h1>Themed Counter Example</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button 
        class="btn @(ThemeInfo is not null ? ThemeInfo.ButtonClass : string.Empty)" 
        @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    protected ThemeInfo? ThemeInfo { get; set; }

    private void IncrementCount() => currentCount++;
}
@page "/themed-counter"
@using BlazorSample.UIThemeClasses

<h1>Themed Counter</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button 
        class="btn @(ThemeInfo is not null ? ThemeInfo.ButtonClass : string.Empty)" 
        @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    protected ThemeInfo? ThemeInfo { get; set; }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/themed-counter"
@using BlazorSample.UIThemeClasses

<h1>Themed Counter</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button 
        class="btn @(ThemeInfo is not null ? ThemeInfo.ButtonClass : string.Empty)" 
        @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    protected ThemeInfo? ThemeInfo { get; set; }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/themed-counter"
@using BlazorSample.UIThemeClasses

<h1>Themed Counter</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button class="btn @ThemeInfo.ButtonClass" @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    protected ThemeInfo ThemeInfo { get; set; }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/themed-counter"
@using BlazorSample.UIThemeClasses

<h1>Themed Counter</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button class="btn @ThemeInfo.ButtonClass" @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    protected ThemeInfo ThemeInfo { get; set; }

    private void IncrementCount()
    {
        currentCount++;
    }
}

与常规组件参数类似,当级联值改变时,接受级联参数的组件会重新呈现。 例如,配置不同的主题实例会导致 CascadingValue组件部分中的 ThemedCounter 组件重新呈现。

MainLayout.razor:

<main>
    <div class="top-row px-4">
        <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
    </div>

    <CascadingValue Value="theme">
        <article class="content px-4">
            @Body
        </article>
    </CascadingValue>
    <button @onclick="ChangeToDarkTheme">Dark mode</button>
</main>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };

    private void ChangeToDarkTheme()
    {
        theme = new() { ButtonClass = "btn-secondary" };
    }
}

CascadingValue<TValue>.IsFixed 可用于指示级联参数在初始化后不会更改。

级联值/参数和呈现模式边界

级联参数不会跨呈现模式边界传递数据:

  • 交互式会话在不同于那些使用静态服务器端呈现(静态 SSR)的页面的上下文中运行。 生成页面的服务器甚至不必是稍后托管交互式服务器会话的计算机,包括 WebAssembly 组件,其服务器与客户端的所在的计算机不同。 静态服务器端呈现(静态 SSR)的好处是获得无状态纯 HTML 呈现的完整性能。

  • 跨越静态和交互式渲染之间边界的状态必须可序列化。 组件可以是引用一连串大量其他对象的任意对象,包括呈现器、DI 容器和每个 DI 服务实例。 必须显式地让状态从静态 SSR 开始序列化,使其可用于随后的交互式呈现组件。 采用两种方法:

    • 通过 Blazor 框架,通过静态 SSR 传递到交互式渲染边界的参数如果可 JSON 序列化,则会自动序列化,否则会引发错误。
    • 储存在 PersistentComponentState 上的状态如果可序列化为 JSON,则可以自动序列化并恢复,或者引发错误。

级联参数不可序列化为 JSON,因为级联参数的典型使用模式有点类似于 DI 服务。 级联参数通常有平台专用的变体,因此,如果框架阻止开发人员使用服务器交互特有的版本或 WebAssembly 特有的版本,这对开发人员将毫无用处。 此外,许多级联参数值通常不可序列化,因此,如果必须停止使用所有不可序列化的级联参数值,则更新现有应用是不切实际的。

建议:

  • 如果需要让所有交互式组件都将状态用作级联参数,建议使用根级级联值。 工厂模式可用,应用可以在启动后发出更新值。 根级级级联值可用于所有组件,包括交互式组件,因为它们是作为 DI 服务处理的。

  • 组件库作者可以为库使用者创建一个扩展方法,与下文的相似:

    builder.Services.AddLibraryCascadingParameters();
    

    指示开发人员调用扩展方法。 这是一种合理的替代方法,可取代指导开发人员在 <RootComponent> 组件中添加 MainLayout 组件这一环节。

级联多个值

若要在同一子树内级联多个相同类型的值,请向每个 CascadingValue 组件及其相应的 [CascadingParameter] 特性提供唯一的 Name 字符串。

在下面的示例中,两个 CascadingValue 组件级联 CascadingType 的不同实例:

<CascadingValue Value="parentCascadeParameter1" Name="CascadeParam1">
    <CascadingValue Value="ParentCascadeParameter2" Name="CascadeParam2">
        ...
    </CascadingValue>
</CascadingValue>

@code {
    private CascadingType? parentCascadeParameter1;

    [Parameter]
    public CascadingType? ParentCascadeParameter2 { get; set; }
}

在后代组件中,级联参数按 Name 从祖先组件中接收其级联值:

@code {
    [CascadingParameter(Name = "CascadeParam1")]
    protected CascadingType? ChildCascadeParameter1 { get; set; }

    [CascadingParameter(Name = "CascadeParam2")]
    protected CascadingType? ChildCascadeParameter2 { get; set; }
}

跨组件层次结构传递数据

级联参数还允许组件跨组件层次结构传递数据。 请考虑下面的 UI 选项卡集示例,其中选项卡集组件维护一系列单独的选项卡。

注意

对于本部分中的示例,应用的命名空间为 BlazorSample。 在自己的示例应用中试验代码时,请将命名空间更改为示例应用的命名空间。

创建选项卡在名为 UIInterfaces 的文件夹中实现的 ITab 接口。

UIInterfaces/ITab.cs:

using Microsoft.AspNetCore.Components;

namespace BlazorSample.UIInterfaces;

public interface ITab
{
    RenderFragment ChildContent { get; }
}

注意

有关 RenderFragment 的详细信息,请参阅 ASP.NET Core Razor 组件

以下 TabSet 组件维护一组选项卡。 选项卡集的 Tab 组件(在本部分后面创建)为列表 (<ul>...</ul>) 提供列表项 (<li>...</li>)。

Tab 组件不会作为参数显式传递给 TabSet。 子 Tab 组件是 TabSet 的子内容的一部分。 但 TabSet 仍需要每个 Tab 组件的引用,以便它可以呈现标头和活动选项卡。若要在不需要额外代码的情况下启用此协调,TabSet 组件可以将自身作为级联值提供,然后由子代 Tab 组件选取。

TabSet.razor:

@using BlazorSample.UIInterfaces

<!-- Display the tab headers -->

<CascadingValue Value="this">
    <ul class="nav nav-tabs">
        @ChildContent
    </ul>
</CascadingValue>

<!-- Display body for only the active tab -->

<div class="nav-tabs-body p-4">
    @ActiveTab?.ChildContent
</div>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    public ITab? ActiveTab { get; private set; }

    public void AddTab(ITab tab)
    {
        if (ActiveTab is null)
        {
            SetActiveTab(tab);
        }
    }

    public void SetActiveTab(ITab tab)
    {
        if (ActiveTab != tab)
        {
            ActiveTab = tab;
            StateHasChanged();
        }
    }
}

后代 Tab 组件将包含的 TabSet 作为级联参数捕获。 Tab 组件将自己添加到 TabSet 和坐标以设置活动选项卡。

Tab.razor:

@using BlazorSample.UIInterfaces
@implements ITab

<li>
    <a @onclick="ActivateTab" class="nav-link @TitleCssClass" role="button">
        @Title
    </a>
</li>

@code {
    [CascadingParameter]
    public TabSet? ContainerTabSet { get; set; }

    [Parameter]
    public string? Title { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    private string? TitleCssClass => 
        ContainerTabSet?.ActiveTab == this ? "active" : null;

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

    private void ActivateTab()
    {
        ContainerTabSet?.SetActiveTab(this);
    }
}

以下 ExampleTabSet 组件使用 TabSet 组件,其中包含三个 Tab 组件。

ExampleTabSet.razor:

@page "/example-tab-set"

<TabSet>
    <Tab Title="First tab">
        <h4>Greetings from the first tab!</h4>

        <label>
            <input type="checkbox" @bind="showThirdTab" />
            Toggle third tab
        </label>
    </Tab>

    <Tab Title="Second tab">
        <h4>Hello from the second tab!</h4>
    </Tab>

    @if (showThirdTab)
    {
        <Tab Title="Third tab">
            <h4>Welcome to the disappearing third tab!</h4>
            <p>Toggle this tab from the first tab.</p>
        </Tab>
    }
</TabSet>

@code {
    private bool showThirdTab;
}

其他资源