Valores e parâmetros em cascata do ASP.NET Core Blazor

Observação

Esta não é a versão mais recente deste artigo. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Importante

Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.

Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Este artigo explica como fazer o fluxo de dados de um componente ancestral Razor para componentes descendentes.

Valores e parâmetros em cascata proporcionam uma forma conveniente de fluir dados para baixo em uma hierarquia de componentes, de um componente ancestral para qualquer número de componentes descendentes. Ao contrário dos Parâmetros de componente, valores e parâmetros em cascata não exigem uma designação de atributo para cada componente descendente em que os dados são consumidos. Os valores e parâmetros em cascata também permitem que os componentes se coordenem uns com os outros em uma hierarquia de componentes.

Observação

Os exemplos de código neste artigo adotam NRTs (tipos de referência anuláveis) e análise estática de estado nulo do compilador do .NET, que têm suporte no ASP.NET Core no .NET 6 ou posterior. Ao direcionar ASP.NET Core 5.0 ou anterior, 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; }
}

Os seguintes registros são feitos no arquivo Program do aplicativo com AddCascadingValue:

  • Dalek com um valor de propriedade para Units que é registrado como um valor em cascata fixo.
  • Um segundo registro Dalek com um valor de propriedade diferente para Units é nomeado "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; }
}

No exemplo a seguir, Dalek é registrado como um valor em cascata usando CascadingValueSource<T>, em que o tipo é <T>. O sinalizador isFixed indica se o valor é fixo. Se for falso, todos os destinatários serão inscritos para receber notificações de atualização, que são emitidas por meio da chamada NotifyChangedAsync. As assinaturas criam sobrecarga e reduzem o desempenho, portanto, defina isFixed como true 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;
});

Aviso

Registrar um tipo de componente como um valor em cascata de nível raiz não registra serviços adicionais para o tipo nem 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, encapsule o <Router>...</Router> no componente Routes (Components/Routes.razor) com o componente e adote a renderização interativa global do lado do servidor (SSR interativa). Para obter um exemplo, consulte a seção CascadingValue componente.

componente CascadingValue

Um componente ancestral fornece um valor em cascata usando o componente CascadingValue da estrutura do Blazor, que encapsula 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 de informações de tema na hierarquia de componentes para fornecer uma classe de estilo CSS a 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 será BlazorSample. Ao testar o código em seu próprio aplicativo de exemplo, troque o namespace do aplicativo pelo namespace do aplicativo de exemplo.

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

O componente de layout a seguir especifica informações de tema (ThemeInfo) como um valor em cascata para todos os componentes que compõem o corpo do layout da propriedade Body. Um valor de btn-success, que é um estilo de botão Bootstrap, é atribuído a ButtonClass. Qualquer componente descendente na hierarquia de componentes pode usar a propriedade ButtonClass por meio do valor em cascata 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 Os Aplicativos Web fornecem abordagens alternativas para valores em cascata que se aplicam de forma mais ampla ao aplicativo do que fornecê-los por meio de um arquivo de layout:

  • Encapsule a marcação do componente Routes em um componente CascadingValue para especificar os dados como um valor em cascata para todos os componentes do aplicativo.

    O exemplo a seguir deriva dados ThemeInfo em cascata a partir do componente Routes.

    Routes.razor:

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

    Observação

    Não há suporte para encapsular a instância do componente Routes no componente App (Components/App.razor) com um componente CascadingValue.

  • 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 deriva dados ThemeInfo em cascata a partir do componente Program.

    Program.cs

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

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

Atributo [CascadingParameter]

Para usar valores em cascata, os componentes descendentes declaram parâmetros em cascata usando o atributo [CascadingParameter]. Os valores em cascata estão associados a parâmetros em cascata por tipo. Vários valores em cascata do mesmo tipo são abordados na seção Cascata de vários valores posteriormente neste artigo.

O componente a seguir associa o valor em cascata ThemeInfo a um parâmetro em cascata, opcionalmente usando o mesmo nome de 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"
@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++;
    }
}

De modo semelhante a um parâmetro de componente regular, os componentes que aceitam um parâmetro em cascata serão gerados novamente quando o valor em cascata for alterado. Por exemplo, a configuração de uma instância de tema diferente faz com que o componente ThemedCounter da seção componente CascadingValue seja renderizado.

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 será alterado 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 a renderização estática (SSR estática). Não há nenhum requisito de que o servidor que produz a página seja mesmo o mesmo computador que hospeda algumas sessões posteriores do Servidor Interativo, incluindo para componentes WebAssembly em que o servidor é um computador diferente para o cliente. O benefício da renderização estática do lado do servidor (SSR estática) é obter o desempenho total da renderização HTML sem estado pura.

  • O estado que cruza o limite entre a renderização estática e interativa deve ser serializável. Os componentes são objetos arbitrários que fazem referência a uma vasta cadeia de outros objetos, incluindo o renderizador, o contêiner de DI e cada instância de serviço de DI. Você deve explicitamente serializar o estado a partir da SSR estática para disponibilizá-lo nos componentes subsequentes renderizados interativamente. Duas abordagens são adotadas:

    • Por meio da estrutura Blazor, os parâmetros passados por uma SSR estática para um limite de renderização interativa são automaticamente serializados se forem serializados via JSON ou geram um erro.
    • O estado armazenado no PersistentComponentState será serializado e recuperado automaticamente se for serializado por JSON ou um erro for gerado.

Parâmetros em cascata não são JSon-serializable porque os padrões de uso típicos para parâmetros em cascata são um pouco parecidos com os serviços de DI. Geralmente, 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 interativas do servidor ou versões específicas do WebAssembly. Além disso, muitos valores de parâmetro 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âmetro 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. Há um padrão de fábrica disponível e o aplicativo pode emitir valores atualizados após sua inicialização. Os valores em cascata de nível raiz estão disponíveis para todos os componentes, incluindo componentes interativos, pois são processados como serviços de DI.

  • Para autores da biblioteca de componentes, você pode criar um método de extensão para consumidores de biblioteca semelhante ao seguinte:

    builder.Services.AddLibraryCascadingParameters();
    

    Instrua os desenvolvedores a chamar seu método de extensão. Essa é uma boa alternativa para instruí-los a adicionar um componente <RootComponent> em seus componentes MainLayout.

Valores múltiplos em cascata

Para colocar em cascata vários valores do mesmo tipo na mesma subárvore, forneça uma cadeia de caracteres Name exclusiva para cada componente CascadingValue e seus atributos [CascadingParameter] correspondentes.

No exemplo a seguir, dois componentes CascadingValue colocam em cascata instâncias diferentes 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 em uma hierarquia de componentes

Os parâmetros em cascata também permitem que os componentes passem dados por uma hierarquia de componentes. Leve em consideração o exemplo de conjunto de guias da interface do usuário a seguir, em que 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 será BlazorSample. Ao testar o código em seu próprio aplicativo de exemplo, troque o namespace pelo namespace do aplicativo de exemplo.

Crie uma interface ITab que as guias implementem em uma 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, confira Componentes Razordo ASP.NET Core.

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

Os componentes Tab filho não são explicitamente passados como parâmetros para TabSet. Em vez disso, os componentes Tab filho fazem parte do conteúdo filho de TabSet. No entanto, o TabSet ainda precisa de uma referência de cada componente Tab para que ele possa renderizar os cabeçalhos e a guia ativa. Para habilitar essa coordenação sem exigir código adicional, o componente TabSetpode fornecer-se como um valor em cascata que, em seguida, é captado pelos componentes descendentes 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();
        }
    }
}

Os componentes Tab descendentes capturam o TabSet presente como um parâmetro em cascata. Os componentes Tab se adicionam à TabSet e se coordenam e 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