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, 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.

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.

De forma predeterminada, 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. Pase 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 Blazor plantilla de proyecto aplicación web. 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. Invalide 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()
    {
        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++;
    }
}
@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 más información sobre los procedimientos recomendados de rendimiento relacionados con ShouldRender, vea Procedimientos recomendados de rendimiento de Blazor en ASP.NET Core.

Cuándo llamar a StateHasChanged

Llamar a StateHasChanged permite desencadenar una representación en cualquier momento. Pero debe evitar llamar a StateHasChanged de forma innecesaria, lo que es un error común, ya que impone costos 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 quiere repetir la representación en puntos intermedios, llame a StateHasChanged en esos puntos.

Tenga en cuenta el siguiente componente de CounterState1, que actualiza el recuento cuatro veces cada vez que se ejecuta el método de 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"

<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 representación a fin de mostrar valores actualizados en la interfaz de usuario, llame a StateHasChanged.

Considere 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 más información, consulte 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. Use 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

<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, consulte 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. Llame a StateHasChanged en otros componentes que desee 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 más información consulte 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 Web Apps Blazor

Un indicador de progreso de carga no está presente en una aplicación creada a partir de la plantilla de proyecto aplicación web Blazor. 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, vea Inicio de Blazor de ASP.NET Core.