Каскадные значения и параметры ASP.NET Core Blazor

Примечание.

Это не последняя версия этой статьи. В текущем выпуске см . версию .NET 8 этой статьи.

Внимание

Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.

В текущем выпуске см . версию .NET 8 этой статьи.

В этой статье содержатся сведения о передаче данных из компонента-предка Razor в компоненты-потомки.

Каскадные значения и параметры — это удобный способ передачи данных по иерархии компонентов из компонента-предка в любое количество компонентов, расположенных ниже в иерархии. В отличие от параметров компонента, каскадные значения и параметры не требуют назначения атрибута для каждого компонента-потомка, где используются данные. Каскадные значения и параметры также позволяют компонентам согласовываться друг с другом в иерархии компонентов.

Примечание.

Примеры кода в этой статье используют типы ссылок, допускающие значение NULL (NRTs) и статический анализ состояния .NET компилятора NULL, которые поддерживаются в ASP.NET Core в .NET 6 или более поздней версии. При назначении ASP.NET Core 5.0 или более ранней версии удалите обозначение типа NULL (?) из CascadingType?, , @ActiveTab?, TabSet?RenderFragment?ITab?и string? типы в примерах статьи.

Каскадные значения корневого уровня

Каскадные значения корневого уровня можно зарегистрировать для всей иерархии компонентов. Поддерживаются именованные каскадные значения и подписки для уведомлений об обновлении.

Следующий класс используется в примерах этого раздела.

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

Следующие регистрации выполняются в файле приложения Program с помощью AddCascadingValue:

  • Dalek значение свойства Units для зарегистрировано в качестве фиксированного каскадного значения.
  • Вторая Dalek регистрация с другим значением свойства для Units имеет имя "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; }
}

В следующем примере 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> компонент () компонентом RoutesComponents/Routes.razorи внедряйте глобальную интерактивную отрисовку на стороне сервера (интерактивная служба SSR). Пример см. в CascadingValue разделе компонента .

Компонент CascadingValue

Компонент-предок предоставляет каскадное значение с помощью компонента CascadingValue платформы Blazor, который заключает поддерево иерархии компонентов и предоставляет одно значение для всех компонентов в его поддереве.

В следующем примере показан поток сведений о теме вниз иерархии компонентов, чтобы предоставить класс стилей CSS к кнопкам в дочерних компонентах.

ThemeInfo Следующий класс C# указывает сведения о теме.

Примечание.

В примерах в этом разделе используется пространство имен приложения BlazorSample. Экспериментируя с кодом в собственном примере приложения, измените пространство имен приложения на пространство имен своего примера приложения.

ThemeInfo.cs:

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

Следующий компонент макетаBody задает сведения о теме оформления () как каскадное значение для всех компонентов, составляющих текст макета свойства ThemeInfo. ButtonClass присваивается значение btn-success, которое является стилем кнопки начальной загрузки. Любой компонент-потомок в иерархии компонентов может использовать свойство ButtonClass через каскадное значение ThemeInfo.

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>
    <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веб-приложения предоставляют альтернативные подходы к каскадным значениям, которые применяются более широко к приложению, чем они предоставляются с помощью одного файла макета:

  • Заключите разметку Routes компонента в компонент, чтобы указать данные в CascadingValue виде каскадного значения для всех компонентов приложения.

    В следующем примере каскады ThemeInfo данных из Routes компонента.

    Routes.razor:

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

    Примечание.

    Оболочка экземпляра компонента в компоненте (Components/App.razor) с компонентом CascadingValue не поддерживается.RoutesApp

  • Укажите каскадное значение корневого уровня в качестве службы, вызвав AddCascadingValue метод расширения в построителе коллекций служб.

    В следующем примере каскадные ThemeInfo данные из Program файла.

    Program.cs

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

Дополнительные сведения см. в следующих разделах этой статьи:

Атрибут [CascadingParameter]

Чтобы использовать каскадные значения, компоненты-потомки объявляют каскадные параметры с помощью атрибута [CascadingParameter]. Каскадные значения привязаны к каскадным параметрам по типу. Каскадное применение нескольких значений одного и того же типа рассматривается в разделе Каскадное применение нескольких значений далее в этой статье.

Следующий компонент привязывает каскадное значение ThemeInfo к каскадному параметру, при необходимости используя то же имя ThemeInfo. Параметр используется для задания класса CSS для кнопки Increment Counter (Themed).

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"
@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 rerender.

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 в интерактивную границу отрисовки, сериализуются автоматически, если они являются JSсериализуемыми в on-serializable или возникает ошибка.
    • Состояние, хранящееся в PersistentComponentState сериализуемом и восстановленном автоматически, если оно JSявляется сериализуемым в on-serializable или возникает ошибка.

Каскадные параметры не JSсериализуются по сети, так как типичные шаблоны использования каскадных параметров несколько похожи на службы DI. Часто существуют варианты каскадных параметров для конкретной платформы, поэтому разработчики не смогут использовать платформу, если платформа перестала использовать серверные интерактивные версии или версии WebAssembly. Кроме того, многие каскадные значения параметров в целом не сериализуются, поэтому было бы нецелесообразно обновить существующие приложения, если вам пришлось остановить использование всех несериализируемых каскадных значений параметров.

Рекомендации.

  • Если необходимо сделать состояние доступным для всех интерактивных компонентов в качестве каскадного параметра, рекомендуется использовать каскадные значения корневого уровня. Шаблон фабрики доступен, и приложение может выдавать обновленные значения после запуска приложения. Каскадные значения корневого уровня доступны для всех компонентов, включая интерактивные компоненты, так как они обрабатываются как службы DI.

  • Для авторов библиотек компонентов можно создать метод расширения для потребителей библиотеки, как показано ниже.

    builder.Services.AddLibraryCascadingParameters();
    

    Поручите разработчикам вызывать метод расширения. Это звуковая альтернатива указаниям им добавить <RootComponent> компонент в их MainLayout компонент.

Каскадное применение нескольких значений

Чтобы выполнить каскадное применение нескольких значений одного типа в одном поддереве, укажите уникальную строку Name для каждого компонента CascadingValue и его соответствующих атрибутов [CascadingParameter].

В следующем примере два компонента 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; }
}

Передача данных в иерархии компонентов

Каскадные параметры также позволяют компонентам передавать данные в рамках иерархии компонентов. Рассмотрим следующий пример набора вкладок пользовательского интерфейса, в котором компонент набора вкладок поддерживает ряд отдельных вкладок.

Примечание.

В примерах в этом разделе используется пространство имен приложения BlazorSample. Экспериментируя с кодом в собственном примере приложения, измените пространство имен приложения на пространство имен своего примера приложения.

Создайте интерфейс ITab, который реализуется вкладками в папке с именем UIInterfaces.

UIInterfaces/ITab.cs:

using Microsoft.AspNetCore.Components;

namespace BlazorSample.UIInterfaces;

public interface ITab
{
    RenderFragment ChildContent { get; }
}

Примечание.

Дополнительные сведения о RenderFragment см. в статье Компоненты Razor ASP.NET Core.

Следующий компонент TabSet содержит набор вкладок. Компоненты набора вкладок Tab, которые создаются далее в этом разделе, предоставляют элементы списка (<li>...</li>) для списка (<ul>...</ul>).

Дочерние компоненты 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;
}

Дополнительные ресурсы