Partilhar via


ASP.NET Core Blazor valores e parâmetros em cascata

Observação

Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão .NET 9 deste artigo.

Advertência

Esta versão do ASP.NET Core não é mais suportada. Para obter mais informações, consulte a Política de suporte do .NET e .NET Core. Para a versão atual, consulte a versão .NET 9 deste artigo.

Importante

Estas informações referem-se a um produto de pré-lançamento que pode ser substancialmente modificado antes de ser lançado comercialmente. A Microsoft não oferece garantias, expressas ou implícitas, em relação às informações fornecidas aqui.

Para a versão atual, consulte a versão .NET 9 deste artigo.

Este artigo explica como transferir dados de um componente ancestral Razor, para componentes descendentes.

Os valores e parâmetros em cascata fornecem uma maneira conveniente de fluir dados para baixo de uma hierarquia de componentes de um componente ancestral para qualquer número de componentes descendentes. Ao contrário parâmetros de componente, os valores e parâmetros em cascata não exigem uma atribuição de atributo para cada componente descendente onde os dados são consumidos. Os valores e parâmetros em cascata também permitem que os componentes se coordenem entre si em uma hierarquia de componentes.

Observação

Os exemplos de código neste artigo adotam tipos de referência anuláveis (NRTs) e a análise estática do estado nulo do compilador .NET, os quais são suportados no ASP.NET Core no .NET 6 ou versões posteriores. Ao visar o ASP.NET Core 5.0 ou versões anteriores, remova a designação de tipo nulo (?) dos tipos CascadingType?, @ActiveTab?, RenderFragment?, ITab?, TabSet?e string? nos exemplos do artigo.

Valores em cascata no nível raiz

Os valores em cascata no nível raiz podem ser registrados para toda a hierarquia de componentes. Há suporte para valores em cascata nomeados e assinaturas para notificações de atualização.

A classe a seguir é usada nos exemplos desta seção.

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

Os seguintes registos são feitos no ficheiro Program da aplicação com AddCascadingValue:

  • Dalek com um valor de propriedade para Units é registrado como um valor em cascata fixo.
  • Um segundo registro Dalek com um valor de propriedade diferente para Units é chamado de "AlphaGroup".
builder.Services.AddCascadingValue(sp => new Dalek { Units = 123 });
builder.Services.AddCascadingValue("AlphaGroup", sp => new Dalek { Units = 456 });

O componente Daleks a seguir exibe os valores em cascata.

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

No exemplo a seguir, Dalek é registrado como um valor em cascata usando CascadingValueSource<T>, onde <T> é o tipo. O sinalizador isFixed indica se o valor é fixo. Se for falso, todos os destinatários são inscritos para notificações de atualização, que são emitidas ao chamar NotifyChangedAsync. As assinaturas criam sobrecarga e reduzem o desempenho, portanto, defina isFixedtrue se o valor não for alterado.

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

    return source;
});

Advertência

Registrar um tipo de componente como um valor em cascata de nível raiz não registra serviços adicionais para o tipo ou permite a ativação do serviço no componente.

Trate os serviços necessários separadamente dos valores em cascata, registrando-os separadamente do tipo em cascata.

Evite usar AddCascadingValue para registrar um tipo de componente como um valor em cascata. Em vez disso, envolva o <Router>...</Router> no componente Routes (Components/Routes.razor) com o componente e adote a renderização interativa global do lado do servidor (SSR interativo). Para obter um exemplo, consulte a seção do componente .

componente CascadingValue

Um componente ancestral fornece um valor em cascata usando o componente CascadingValue da estrutura Blazor, que envolve uma subárvore de uma hierarquia de componentes e fornece um único valor para todos os componentes dentro de sua subárvore.

O exemplo a seguir demonstra o fluxo das informações de tema através da hierarquia de componentes para fornecer uma classe CSS de estilo aos botões em componentes filho.

A classe C# ThemeInfo a seguir especifica as informações do tema.

Observação

Para os exemplos nesta seção, o namespace do aplicativo é BlazorSample. Ao experimentar o código em seu próprio aplicativo de exemplo, altere o namespace do aplicativo para o namespace do aplicativo de exemplo.

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

O componente de layout a seguir especifica informações de tema (ThemeInfo) como valor em cascata para todos os componentes que fazem parte da estrutura de layout da propriedade Body. ButtonClass é atribuído um valor de btn-success, que é um estilo de botão Bootstrap. Qualquer componente descendente na hierarquia de componentes pode usar a propriedade ButtonClass por meio do ThemeInfo valor em cascata.

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 Apps fornecem abordagens alternativas para valores em cascata que se aplicam mais amplamente ao aplicativo do que fornecê-los por meio de um único arquivo de layout:

  • Envolva a marcação do componente Routes no componente CascadingValue para especificar os dados como um valor em cascata para todos os componentes da aplicação.

    O exemplo a seguir encadeia os dados de ThemeInfo a partir do componente Routes.

    Routes.razor:

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

    Observação

    Encapsular a instância do componente Routes dentro do componente App (Components/App.razor) usando um componente CascadingValue não é suportado.

  • Especifique um valor em cascata de nível raiz como um serviço chamando o método de extensão AddCascadingValue no construtor de coleção de serviços.

    O exemplo a seguir encadeia os dados ThemeInfo do arquivo Program.

    Program.cs

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

Para obter mais informações, consulte as seguintes seções deste artigo:

Atributo [CascadingParameter]

Para fazer uso de valores em cascata, os componentes descendentes declaram parâmetros em cascata usando o atributo [CascadingParameter]. Os valores em cascata estão vinculados a parâmetros em cascata por tipo. A cascata de múltiplos valores do mesmo tipo é abordada na seção Cascata de múltiplos valores mais adiante neste artigo.

O componente seguinte vincula o valor em cascata ThemeInfo a um parâmetro em cascata, opcionalmente utilizando o mesmo nome que ThemeInfo. O parâmetro é usado para definir a classe CSS para o botão 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"

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

Semelhante a um parâmetro de componente regular, os componentes que recebem um parâmetro em cascata são re-renderizados quando o valor em cascata é alterado. Por exemplo, configurar uma instância de tema diferente faz com que o componente da seção componente seja rerenderizado.

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 pode ser usado para indicar que um parâmetro em cascata não muda após a inicialização.

Valores/parâmetros em cascata e limites do modo de renderização

Os parâmetros em cascata não passam dados entre os limites do modo de renderização:

  • As sessões interativas são executadas em um contexto diferente das páginas que usam renderização estática do lado do servidor (SSR estático). Não há nenhum requisito de que o servidor que produz a página seja mesmo a mesma máquina que hospeda alguma sessão posterior do Interactive Server, inclusive para componentes WebAssembly em que o servidor é uma máquina diferente do cliente. O benefício da renderização estática do lado do servidor (SSR estático) é obter o desempenho máximo da renderização pura de HTML sem estado.

  • O estado que cruza o limite entre renderização estática e interativa deve ser serializável. Componentes são objetos arbitrários que fazem referência a uma vasta cadeia de outros objetos, incluindo o renderizador, o contêiner DI e cada instância de serviço DI. Você deve explicitamente fazer com que o estado seja serializado a partir do SSR estático para torná-lo disponível em componentes subsequentes renderizados interativamente. São adotadas duas abordagens:

    • Por meio da estrutura Blazor, os parâmetros passados através de um SSR estático para o limite de renderização interativa são serializados automaticamente se forem serializáveis por JSON, ou um erro é emitido.
    • O estado armazenado no PersistentComponentState é serializado e recuperado automaticamente se for serializável por JSON; caso contrário, é gerado um erro.

Os parâmetros em cascata não são serializáveis em JSON porque os padrões de uso típicos para parâmetros em cascata são um pouco como serviços DI. Muitas vezes, há variantes específicas da plataforma de parâmetros em cascata, portanto, seria inútil para os desenvolvedores se a estrutura impedisse os desenvolvedores de ter versões específicas de servidor interativo ou versões específicas do WebAssembly. Além disso, muitos valores de parâmetros em cascata em geral não são serializáveis, portanto, seria impraticável atualizar aplicativos existentes se você tivesse que parar de usar todos os valores de parâmetros em cascata não serializáveis.

Recomendações:

  • Se você precisar disponibilizar o estado para todos os componentes interativos como um parâmetro em cascata, recomendamos usar valores em cascata no nível raiz. Um padrão de fábrica está disponível e o aplicativo pode emitir valores atualizados após a inicialização do aplicativo. Os valores em cascata de nível raiz estão disponíveis para todos os componentes, incluindo componentes interativos, uma vez que são processados como serviços DI.

  • Para os autores de bibliotecas de componentes, pode-se criar um método de extensão para os utilizadores da biblioteca, semelhante ao seguinte:

    builder.Services.AddLibraryCascadingParameters();
    

    Instrua os desenvolvedores a chamar seu método de extensão. Esta é uma boa alternativa para orientá-los a adicionar um componente <RootComponent> no seu componente MainLayout.

Cascata de vários valores

Para encadear múltiplos valores do mesmo tipo dentro da mesma subárvore, forneça uma cadeia de caracteres Name única para cada componente CascadingValue e seus atributos[CascadingParameter]correspondentes.

No exemplo a seguir, dois componentes CascadingValue encadeiam diferentes instâncias de CascadingType:

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

@code {
    private CascadingType? parentCascadeParameter1;

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

Em um componente descendente, os parâmetros em cascata recebem seus valores em cascata do componente ancestral por Name:

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

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

Passar dados através de uma hierarquia de componentes

Os parâmetros em cascata também permitem que os componentes passem dados através de uma hierarquia de componentes. Considere o seguinte exemplo de conjunto de guias da interface do usuário, onde um componente de conjunto de guias mantém uma série de guias individuais.

Observação

Para os exemplos nesta seção, o namespace do aplicativo é BlazorSample. Ao experimentar o código em seu próprio aplicativo de exemplo, altere o namespace para o namespace do aplicativo de exemplo.

Crie uma interface ITab que as abas implementem numa pasta chamada UIInterfaces.

UIInterfaces/ITab.cs:

using Microsoft.AspNetCore.Components;

namespace BlazorSample.UIInterfaces;

public interface ITab
{
    RenderFragment ChildContent { get; }
}

Observação

Para obter mais informações sobre RenderFragment, consulte componentes do ASP.NET Core Razor.

O componente TabSet, a seguir, mantém um conjunto de abas. Os componentes Tab do conjunto de guias, que são criados posteriormente nesta seção, fornecem os itens da lista (<li>...</li>) para a lista (<ul>...</ul>).

Os componentes do filho Tab não são passados explicitamente como parâmetros para o TabSet. Em vez disso, os componentes filho Tab fazem parte do conteúdo filho do TabSet. No entanto, o TabSet ainda precisa de uma referência a cada componente Tab para que possa renderizar os cabeçalhos e a guia ativa. Para habilitar essa coordenação sem exigir código adicional, o do componente TabSet pode fornecer a si mesmo como um valor em cascata que é então captado pelos componentes Tab descendentes.

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

Os componentes descendentes Tab capturam o TabSet envolvente como um parâmetro em cascata. Os componentes Tab são adicionados ao TabSet e coordenam-se para definir a guia ativa.

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

O componente ExampleTabSet a seguir usa o componente TabSet, que contém três componentes 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;
}

Recursos adicionais