Compartir vía


Representación de componentes de Razor de ASP.NET Core

Nota

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

Advertencia

Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulta la Directiva de soporte técnico de .NET y .NET Core. Para la versión actual, consulta 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, consulta la versión .NET 8 de este artículo.

En este artículo se explica la representación del componente Razor en aplicaciones ASP.NET Core Blazor, incluido cuándo llamar a StateHasChanged para desencadenar manualmente un componente para representarlo.

Convenciones de representación para ComponentBase

Los componentes deben representarse la primera vez que se agregan a la jerarquía de componentes por parte de su componente primario. Esta es la única vez que debe representarse un componente. Los componentes pueden representarse en otras ocasiones según su lógica y sus convenciones propias.

Los componentes de Razor se heredan de la clase base ComponentBase, que contiene la lógica para desencadenar la nueva representación en las siguientes ocasiones:

Los componentes heredados de ComponentBase omiten las nuevas representaciones debido a las actualizaciones de parámetros si se cumple alguna de las condiciones siguientes:

Control del flujo de representación

En la mayoría de los casos, las convenciones de ComponentBase dan como resultado el subconjunto correcto de nuevas representaciones de componentes después de que se produzca un evento. Normalmente, no es necesario que los desarrolladores proporcionen ninguna lógica manual para indicar al marco qué componentes se van a volver a representar y cuándo. El efecto general de las convenciones del marco es que el componente que recibe un evento se vuelva a representar a sí mismo, lo que desencadena de forma recursiva la nueva representación de los componentes descendientes cuyos valores de parámetro puedan haber cambiado.

Para más información sobre las implicaciones de rendimiento de las convenciones del marco y cómo optimizar la jerarquía de componentes de una aplicación para la representación, vea Procedimientos recomendados de rendimiento de Blazor en ASP.NET Core.

Representación de streaming

Use representación de streaming con representación estática del lado servidor (SSR estático) o representación previa para transmitir actualizaciones de contenido en el flujo de respuesta y mejorar la experiencia del usuario para los componentes que realizan tareas asincrónicas de larga duración para representarse por completo.

Por ejemplo, considere un componente que realiza una consulta de base de datos de larga duración o una llamada API web para representar los datos cuando se carga la página. Normalmente, las tareas asincrónicas ejecutadas como parte de la representación de un componente del lado servidor deben completarse antes de enviar la respuesta representada, lo que puede retrasar la carga de la página. Cualquier retraso significativo en la representación de la página daña la experiencia del usuario. Para mejorar la experiencia del usuario, la representación de streaming empieza representando rápidamente toda la página con contenido de marcador de posición mientras se ejecutan las operaciones asincrónicas. Una vez completadas las operaciones, el contenido actualizado se envía al cliente en la misma conexión de respuesta y se revisa en el DOM.

La representación de streaming requiere que el servidor evite almacenar en búfer la salida. Los datos de respuesta deben fluir al cliente a medida que se generan los datos. En el caso de los hosts que aplican el almacenamiento en búfer, la representación de streaming se degrada correctamente y la página se carga sin representación de streaming.

Para transmitir actualizaciones de contenido al usar la representación estática del lado servidor (SSR estático) o la representación previa, aplique el [StreamRendering(true)] atributo al componente. La representación de streaming debe estar habilitada explícitamente porque las actualizaciones transmitidas pueden provocar que el contenido de la página cambie. Los componentes sin el atributo adoptan automáticamente la representación de streaming si el componente primario usa la característica. Pasa false al atributo de un componente secundario para deshabilitar la característica en ese momento y más abajo en el subárbol del componente. El atributo es funcional cuando se aplica a los componentes proporcionados por una Razor biblioteca de clases.

El ejemplo siguiente se basa en el componente Weather de una aplicación creada a partir de la plantilla de proyecto Blazor Web App. La llamada para Task.Delay simula la recuperación de datos meteorológicos de forma asincrónica. El componente representa inicialmente el contenido del marcador de posición ("Loading...") sin esperar a que se complete el retraso asincrónico. Cuando se completa el retraso asincrónico y se genera el contenido de los datos meteorológicos, el contenido se transmite a la respuesta y se revisa en la tabla de previsión meteorológica.

Weather.razor:

@page "/weather"
@attribute [StreamRendering(true)]

...

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        ...
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    ...

    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(500);

        ...

        forecasts = ...
    }
}

Supresión de la actualización de la interfaz de usuario (ShouldRender)

Se llama a ShouldRender cada vez que se representa un componente. Invalida ShouldRender para administrar la actualización de la interfaz de usuario. Si la implementación devuelve true, la interfaz de usuario se actualiza.

Aunque se invalide ShouldRender, el componente siempre se representa inicialmente.

ControlRender.razor:

@page "/control-render"

<PageTitle>Control Render</PageTitle>

<h1>Control Render Example</h1>

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

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

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender() => shouldRender;

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

<PageTitle>Control Render</PageTitle>

<h1>Control Render Example</h1>

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

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

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender() => shouldRender;

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

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

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

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

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

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

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

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

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

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

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

Para obtener más información sobre los procedimientos recomendados de rendimiento relacionados con ShouldRender, consulta Procedimientos recomendados de rendimiento de Blazor en ASP.NET Core.

StateHasChanged

Al llamar a StateHasChanged, se pide que se produzca una nueva representación cuando el subproceso principal de la aplicación esté libre.

Los componentes se colocan en cola para la nueva representación y no se vuelven a poner en cola si ya hay una repetición de la nueva representación pendiente. Si un componente llama a StateHasChanged cinco veces en una fila en un bucle, el componente solo se representa una vez. Este comportamiento se codifica en ComponentBase, que comprueba primero si ha puesto en cola una nueva representación antes de poner en cola uno adicional.

Un componente puede representarse varias veces durante el mismo ciclo, que normalmente se produce cuando un componente tiene elementos secundarios que interactúan entre sí:

  • Un componente primario representa varios elementos secundarios.
  • Los componentes secundarios representan y desencadenan una actualización en el elemento primario.
  • Un componente primario vuelve a representar con nuevo estado.

Este diseño permite llamar a StateHasChanged cuando sea necesario sin el riesgo de introducir representación innecesaria. Siempre puedes tomar el control de este comportamiento en componentes individuales implementando IComponent directamente y controlando manualmente cuando el componente se representa.

Ten en cuenta el siguiente método IncrementCount que incrementa un recuento, llama a StateHasChanged e incrementa de nuevo el recuento:

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

Ejecución paso a paso del código del depurador, es posible que pienses que el recuento se actualiza en la interfaz de usuario para la primera ejecución currentCount++ inmediatamente después de llamar a StateHasChanged. Sin embargo, la interfaz de usuario no muestra un recuento actualizado en ese momento debido al procesamiento sincrónico que tiene lugar para la ejecución de este método. No hay ninguna oportunidad para que el representador represente el componente hasta que finalice el controlador de eventos. La interfaz de usuario muestra aumentos para ambas ejecuciones currentCount++ en una sola representación.

Si esperas algo entre las líneas currentCount++, la llamada esperada ofrece al representador una oportunidad de representar. Esto ha llevado a algunos desarrolladores a llamar a Delay con un retraso de milisegundos en sus componentes para permitir que se produzca una representación, pero no se recomienda ralentizar arbitrariamente una aplicación para poner en cola una representación.

El mejor enfoque es esperar a Task.Yield, lo que obliga al componente a procesar el código de forma asincrónica y representar durante el lote actual con una segunda representación en un lote independiente después de que la tarea de producción ejecute la continuación.

Ten en cuenta el siguiente método IncrementCount revisado, que actualiza la interfaz de usuario dos veces porque se realiza la representación puesta en cola por StateHasChanged cuando se produce la tarea con la llamada a Task.Yield:

private async Task IncrementCount()
{
    currentCount++;
    StateHasChanged();
    await Task.Yield();
    currentCount++;
}

Pero debes evitar llamar a StateHasChanged de forma innecesaria, lo que es un error común, ya que impone costes de representación innecesarios. El código no debe llamar a StateHasChanged en los casos siguientes:

  • Se controlan eventos de manera rutinaria, ya sea de forma sincrónica o asincrónica, porque ComponentBase desencadena una representación para la mayoría de los controladores de eventos rutinarios.
  • Se implementa una lógica típica del ciclo de vida, como OnInitialized o OnParametersSetAsync, ya sea de forma sincrónica o asincrónica, porque ComponentBase desencadena una representación para los eventos de ciclo de vida típicos.

Sin embargo, es posible que tenga sentido llamar a StateHasChanged en los casos descritos en las secciones siguientes de este artículo:

Un controlador asincrónico implica varias fases asincrónicas

Debido al modo en el que se definen las tareas en .NET, un receptor de una instancia de Task solo puede observar su finalización final, no los estados asincrónicos intermedios. Por tanto, ComponentBase solo puede desencadenar la nueva representación cuando se devuelve Task por primera vez y cuando se completa Task finalmente. El marco de trabajo no puede saber que tiene que volver a representar un componente en otros puntos intermedios, como cuando IAsyncEnumerable<T> devuelve datos en una serie de valores Tasks intermedios. Si quieres repetir la representación en puntos intermedios, llame a StateHasChanged en esos puntos.

Ten en cuenta el siguiente componente de CounterState1, que actualiza el recuento cuatro veces cada vez que se ejecuta el método IncrementCount:

  • Las representaciones automáticas se producen después del primer y último incremento de currentCount.
  • Las representaciones manuales se desencadenan mediante llamadas a StateHasChanged cuando el marco no desencadena automáticamente nuevas representaciones en puntos de procesamiento intermedios en los que currentCount se incrementa.

CounterState1.razor:

@page "/counter-state-1"

<PageTitle>Counter State 1</PageTitle>

<h1>Counter State Example 1</h1>

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

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<PageTitle>Counter State 1</PageTitle>

<h1>Counter State Example 1</h1>

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

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

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

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

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

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

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

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

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

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}

Recepción de una llamada de algo externo al sistema de control de eventos y representación de Blazor

ComponentBase solo conoce sus propios métodos de ciclo de vida y los eventos desencadenados por Blazor. ComponentBase no conoce otros eventos que se pueden producir en el código. Por ejemplo, los eventos de C# generados por un almacén de datos personalizado son desconocidos para Blazor. Para que estos eventos desencadenen la nueva la representación a fin de mostrar valores actualizados en la interfaz de usuario, llame a StateHasChanged.

Considera el siguiente componente de CounterState2, el cual usa System.Timers.Timer para actualizar un recuento a intervalos regulares y llama a StateHasChanged para actualizar la interfaz de usuario:

  • OnTimerCallback se ejecuta fuera de cualquier flujo de representación o notificación de eventos administrado por Blazor. Por lo tanto, OnTimerCallback debe llamar a StateHasChanged porque Blazor no es consciente de los cambios en currentCount en la devolución de llamada.
  • El componente implementa IDisposable, donde Timer se desecha cuando el marco llama al método Dispose. Para obtener más información, consulta Ciclo de vida de los componentes de ASP.NET Core Razor.

Como la devolución de llamada se invoca fuera del contexto de sincronización de Blazor, el componente debe encapsular la lógica de OnTimerCallback en ComponentBase.InvokeAsync para moverla al contexto de sincronización del representador. Esto equivale a la serialización en el subproceso de interfaz de usuario en otros marcos de interfaz de usuario. Solo se puede llamar a StateHasChanged desde el contexto de sincronización del representador; en caso contrario se inicia una excepción.

System.InvalidOperationException: "El subproceso actual no está asociado a Dispatcher. Usa InvokeAsync() para cambiar la ejecución a Dispatcher al desencadenar la representación o el estado del componente".

CounterState2.razor:

@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<PageTitle>Counter State 2</PageTitle>

<h1>Counter State Example 2</h1>

<p>
    This counter demonstrates <code>Timer</code> disposal.
</p>

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

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<PageTitle>Counter State 2</PageTitle>

<h1>Counter State Example 2</h1>

<p>
    This counter demonstrates <code>Timer</code> disposal.
</p>

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

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

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

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

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

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

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

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

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

@code {
    private int currentCount = 0;
    private Timer timer = new Timer(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}

Para representar el componente fuera del subárbol que se vuelve a representar mediante un evento concreto

La interfaz de usuario puede implicar lo siguiente:

  1. Enviar un evento a un componente.
  2. Cambiar algún estado.
  3. Representar un componente completamente diferente que no sea descendiente del componente que recibe el evento.

Una manera de abordar este escenario consiste en que una clase de administración de estado, a menudo como un servicio de inserción de dependencias (DI), se inserte en varios componentes. Cuando un componente llama a un método en el administrador de estados, este genera un evento de C# que, después, lo recibe un componente independiente.

Para conocer los enfoques para administrar el estado, consulta los siguientes recursos:

Para el enfoque del administrador de estado, los eventos de C# están fuera de la canalización de representación de Blazor. Llama a StateHasChanged en otros componentes que desees volver a representar en respuesta a los eventos del administrador de estado.

El enfoque del administrador de estado es similar al caso previo con System.Timers.Timer en la sección anterior. Como la pila de llamadas de ejecución normalmente permanece en el contexto de sincronización del representador, InvokeAsync no suele ser necesario. Llamar a InvokeAsync solo es necesario si la lógica escapa al contexto de sincronización, como al llamar a ContinueWith en Task o esperar por Task con ConfigureAwait(false). Para obtener más información consulta la sección de Recepción de una llamada de algo externo al sistema de control de eventos y representación de Blazor.

Indicador de progreso de carga de WebAssembly para Blazor Web App

Un indicador de progreso de carga no está presente en una aplicación creada a partir de la plantilla de proyecto Blazor Web App. Se planea una nueva característica de indicador de progreso de carga para una versión futura de .NET. Mientras tanto, una aplicación puede adoptar código personalizado para crear un indicador de progreso de carga. Para obtener más información, consulta Inicio de Blazor de ASP.NET Core.