Valores y parámetros en cascada de Blazor en ASP.NET Core

Nota

Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión .NET 8 de este artículo.

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulte la versión .NET 8 de este artículo.

Obtenga información sobre cómo transferir datos desde un componente Razor antecesor a componentes descendientes.

Los valores y parámetros en cascada proporcionan una manera de conveniente de hacer fluir los datos por una jerarquía de componentes desde un componente antecesor a cualquier número de componentes descendiente. A diferencia de los parámetros de componente, los valores y parámetros en cascada no requieren una asignación de atributo para cada componente descendiente en el que se consuman los datos. Los valores y parámetros en cascada también permiten que los componentes se coordinen entre sí en una jerarquía de componentes.

Nota:

En los ejemplos de código de este artículo se adoptan tipos de referencia que admiten un valor NULL (NRT) y análisis estático de estado NULL del compilador de .NET, que se admiten en ASP.NET Core en .NET 6 o posterior. Al tener como destino ASP.NET Core 5.0 o versiones anteriores, quite la designación de tipo null (?) de los tipos CascadingType?, @ActiveTab?, RenderFragment?, ITab?, TabSet? y string? de los ejemplos del artículo.

Valores en cascada de nivel raíz

Los valores en cascada de nivel raíz se pueden registrar para toda la jerarquía de componentes. Se admiten los valores en cascada con nombre y las suscripciones para las notificaciones de actualización.

En los ejemplos de esta sección se usa la siguiente clase.

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

Los registros siguientes se realizan en el archivo Program de la aplicación con AddCascadingValue:

  • Dalek con un valor de propiedad para Units se registra como un valor fijo en cascada.
  • Un segundo registro Dalek con un valor de propiedad diferente para Units se denomina "AlphaGroup".
builder.Services.AddCascadingValue(sp => new Dalek { Units = 123 });
builder.Services.AddCascadingValue("AlphaGroup", sp => new Dalek { Units = 456 });

En el componente Daleks siguiente se muestran los valores en cascada.

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

En el ejemplo siguiente, Dalek se registra como un valor en cascada mediante CascadingValueSource<T>, donde <T> es el tipo. La marca isFixed indica si el valor es fijo. Si es false, todos los destinatarios se suscriben para las notificaciones de actualización, que se emiten llamando a NotifyChangedAsync. Las suscripciones crean sobrecarga y reducen el rendimiento, por lo que se establece isFixed en true si el valor no cambia.

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

Advertencia

El registro de un tipo de componente como un valor en cascada de nivel raíz no registra servicios adicionales para el tipo ni permite la activación del servicio en el componente.

Trate los servicios necesarios por separado de los valores en cascada, registrándolos por separado del tipo en cascada.

Evite usar AddCascadingValue para registrar un tipo de componente como un valor en cascada. En su lugar, encapsule el <Router>...</Router> en el componente Routes (Components/Routes.razor) con el componente y adopte la representación global interactiva del lado servidor (SSR interactivo). Para obtener un ejemplo, consulte la CascadingValue sección componente.

Componente de CascadingValue

Un componente antecesor proporciona un valor en cascada mediante el componente CascadingValue del marco Blazor, que ajusta un subárbol de una jerarquía de componentes y proporciona un valor único a todos los componentes de su subárbol.

El siguiente ejemplo demuestra el flujo de información del tema en la jerarquía de componentes para proporcionar una clase de estilo CSS a los botones de los componentes secundarios.

La siguiente clase de C# ThemeInfo especifica la información del tema.

Nota:

En los ejemplos de esta sección, el espacio de nombres de la aplicación es BlazorSample. Cuando experimente con el código de su aplicación de ejemplo, cambie el espacio de nombres de la aplicación por el de la aplicación de ejemplo.

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

El componente de diseño siguiente especifica información de tema (ThemeInfo) como un valor en cascada para todos los componentes que constituyen el cuerpo del diseño de la propiedad Body. Se asigna a ButtonClass un valor de btn-success, que es un estilo de botón de arranque. Cualquier componente descendiente de la jerarquía de componentes puede usar la propiedad ButtonClass a través del valor en cascada 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 Web Apps proporciona enfoques alternativos para los valores en cascada que se aplican de forma más amplia a la aplicación que proporcionarlos a través de un archivo de diseño único:

  • Encapsula el marcado del componente de Routes en un componente de CascadingValue para especificar los datos como un valor en cascada para todos los componentes de la aplicación.

    En el siguiente ejemplo se muestran los datos ThemeInfo en cascada del componente Routes.

    Routes.razor:

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

    Nota:

    Ajustar el componente de la instancia Routes en el componente App (Components/App.razor) con un componente CascadingValueno se admite.

  • Especifique un valor en cascada de nivel raíz como servicio llamando al método de extensión AddCascadingValue en el generador de colecciones de servicios.

    En el siguiente ejemplo se muestran los datos ThemeInfo en cascada del archivoProgram.

    Program.cs

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

Para obtener más información, consulte las siguientes secciones de este artículo:

Atributo [CascadingParameter]

Para usar valores en cascada, los componentes descendientes declaran parámetros en cascada mediante el atributo [CascadingParameter]. Los valores en cascada se enlazan a los parámetros en cascada según el tipo. Más adelante en este artículo, en la sección Valores múltiples en cascada, se describe cómo se ponen en cascada varios valores del mismo tipo.

El componente siguiente enlaza el valor en cascada ThemeInfo a un parámetro en cascada y, opcionalmente, usa el mismo nombre de ThemeInfo. El parámetro se usa para establecer la clase CSS para el botón 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 forma similar a un parámetro de componente normal, los componentes que aceptan un parámetro en cascada se reprocesan cuando se cambia el valor en cascada. Por ejemplo, la configuración de una instancia de tema diferente hace que se muestre el componente ThemedCounter de la sección CascadingValue componente.

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 se puede usar para indicar que un parámetro en cascada no cambia después de la inicialización.

Valores o parámetros en cascada y límites de modo de representación

Los parámetros en cascada no pasan datos a través de los límites del modo de representación:

  • Las sesiones interactivas se ejecutan en un contexto diferente a las páginas que usan la representación estática del lado servidor (SSR estático). No es necesario que el servidor que produce la página sea incluso la misma máquina que hospeda una sesión de servidor interactivo posterior, incluidos los componentes de WebAssembly donde el servidor es una máquina diferente al cliente. La ventaja de la representación de servidor estático es obtener el rendimiento completo de la representación HTML sin estado pura.

  • El estado que cruza el límite entre la representación estática e interactiva debe ser serializable. Los componentes son objetos arbitrarios que hacen referencia a una gran cadena de otros objetos, como el representador, el contenedor de di y todas las instancias del servicio de inserción de dependencias. Debe hacer que el estado se serialice explícitamente desde la representación del servidor estático para que esté disponible en componentes representados interactivamente posteriores. Se adoptan dos enfoques:

    • A través del marco de Blazor, los parámetros pasados a través de una representación de servidor estático al límite interactivo se serializan automáticamente si son JSserializables en ON o se genera un error.
    • El estado almacenado en PersistentComponentState se serializa y se recupera automáticamente si es JSserializable en ON o se genera un error.

Los parámetros en cascada no son JSserializables en ON porque los patrones de uso típicos de los parámetros en cascada son algo así como los servicios DI. A menudo hay variantes específicas de la plataforma de parámetros en cascada, por lo que sería útil para los desarrolladores si el marco de trabajo detuvo a los desarrolladores de tener versiones específicas de servidor interactivas o versiones específicas de WebAssembly. Además, muchos valores de parámetro en cascada en general no son serializables, por lo que sería poco práctico actualizar las aplicaciones existentes si tuviera que dejar de usar todos los valores de parámetros en cascada no serializables.

Recomendaciones:

  • Si necesita poner el estado a disposición de todos los componentes interactivos como parámetro en cascada, se recomienda usar valores en cascada de nivel raíz. Hay disponible un patrón de fábrica y la aplicación puede emitir valores actualizados después del inicio de la aplicación. Los valores en cascada de nivel raíz están disponibles para todos los componentes, incluidos los componentes interactivos, ya que se procesan como servicios de inserción de dependencias.

  • En el caso de los autores de bibliotecas de componentes, puede crear un método de extensión para los consumidores de bibliotecas similares a los siguientes:

    builder.Services.AddLibraryCascadingParameters();
    

    Indique a los desarrolladores que llamen al método de extensión. Se trata de una alternativa de sonido para indicarles que agreguen un componente de <RootComponent> en su componente de MainLayout.

Valores múltiples en cascada

Para poner en cascada varios valores del mismo tipo en un mismo subárbol, proporcione una cadena Name única en cada componente CascadingValue y sus correspondientes atributos [CascadingParameter].

En el siguiente ejemplo, dos componentes CascadingValue ponen en cascada instancias distintas de CascadingType:

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

@code {
    private CascadingType? parentCascadeParameter1;

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

En un componente descendiente, los parámetros en cascada reciben sus valores en cascada del componente antecesor en función de Name:

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

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

Paso de datos en una jerarquía de componentes

Los parámetros en cascada también permiten que los componentes pasen datos en jerarquía de componentes. Considere el siguiente ejemplo de conjunto de pestañas de interfaz de usuario, donde un componente del conjunto de pestañas mantiene una serie de pestañas individuales.

Nota:

En los ejemplos de esta sección, el espacio de nombres de la aplicación es BlazorSample. Cuando experimente con el código de su aplicación de ejemplo, cambie el espacio de nombres por el de la aplicación de ejemplo.

Cree una interfaz ITab que las pestañas implementen en una carpeta denominada UIInterfaces.

UIInterfaces/ITab.cs:

using Microsoft.AspNetCore.Components;

namespace BlazorSample.UIInterfaces;

public interface ITab
{
    RenderFragment ChildContent { get; }
}

Nota:

Para obtener más información sobre RenderFragment, consulte Componentes de Razor de ASP.NET Core.

El componente TabSet siguiente mantiene un conjunto de pestañas. Los componentes Tab del conjunto de pestañas, que se crean más adelante en esta sección, proporcionan los elementos de lista (<li>...</li>) para la lista (<ul>...</ul>).

Los componentes Tab secundarios no se pasan explícitamente como parámetros a TabSet, sino que forman parte del contenido secundario de TabSet. Pese a ello, TabSet sigue necesitando una referencia a cada componente Tab para poder representar los encabezados y la pestaña activa. Para permitir esta coordinación sin requerir código extra, el componente TabSetpuede proporcionarse a sí mismo como un valor en cascada que, luego, van a seleccionar los componentes Tab descendientes.

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

Los componentes Tab descendientes capturan el elemento TabSet contenedor como parámetro en cascada. Los componentes Tab se agregan a sí mismos al elemento TabSet y se coordinan para establecer la pestaña activa.

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

El componente ExampleTabSet siguiente usa el componente TabSet, que contiene tres 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 adicionales