Ciclo de vida de los componentes de ASP.NET Core Razor

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 el ciclo de vida de los componentes de ASP.NET Core Razor y cómo usar los eventos de ciclo de vida.

Eventos de ciclo de vida

El componente Razor procesa los eventos de ciclo de vida de los componentes Razor en un conjunto de métodos de ciclo de vida sincrónicos y asincrónicos. Los métodos de ciclo de vida se pueden invalidar para realizar operaciones adicionales en los componentes durante la inicialización y representación de estos.

En este artículo se simplifica el procesamiento de eventos del ciclo de vida de los componentes para aclarar la lógica compleja del marco y no se cubren todos los cambios realizados durante los años. Es posible que tenga que acceder al origen de referencia de ComponentBase para integrar el procesamiento de eventos personalizados con el procesamiento de eventos del ciclo de vida de Blazor. Los comentarios de código del origen de referencia incluyen comentarios adicionales sobre el procesamiento de eventos del ciclo de vida que no aparecen en este artículo ni en la documentación de la API.

Nota:

Los vínculos de la documentación al origen de referencia de .NET cargan normalmente la rama predeterminada del repositorio, que representa el desarrollo actual para la próxima versión de .NET. Para seleccionar una etiqueta de una versión específica, use la lista desplegable Cambiar ramas o etiquetas. Para obtener más información, vea Procedimientos para seleccionar una etiqueta de versión de código fuente de ASP.NET Core (dotnet/AspNetCore.Docs #26205).

En los siguientes diagramas simplificados se ilustra el procesamiento de eventos de ciclo de vida de los componentes Razor. Los métodos de C# asociados a los eventos de ciclo de vida se definen con ejemplos en las siguientes secciones de este artículo.

Eventos del ciclo de vida del componente:

  1. Si el componente se representa por primera vez en una solicitud:
    • Cree la instancia del componente.
    • Realice la inserción de propiedades.
    • Llame a OnInitialized{Async}. Si se devuelve un elemento Task incompleto, Task se espera y, luego, el componente se vuelve a representar. Se llama al método sincrónico antes del método asincrónico.
  2. Llame a OnParametersSet{Async}. Si se devuelve un elemento Task incompleto, Task se espera y, luego, el componente se vuelve a representar. Se llama al método sincrónico antes del método asincrónico.
  3. Realice la representación de todo el trabajo sincrónico y complete la clase Task.

Nota

Es posible que las acciones asincrónicas realizadas en eventos de ciclo de vida no se hayan completado antes de que se represente un componente. Para más información, consulte la sección Control de acciones asincrónicas incompletas en la representación más adelante en este artículo.

Un componente primario se representa antes que sus componentes secundarios porque la representación es lo que determina qué elementos secundarios están presentes. Si se usa la inicialización de componentes primarios sincrónicos, se garantiza que la inicialización primaria se complete primero. Si se usa la inicialización asincrónica de componentes primarios, no se puede determinar el orden de finalización de la inicialización de componentes primarios y secundarios porque depende del código de inicialización en ejecución.

Eventos de ciclo de vida de componentes de un componente Razor en Blazor

Procesamiento de eventos DOM:

  1. Se ejecuta el controlador de eventos.
  2. Si se devuelve un elemento Task incompleto, Task se espera y, luego, el componente se vuelve a representar.
  3. Realice la representación de todo el trabajo sincrónico y complete la clase Task.

Procesamiento de eventos DOM

Ciclo de vida de Render:

  1. Evite más operaciones de representación en el componente cuando se cumplan las dos condiciones siguientes:
    • No es la primera representación.
    • ShouldRender devuelve false.
  2. Compile la diff (comparación) del árbol de representación y represente el componente.
  3. Espere a que se actualice DOM.
  4. Llame a OnAfterRender{Async}. Se llama al método sincrónico antes del método asincrónico.

Ciclo de vida de representación

Las llamadas del desarrollador a StateHasChanged producen una representación. Para más información, consulte Representación de componentes de Razor de ASP.NET Core.

Cuando los parámetros están establecidos (SetParametersAsync)

SetParametersAsync establece los parámetros que proporciona el elemento primario del componente en el árbol de representación o a partir de los parámetros de ruta.

El parámetro ParameterView del método contiene el conjunto de valores de parámetros de componentes del componente cada vez que se llama a SetParametersAsync. Al invalidar el método SetParametersAsync, el código del desarrollador puede interactuar directamente con los parámetros de ParameterView.

La implementación predeterminada de SetParametersAsync establece el valor de cada propiedad con el atributo [Parameter] o [CascadingParameter], que tiene un valor correspondiente en ParameterView. Los parámetros que no tienen un valor correspondiente en ParameterView se dejan sin cambios.

Si ComponentBase.SetParametersAsync no se invoca con base.SetParametersAsync();, el código del desarrollador puede interpretar el valor de los parámetros entrantes de cualquier manera que sea necesaria. Por ejemplo, no hay ningún requisito para asignar los parámetros entrantes a las propiedades de la clase.

Si decide no llamar al método de clase base, debe llamar a sus propios métodos de inicialización de componentes (OnInitialized/OnInitializedAsync). De lo contrario, no se llamarán porque llamar a ComponentBase.SetParametersAsync es lo que los invoca. StateHasChanged también se debe llamar después de la inicialización. Consulte el ComponentBase origen de referencia de al estructurar el código si no planea llamar a ComponentBase.SetParametersAsync.

Nota:

Los vínculos de la documentación al origen de referencia de .NET cargan normalmente la rama predeterminada del repositorio, que representa el desarrollo actual para la próxima versión de .NET. Para seleccionar una etiqueta de una versión específica, use la lista desplegable Cambiar ramas o etiquetas. Para obtener más información, vea Procedimientos para seleccionar una etiqueta de versión de código fuente de ASP.NET Core (dotnet/AspNetCore.Docs #26205).

Si quiere confiar en la API de inicialización y representación de ComponentBase.SetParametersAsync pero no procesar parámetros entrantes, tiene la opción de pasar un ParameterView vacío al método de clase base:

base.SetParametersAsync(ParameterView.Empty);

Si los controladores de eventos se proporcionan en el código del desarrollador, desenlácelos para su eliminación. Para obtener más información, vea la sección Eliminación de componentes con IDisposableIAsyncDisposable.

En el ejemplo siguiente, ParameterView.TryGetValue asigna el valor del parámetro Param a value si el análisis de un parámetro de ruta para Param se realiza correctamente. Cuando value no es null, el componente muestra el valor.

Aunque la coincidencia de parámetros de ruta no distingue entre mayúsculas y minúsculas, TryGetValue solo coincide con los nombres de parámetro que sí lo hacen en la plantilla de ruta. En el ejemplo siguiente se requiere el uso de /{Param?} en la plantilla de ruta para obtener el valor con TryGetValue, y no /{param?}. Si se usa /{param?} en este escenario, TryGetValue devuelve false y message no se establece en ninguna cadena message.

SetParamsAsync.razor;

@page "/set-params-async/{Param?}"

<PageTitle>Set Parameters Async</PageTitle>

<h1>Set Parameters Async Example</h1>

<p>@message</p>

@code {
    private string message = "Not set";

    [Parameter]
    public string? Param { get; set; }

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (parameters.TryGetValue<string>(nameof(Param), out var value))
        {
            if (value is null)
            {
                message = "The value of 'Param' is null.";
            }
            else
            {
                message = $"The value of 'Param' is {value}.";
            }
        }

        await base.SetParametersAsync(parameters);
    }
}
@page "/set-params-async/{Param?}"

<p>@message</p>

@code {
    private string message = "Not set";

    [Parameter]
    public string? Param { get; set; }

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (parameters.TryGetValue<string>(nameof(Param), out var value))
        {
            if (value is null)
            {
                message = "The value of 'Param' is null.";
            }
            else
            {
                message = $"The value of 'Param' is {value}.";
            }
        }

        await base.SetParametersAsync(parameters);
    }
}
@page "/set-params-async/{Param?}"

<p>@message</p>

@code {
    private string message = "Not set";

    [Parameter]
    public string? Param { get; set; }

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (parameters.TryGetValue<string>(nameof(Param), out var value))
        {
            if (value is null)
            {
                message = "The value of 'Param' is null.";
            }
            else
            {
                message = $"The value of 'Param' is {value}.";
            }
        }

        await base.SetParametersAsync(parameters);
    }
}
@page "/set-params-async/{Param?}"

<p>@message</p>

@code {
    private string message = "Not set";

    [Parameter]
    public string Param { get; set; }

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (parameters.TryGetValue<string>(nameof(Param), out var value))
        {
            if (value is null)
            {
                message = "The value of 'Param' is null.";
            }
            else
            {
                message = $"The value of 'Param' is {value}.";
            }
        }

        await base.SetParametersAsync(parameters);
    }
}
@page "/set-params-async"
@page "/set-params-async/{Param}"

<p>@message</p>

@code {
    private string message = "Not set";

    [Parameter]
    public string Param { get; set; }

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (parameters.TryGetValue<string>(nameof(Param), out var value))
        {
            if (value is null)
            {
                message = "The value of 'Param' is null.";
            }
            else
            {
                message = $"The value of 'Param' is {value}.";
            }
        }

        await base.SetParametersAsync(parameters);
    }
}

Inicialización de componentes (OnInitialized{Async})

OnInitialized y OnInitializedAsync se usan exclusivamente para inicializar un componente durante toda la duración de la instancia del componente. Los valores de parámetro y los cambios en estos no deben afectar a la inicialización realizada en estos métodos. Por ejemplo, la carga de opciones estáticas en una lista desplegable que no cambia durante la vigencia del componente y que no depende de los valores de parámetro se realiza en uno de estos métodos de ciclo de vida. Si los valores de parámetro o los cambios en los valores de parámetro afectan al estado del componente, use OnParametersSet{Async} en su lugar.

Estos métodos se invocan cuando se inicializa el componente después de haber recibido sus parámetros iniciales en SetParametersAsync. Se llama al método sincrónico antes del método asincrónico.

Si se usa la inicialización de componentes primarios sincrónicos, se garantiza que la inicialización primaria se complete antes que la de los componentes secundarios. Si se usa la inicialización asincrónica de componentes primarios, no se puede determinar el orden de finalización de la inicialización de componentes primarios y secundarios porque depende del código de inicialización en ejecución.

En el caso de una operación sincrónica, invalide OnInitialized:

OnInit.razor;

@page "/on-init"

<PageTitle>On Initialized</PageTitle>

<h1>On Initialized Example</h1>

<p>@message</p>

@code {
    private string? message;

    protected override void OnInitialized()
    {
        message = $"Initialized at {DateTime.Now}";
    }
}
@page "/on-init"

<p>@message</p>

@code {
    private string? message;

    protected override void OnInitialized()
    {
        message = $"Initialized at {DateTime.Now}";
    }
}
@page "/on-init"

<p>@message</p>

@code {
    private string? message;

    protected override void OnInitialized()
    {
        message = $"Initialized at {DateTime.Now}";
    }
}
@page "/on-init"

<p>@message</p>

@code {
    private string message;

    protected override void OnInitialized()
    {
        message = $"Initialized at {DateTime.Now}";
    }
}
@page "/on-init"

<p>@message</p>

@code {
    private string message;

    protected override void OnInitialized()
    {
        message = $"Initialized at {DateTime.Now}";
    }
}

Para realizar una operación asincrónica, invalide OnInitializedAsync y use el operador await:

protected override async Task OnInitializedAsync()
{
    await ...
}

Si se usa una clase base personalizada con lógica de inicialización personalizada, llame a OnInitializedAsync en la clase base:

protected override async Task OnInitializedAsync()
{
    await ...

    await base.OnInitializedAsync();
}

No es necesario llamar a ComponentBase.OnInitializedAsync a menos que se use una clase base personalizada con lógica personalizada. Para obtener más información, vea la sección Métodos de ciclo de vida de clases base.

Las aplicaciones de Blazor que representan previamente su contenido en el servidor llaman a OnInitializedAsyncdos veces:

  • Una primera vez cuando el componente se representa inicialmente de forma estática como parte de la página.
  • Una segunda vez cuando el explorador representa el componente.

Para evitar que el código de desarrollador en OnInitializedAsync se ejecute dos veces al realizar la representación previa, vea la sección Reconexión con estado después de la representación previa. El contenido de la sección se centra en Web Apps Blazor y en la SignalRreconexión con estado. Para conservar el estado durante la ejecución del código de inicialización durante la representación previa, vea Representación previa de componentes Razor de ASP.NET Core.

Para evitar que el código de desarrollador en OnInitializedAsync se ejecute dos veces al realizar la representación previa, vea la sección Reconexión con estado después de la representación previa. Aunque el contenido de la sección se centra en Blazor Server y en la reconexiónSignalRcon estado, el escenario para la representación previa en las soluciones Blazor WebAssembly hospedadas (WebAssemblyPrerendered) conlleva condiciones y enfoques similares para impedir que el código del desarrollador se ejecute dos veces. Para conservar el estado durante la ejecución del código de inicialización durante la representación previa, vea Integración y representación previa de componentes Razor de ASP.NET Core.

Durante la representación previa de una aplicación Blazor, no es posible realizar ciertas acciones, como llamar a JavaScript (interoperabilidad de JS). Es posible que los componentes tengan que representarse de forma diferente cuando se representen previamente. Para obtener más información, consulte la sección Representación previa con interoperabilidad de JavaScript.

Si los controladores de eventos se proporcionan en el código del desarrollador, desenlácelos para su eliminación. Para obtener más información, vea la sección Eliminación de componentes con IDisposableIAsyncDisposable.

Use representación de streaming con la representación estática del lado servidor (SSR estático) o la representación previa para mejorar la experiencia del usuario para los componentes que realizan tareas asincrónicas de larga duración en OnInitializedAsync para una representación completa. Para obtener más información, consulte Representación de componentes de Razor de ASP.NET Core.

Después de establecer los parámetros (OnParametersSet{Async})

Se llama a OnParametersSet o OnParametersSetAsync:

  • Después de inicializar el componente en OnInitialized o OnInitializedAsync.

  • Cuando el componente primario vuelve a representarse y proporciona lo siguiente:

    • Tipos inmutables primitivos conocidos cuando un parámetro por lo menos ha cambiado.
    • Parámetros de tipo complejo. El marco no puede saber si los valores de un parámetro de tipo complejo se han mutado internamente, por lo que siempre trata el conjunto de parámetros como modificado cuando uno o más parámetros de tipo complejo están presentes.

    Para más información, consulte Representación de componentes de Razor de ASP.NET Core.

Se llama al método sincrónico antes del método asincrónico.

Los métodos se pueden invocar incluso aunque los valores de parámetro no hayan cambiado. Este comportamiento subraya la necesidad de que los desarrolladores implementen lógica adicional dentro de los métodos para comprobar si los valores de parámetro han cambiado antes de volver a inicializar los datos o el estado dependientes de esos parámetros.

En el siguiente componente de ejemplo, vaya a la página del componente con una dirección URL:

  • Con una fecha de inicio recibida por StartDate: /on-parameters-set/2021-03-19
  • Sin una fecha de inicio, donde a StartDate se le asigna un valor de la hora local actual: /on-parameters-set

Nota:

En una ruta de componente, no es posible restringir un parámetro DateTime con datetime de restricción de ruta y hacer que el parámetro sea opcional. Por lo tanto, el siguiente componente OnParamsSet utiliza dos directivas @page para controlar el enrutamiento con y sin un segmento de fecha proporcionado en la dirección URL.

OnParamsSet.razor;

@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<PageTitle>On Parameters Set</PageTitle>

<h1>On Parameters Set Example</h1>

<p>
    Pass a datetime in the URI of the browser's address bar. 
    For example, add <code>/1-1-2024</code> to the address.
</p>

<p>@message</p>

@code {
    private string? message;

    [Parameter]
    public DateTime StartDate { get; set; }

    protected override void OnParametersSet()
    {
        if (StartDate == default)
        {
            StartDate = DateTime.Now;

            message = $"No start date in URL. Default value applied " +
                $"(StartDate: {StartDate}).";
        }
        else
        {
            message = $"The start date in the URL was used " +
                $"(StartDate: {StartDate}).";
        }
    }
}
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<p>@message</p>

@code {
    private string? message;

    [Parameter]
    public DateTime StartDate { get; set; }

    protected override void OnParametersSet()
    {
        if (StartDate == default)
        {
            StartDate = DateTime.Now;

            message = $"No start date in URL. Default value applied (StartDate: {StartDate}).";
        }
        else
        {
            message = $"The start date in the URL was used (StartDate: {StartDate}).";
        }
    }
}
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<p>@message</p>

@code {
    private string? message;

    [Parameter]
    public DateTime StartDate { get; set; }

    protected override void OnParametersSet()
    {
        if (StartDate == default)
        {
            StartDate = DateTime.Now;

            message = $"No start date in URL. Default value applied (StartDate: {StartDate}).";
        }
        else
        {
            message = $"The start date in the URL was used (StartDate: {StartDate}).";
        }
    }
}
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<p>@message</p>

@code {
    private string message;

    [Parameter]
    public DateTime StartDate { get; set; }

    protected override void OnParametersSet()
    {
        if (StartDate == default)
        {
            StartDate = DateTime.Now;

            message = $"No start date in URL. Default value applied (StartDate: {StartDate}).";
        }
        else
        {
            message = $"The start date in the URL was used (StartDate: {StartDate}).";
        }
    }
}
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<p>@message</p>

@code {
    private string message;

    [Parameter]
    public DateTime StartDate { get; set; }

    protected override void OnParametersSet()
    {
        if (StartDate == default)
        {
            StartDate = DateTime.Now;

            message = $"No start date in URL. Default value applied (StartDate: {StartDate}).";
        }
        else
        {
            message = $"The start date in the URL was used (StartDate: {StartDate}).";
        }
    }
}

El trabajo asincrónico al aplicar parámetros y valores de propiedad debe producirse durante el evento de ciclo de vida de OnParametersSetAsync:

protected override async Task OnParametersSetAsync()
{
    await ...
}

Si se usa una clase base personalizada con lógica de inicialización personalizada, llame a OnParametersSetAsync en la clase base:

protected override async Task OnParametersSetAsync()
{
    await ...

    await base.OnParametersSetAsync();
}

No es necesario llamar a ComponentBase.OnParametersSetAsync a menos que se use una clase base personalizada con lógica personalizada. Para obtener más información, vea la sección Métodos de ciclo de vida de clases base.

Si los controladores de eventos se proporcionan en el código del desarrollador, desenlácelos para su eliminación. Para obtener más información, vea la sección Eliminación de componentes con IDisposableIAsyncDisposable.

Para obtener más información sobre los parámetros de ruta y las restricciones, vea Enrutamiento y navegación de Blazor de ASP.NET Core.

Para obtener un ejemplo de implementar SetParametersAsync manualmente para mejorar el rendimiento en algunos escenarios, consulte procedimientos recomendados de rendimientoBlazor de ASP.NET Core.

Después de representar el componente (OnAfterRender{Async})

OnAfterRender y OnAfterRenderAsync se invocan después de que un componente se haya representado de forma interactiva y la interfaz de usuario haya terminado de actualizarse (por ejemplo, después de agregar elementos al DOM del explorador). En este momento, se rellenan las referencias a elementos y componentes. Use esta fase para realizar pasos de inicialización adicionales con el contenido representado, como llamadas de interoperabilidad de JS que interactúan con los elementos DOM representados. Se llama al método sincrónico antes del método asincrónico.

Estos métodos no se invocan durante la representación previa o la representación estática del lado servidor (SSR estático) en el servidor porque esos procesos no están conectados a un DOM del explorador activo y ya están completos antes de actualizar el DOM.

Para OnAfterRenderAsync, el componente no vuelve a representar automáticamente después de la finalización de ningún Task devuelto para evitar un bucle de representación infinito.

Se llama a OnAfterRender y OnAfterRenderAsync una vez que un componente haya terminado la representación. En este momento, se rellenan las referencias a elementos y componentes. Use esta fase para realizar pasos de inicialización adicionales con el contenido representado, como llamadas de interoperabilidad de JS que interactúan con los elementos DOM representados. Se llama al método sincrónico antes del método asincrónico.

Estos métodos no se invocan durante la representación previa porque la representación previa no está conectada a un DOM del explorador activo y ya está completa antes de actualizar el DOM.

Para OnAfterRenderAsync, el componente no vuelve a representar automáticamente después de la finalización de ningún Task devuelto para evitar un bucle de representación infinito.

El parámetro firstRender para OnAfterRender y OnAfterRenderAsync:

  • Se establece en true la primera vez que se representa la instancia del componente.
  • Se puede utilizar para garantizar que el trabajo de inicialización solo se realiza una vez.

AfterRender.razor;

@page "/after-render"
@inject ILogger<AfterRender> Logger 

<PageTitle>After Render</PageTitle>

<h1>After Render Example</h1>

<p>
    <button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>

<p>Study logged messages in the console.</p>

@code {
    protected override void OnAfterRender(bool firstRender)
    {
        Logger.LogInformation("OnAfterRender: firstRender = {FirstRender}", firstRender);
    }

    private void HandleClick()
    {
        Logger.LogInformation("HandleClick called");
    }
}
@page "/after-render"
@inject ILogger<AfterRender> Logger

<PageTitle>After Render</PageTitle>

<h1>After Render Example</h1>

<p>
    <button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>

<p>Study logged messages in the console.</p>

@code {
    protected override void OnAfterRender(bool firstRender)
    {
        Logger.LogInformation("OnAfterRender: firstRender = {FirstRender}", firstRender);
    }

    private void HandleClick()
    {
        Logger.LogInformation("HandleClick called");
    }
}
@page "/after-render"
@inject ILogger<AfterRender> Logger 

<PageTitle>After Render</PageTitle>

<h1>After Render Example</h1>

<p>
    <button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>

<p>Study logged messages in the console.</p>

@code {
    protected override void OnAfterRender(bool firstRender)
    {
        Logger.LogInformation("OnAfterRender: firstRender = {FirstRender}", firstRender);
    }

    private void HandleClick()
    {
        Logger.LogInformation("HandleClick called");
    }
}
@page "/after-render"
@using Microsoft.Extensions.Logging
@inject ILogger<AfterRender> Logger 

<h1>After Render Example</h1>

<p>
    <button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>

<p>Study logged messages in the console.</p>

@code {
    protected override void OnAfterRender(bool firstRender)
    {
        Logger.LogInformation("OnAfterRender: firstRender = {FirstRender}", firstRender);
    }

    private void HandleClick()
    {
        Logger.LogInformation("HandleClick called");
    }
}
@page "/after-render"
@using Microsoft.Extensions.Logging
@inject ILogger<AfterRender> Logger

<h1>After Render Example</h1>

<p>
    <button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>

<p>Study logged messages in the console.</p>

@code {
    protected override void OnAfterRender(bool firstRender)
    {
        Logger.LogInformation("OnAfterRender: firstRender = {FirstRender}", firstRender);
    }

    private void HandleClick()
    {
        Logger.LogInformation("HandleClick called");
    }
}

El ejemplo de AfterRender.razor genera la siguiente salida en la consola cuando se carga la página y se selecciona el botón:

OnAfterRender: firstRender = True
HandleClick called
OnAfterRender: firstRender = False

El trabajo asincrónico inmediatamente después de la representación debe producirse durante el evento de ciclo de vida de OnAfterRenderAsync:

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    ...
}

Si se usa una clase base personalizada con lógica de inicialización personalizada, llame a OnAfterRenderAsync en la clase base:

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    ...

    await base.OnAfterRenderAsync(firstRender);
}

No es necesario llamar a ComponentBase.OnAfterRenderAsync a menos que se use una clase base personalizada con lógica personalizada. Para obtener más información, vea la sección Métodos de ciclo de vida de clases base.

Incluso si se devuelve un elemento Task desde OnAfterRenderAsync, el marco no programa un ciclo de representación adicional para el componente una vez que la tarea se completa. Esto se hace para evitar un bucle de representación infinito. Este aspecto varía con respecto a los otros métodos de ciclo de vida, los cuales programan un ciclo de representación adicional una vez se completa la clase Task devuelta.

Durante el proceso de representación previa en el servidor no se llama aOnAfterRender ni a OnAfterRenderAsync. Se llama a estos métodos cuando el componente se representa de forma interactiva una vez finalizada la representación previa. Cuando la aplicación se representa previamente:

  1. El componente se ejecuta en el servidor para generar algo de marcado HTML estático en la respuesta HTTP. Durante esta fase, no se llama a OnAfterRender ni a OnAfterRenderAsync.
  2. Cuando el script Blazor (blazor.{server|webassembly|web}.js) se inicia en el explorador, el componente se reinicia en modo de representación interactiva. Cuando un componente se reinicia, se llama a OnAfterRender y OnAfterRenderAsync, dado que la aplicación ya no está en fase de representación previa.

Si los controladores de eventos se proporcionan en el código del desarrollador, desenlácelos para su eliminación. Para obtener más información, vea la sección Eliminación de componentes con IDisposableIAsyncDisposable.

Métodos de ciclo de vida de clase base

Al invalidar los métodos de ciclo de vida de Blazor, no es necesario llamar a métodos de ciclo de vida de clase base para ComponentBase. Sin embargo, un componente debe llamar a un método de ciclo de vida de clase base invalidado si el método de clase base contiene lógica que se debe ejecutar. Los consumidores de bibliotecas suelen llamar a métodos de ciclo de vida de clase base al heredar una clase base porque las clases base de biblioteca suelen tener lógica de ciclo de vida personalizada para ejecutarse. Si la aplicación usa una clase base de una biblioteca, consulte la documentación de la biblioteca para obtener instrucciones.

En el ejemplo siguiente, base.OnInitialized(); se llama a para asegurarse de que se ejecuta el método OnInitialized de la clase base. Sin la llamada, BlazorRocksBase2.OnInitialized no se ejecuta.

BlazorRocks2.razor;

@page "/blazor-rocks-2"
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<PageTitle>Blazor Rocks!</PageTitle>

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}
@page "/blazor-rocks-2"
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<PageTitle>Blazor Rocks!</PageTitle>

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}
@page "/blazor-rocks-2"
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<PageTitle>Blazor Rocks!</PageTitle>

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}
@page "/blazor-rocks-2"
@using Microsoft.Extensions.Logging
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}
@page "/blazor-rocks-2"
@using Microsoft.Extensions.Logging
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}

BlazorRocksBase2.cs;

using Microsoft.AspNetCore.Components;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";

    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
    }
}
using Microsoft.AspNetCore.Components;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";

    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
    }
}
using Microsoft.AspNetCore.Components;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";

    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
    }
}
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";

    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
    }
}
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";

    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
    }
}

Cambios de estado (StateHasChanged)

StateHasChanged notifica al componente que su estado ha cambiado. Cuando es aplicable, la llamada a StateHasChanged hace que el componente se represente.

A StateHasChanged se le llama automáticamente para métodos EventCallback. Para obtener más información sobre las devoluciones de llamada de eventos, vea Control de eventos de Blazor en ASP.NET Core .

Para más información sobre la representación de componentes y cuándo llamar a StateHasChanged, incluido cuando llamarlo con ComponentBase.InvokeAsync, consulte Representación de componentes de Razor de ASP.NET Core.

Control de acciones asincrónicas incompletas en la representación

Es posible que las acciones asincrónicas realizadas en eventos de ciclo de vida no se hayan completado antes de que se represente el componente. Los objetos podrían ser null o rellenarse de forma incompleta con datos mientras se ejecuta el método de ciclo de vida. Proporcione lógica de representación para confirmar que los objetos se inicializan. Represente los elementos de la interfaz de usuario del marcador de posición (por ejemplo, un mensaje de carga) mientras los objetos sean null.

En el componente siguiente, OnInitializedAsync se invalida para proporcionar de forma asincrónica datos de clasificación de películas (movies). Cuando movies es null, se muestra al usuario un mensaje de carga. Una vez que se completa el elemento Task que devuelve OnInitializedAsync, el componente se representará con el estado actualizado.

<h1>Sci-Fi Movie Ratings</h1>

@if (movies == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <ul>
        @foreach (var movie in movies)
        {
            <li>@movie.Title &mdash; @movie.Rating</li>
        }
    </ul>
}

@code {
    private Movies[]? movies;

    protected override async Task OnInitializedAsync()
    {
        movies = await GetMovieRatings(DateTime.Now);
    }
}

Control de errores

Para información sobre cómo controlar los errores durante la ejecución del método de ciclo de vida, consulte Control de errores en aplicaciones Blazor de ASP.NET Core.

Reconexión con estado después de la representación previa

Al representarse previamente en el servidor, un componente se representa inicialmente de forma estática como parte de la página. Una vez que el explorador vuelve a establecer una conexión a SignalR con el servidor, el componente se representa otra vez y es interactivo. Si el método de ciclo de vida OnInitialized{Async} para inicializar el componente está presente, el método se ejecuta dos veces:

  • Cuando el componente se representa previamente de forma estática.
  • Después de establecerse la conexión con el servidor.

Esto puede dar como resultado un cambio evidente en los datos mostrados en la interfaz de usuario cuando el componente se representa finalmente. Para evitar este comportamiento, pase un identificador para almacenar en caché el estado durante la representación previa y recuperar el estado después de la representación previa.

En el código siguiente se muestra un objeto WeatherForecastService que evita el cambio en la presentación de datos debido a la representación previa. El esperado Delay (await Task.Delay(...)) simula un breve retraso antes de devolver datos del método GetForecastAsync.

Agregue servicios IMemoryCache con AddMemoryCache en la colección de servicios en el archivo Program de la aplicación:

builder.Services.AddMemoryCache();

WeatherForecastService.cs;

using Microsoft.Extensions.Caching.Memory;

namespace BlazorSample;

public class WeatherForecastService(IMemoryCache memoryCache)
{
    private static readonly string[] summaries =
    [
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    ];

    public IMemoryCache MemoryCache { get; } = memoryCache;

    public Task<WeatherForecast[]?> GetForecastAsync(DateOnly startDate)
    {
        return MemoryCache.GetOrCreateAsync(startDate, async e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromSeconds(30)
            });

            await Task.Delay(TimeSpan.FromSeconds(10));

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = summaries[Random.Shared.Next(summaries.Length)]
            }).ToArray();
        });
    }
}
using Microsoft.Extensions.Caching.Memory;

public class WeatherForecastService
{
    private static readonly string[] summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public WeatherForecastService(IMemoryCache memoryCache)
    {
        MemoryCache = memoryCache;
    }

    public IMemoryCache MemoryCache { get; }

    public Task<WeatherForecast[]?> GetForecastAsync(DateTime startDate)
    {
        return MemoryCache.GetOrCreateAsync(startDate, async e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromSeconds(30)
            });

            await Task.Delay(TimeSpan.FromSeconds(10));

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = summaries[Random.Shared.Next(summaries.Length)]
            }).ToArray();
        });
    }
}
using Microsoft.Extensions.Caching.Memory;

public class WeatherForecastService
{
    private static readonly string[] summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public WeatherForecastService(IMemoryCache memoryCache)
    {
        MemoryCache = memoryCache;
    }

    public IMemoryCache MemoryCache { get; }

    public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
    {
        return MemoryCache.GetOrCreateAsync(startDate, async e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromSeconds(30)
            });

            await Task.Delay(TimeSpan.FromSeconds(10));

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = summaries[Random.Shared.Next(summaries.Length)]
            }).ToArray();
        });
    }
}
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using BlazorSample.Shared;

public class WeatherForecastService
{
    private static readonly string[] summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public WeatherForecastService(IMemoryCache memoryCache)
    {
        MemoryCache = memoryCache;
    }

    public IMemoryCache MemoryCache { get; }

    public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
    {
        return MemoryCache.GetOrCreateAsync(startDate, async e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromSeconds(30)
            });

            var rng = new Random();

            await Task.Delay(TimeSpan.FromSeconds(10));

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = summaries[rng.Next(summaries.Length)]
            }).ToArray();
        });
    }
}
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using BlazorSample.Shared;

public class WeatherForecastService
{
    private static readonly string[] summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public WeatherForecastService(IMemoryCache memoryCache)
    {
        MemoryCache = memoryCache;
    }

    public IMemoryCache MemoryCache { get; }

    public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
    {
        return MemoryCache.GetOrCreateAsync(startDate, async e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromSeconds(30)
            });

            var rng = new Random();

            await Task.Delay(TimeSpan.FromSeconds(10));

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = summaries[rng.Next(summaries.Length)]
            }).ToArray();
        });
    }
}

Para obtener más información sobre RenderMode, vea Guía de BlazorSignalR para ASP.NET Core.

El contenido de esta sección se centra en Web Apps Blazor y en la SignalRreconexión con estado. Para conservar el estado durante la ejecución del código de inicialización durante la representación previa, vea Representación previa de componentes Razor de ASP.NET Core.

Aunque el contenido de la sección se centra en Blazor Server y en la reconexiónSignalRcon estado, el escenario para la representación previa en las soluciones Blazor WebAssembly hospedadas (WebAssemblyPrerendered) conlleva condiciones y enfoques similares para impedir que el código del desarrollador se ejecute dos veces. Para conservar el estado durante la ejecución del código de inicialización durante la representación previa, vea Integración y representación previa de componentes Razor de ASP.NET Core.

Representación previa con interoperabilidad de JavaScript

Esta sección se dirige a las aplicaciones del lado servidor que ofrecen una representación previa de los componentes de Razor. La representación previa se describe en Representación previa de componentes Razor de ASP.NET Core.

Nota:

La navegación interna para el enrutamiento interactivo en Web Apps Blazor no implica solicitar contenido de página nuevo desde el servidor. Por lo tanto, la representación previa no se produce para las solicitudes de página internas. Si la aplicación adopta el enrutamiento interactivo, realice una recarga de página completa para los ejemplos de componentes que muestran el comportamiento de representación previa. Para más información, consulte Representación previa de componentes Razor de ASP.NET Core .

Esta sección se aplica a las aplicaciones del lado del servidor y a las aplicaciones Blazor WebAssembly alojadas que pre representan componentes Razor. La representación previa se describe en Integración y representación previa de componentes Razor de ASP.NET Core.

Durante la representación previa de una aplicación, no es posible realizar ciertas acciones, como llamar a JavaScript (JS).

En el ejemplo siguiente, se llama a la función setElementText1 con JSRuntimeExtensions.InvokeVoidAsync y no devuelve un valor.

Nota:

Para obtener una guía general sobre la ubicación de JS y nuestras recomendaciones para aplicaciones de producción, consulte Ubicación de JavaScript en aplicaciones Blazor de ASP.NET Core.

<script>
  window.setElementText1 = (element, text) => element.innerText = text;
</script>

Advertencia

En el ejemplo anterior se modifica el DOM directamente solo con fines de demostración. No se recomienda modificar directamente el DOM con JS en la mayoría de los escenarios, ya que JS puede interferir con el seguimiento de cambios de Blazor. Para más información, vea Interoperabilidad de JavaScript en Blazor de ASP.NET Core (interoperabilidad de JS).

Durante el proceso de representación previa en el servidor no se llama al evento de ciclo de vida OnAfterRender{Async}. Invalide el método OnAfterRender{Async} para retrasar las llamadas de interoperabilidad de JS hasta que el componente se represente y sea interactivo en el cliente después de la representación previa.

PrerenderedInterop1.razor;

@page "/prerendered-interop-1"
@using Microsoft.JSInterop
@inject IJSRuntime JS

<PageTitle>Prerendered Interop 1</PageTitle>

<h1>Prerendered Interop Example 1</h1>

<div @ref="divElement">Text during render</div>

@code {
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync(
                "setElementText1", divElement, "Text after render");
        }
    }
}

Nota:

En el ejemplo anterior se contamina el cliente con funciones globales. Para obtener un mejor enfoque en las aplicaciones de producción, consulte Aislamiento de JavaScript en módulos de JavaScript.

Ejemplo:

export setElementText1 = (element, text) => element.innerText = text;

En el componente siguiente se muestra cómo usar la interoperabilidad de JS como parte de la lógica de inicialización de un componente de una manera compatible con la representación previa. El componente muestra que es posible desencadenar una actualización de representación desde dentro de OnAfterRenderAsync. El desarrollador debe tener cuidado y evitar la creación de un bucle infinito en este escenario.

En el ejemplo siguiente, se llama a la función setElementText2 con IJSRuntime.InvokeAsync y devuelve un valor.

Nota:

Para obtener una guía general sobre la ubicación de JS y nuestras recomendaciones para aplicaciones de producción, consulte Ubicación de JavaScript en aplicaciones Blazor de ASP.NET Core.

<script>
  window.setElementText2 = (element, text) => {
    element.innerText = text;
    return text;
  };
</script>

Advertencia

En el ejemplo anterior se modifica el DOM directamente solo con fines de demostración. No se recomienda modificar directamente el DOM con JS en la mayoría de los escenarios, ya que JS puede interferir con el seguimiento de cambios de Blazor. Para más información, vea Interoperabilidad de JavaScript en Blazor de ASP.NET Core (interoperabilidad de JS).

Cuando se llama a JSRuntime.InvokeAsync, ElementReference solo se utiliza en OnAfterRenderAsync y no en ningún otro método de ciclo de vida anterior porque no existe ningún elemento de DOM de HTML hasta después de representar el componente.

Se llama a StateHasChanged para volver a representar el componente con el nuevo estado obtenido de la llamada de interoperabilidad de JS (para obtener más información, vea Representación de componentes de Razor de ASP.NET Core). El código no crea un bucle infinito porque solo se llama a StateHasChanged cuando null es data.

PrerenderedInterop2.razor;

@page "/prerendered-interop-2"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<PageTitle>Prerendered Interop 2</PageTitle>

<h1>Prerendered Interop Example 2</h1>

<p>
    Get value via JS interop call:
    <strong id="val-get-by-interop">@(infoFromJs ?? "No value yet")</strong>
</p>

<p>
    Set value via JS interop call: 
    <strong id="val-set-by-interop" @ref="divElement"></strong>
</p>



@code {
    private string? infoFromJs;
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && infoFromJs == null)
        {
            infoFromJs = await JS.InvokeAsync<string>(
                "setElementText2", divElement, "Hello from interop call!");

            StateHasChanged();
        }
    }
}

Nota:

En el ejemplo anterior se contamina el cliente con funciones globales. Para obtener un mejor enfoque en las aplicaciones de producción, consulte Aislamiento de JavaScript en módulos de JavaScript.

Ejemplo:

export setElementText2 = (element, text) => {
  element.innerText = text;
  return text;
};

Eliminación de componentes con IDisposable y IAsyncDisposable

Si un componente implementa IDisposable, IAsyncDisposable, o ambos, el marco de trabajo invoca la eliminación de recursos cuando el componente se quita de la interfaz de usuario. La eliminación puede realizarse en cualquier momento, incluso durante la inicialización de componentes.

Los componentes no deberían tener que implementar IDisposable y IAsyncDisposable de forma simultánea. Si se implementan ambos, el marco de trabajo solo ejecuta la sobrecarga asincrónica.

El código para desarrolladores debe asegurarse de que las implementaciones de IAsyncDisposable no tarden demasiado tiempo en completarse.

Eliminación de referencias de objetos de interoperabilidad de JavaScript

Los ejemplos de los artículos de interoperabilidad de JavaScript (JS) muestran patrones típicos de eliminación de objetos:

Las referencias de objeto de interoperabilidad JS se implementan como un mapa con clave por un identificador en el lado de la llamada de interoperabilidad JS que crea la referencia. Cuando se inicia la eliminación de objetos desde el lado de .NET o JS, Blazor quita la entrada del mapa y el objeto se puede recopilar como elemento no utilizado siempre que no haya ninguna otra referencia sólida al objeto presente.

Como mínimo, elimine siempre los objetos creados en .NET para evitar la pérdida de memoria administrada de .NET.

Tareas de limpieza del DOM durante la eliminación de componentes

Para más información, vea Interoperabilidad de JavaScript en Blazor de ASP.NET Core (interoperabilidad de JS).

Para obtener instrucciones sobre JSDisconnectedException cuando se desconecta un circuito, consulte ASP.NET Core Blazor interoperabilidad de JavaScript (JS interoperabilidad). Para obtener instrucciones generales de control de errores de interoperabilidad de JavaScript, consulte la sección Interoperabilidad de JavaScript en Control de errores en aplicaciones Blazor de ASP.NET Core.

IDisposable sincrónico

Para las tareas de eliminación sincrónicas, use IDisposable.Dispose.

El siguiente componente:

  • Implementa IDisposable con la directiva @implementsRazor.
  • Elimina obj, que es un tipo que implementa IDisposable.
  • Se realiza una comprobación de valores Null porque obj se crea en un método de ciclo de vida (no se muestra).
@implements IDisposable

...

@code {
    ...

    public void Dispose()
    {
        obj?.Dispose();
    }
}

Si un objeto único requiere la eliminación, se puede usar una expresión lambda para eliminar el objeto cuando se llama a Dispose. El ejemplo siguiente aparece en el artículo Representación de componentes de Razor de ASP.NET Core y muestra el uso de una expresión lambda para la eliminación de un elemento Timer.

TimerDisposal1.razor;

@page "/timer-disposal-1"
@using System.Timers
@implements IDisposable

<PageTitle>Timer Disposal 1</PageTitle>

<h1>Timer Disposal Example 1</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();
}

CounterWithTimerDisposal1.razor;

@page "/counter-with-timer-disposal-1"
@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();
}

CounterWithTimerDisposal1.razor;

@page "/counter-with-timer-disposal-1"
@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();
}

CounterWithTimerDisposal1.razor;

@page "/counter-with-timer-disposal-1"
@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();
}

CounterWithTimerDisposal1.razor;

@page "/counter-with-timer-disposal-1"
@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();
}

Nota:

En el ejemplo anterior, la llamada a StateHasChanged se encapsula mediante una llamada a ComponentBase.InvokeAsync porque la devolución de llamada se invoca fuera del contexto de sincronización de Blazor. Para más información, consulte Representación de componentes de Razor de ASP.NET Core.

Si el objeto se crea en un método de ciclo de vida, como OnInitialized{Async}, compruebe null antes de llamar a Dispose.

TimerDisposal2.razor;

@page "/timer-disposal-2"
@using System.Timers
@implements IDisposable

<PageTitle>Timer Disposal 2</PageTitle>

<h1>Timer Disposal Example 2</h1>

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

@code {
    private int currentCount = 0;
    private Timer? timer;

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

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

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

CounterWithTimerDisposal2.razor;

@page "/counter-with-timer-disposal-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;

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

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

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

CounterWithTimerDisposal2.razor;

@page "/counter-with-timer-disposal-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;

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

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

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

CounterWithTimerDisposal2.razor;

@page "/counter-with-timer-disposal-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;

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

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

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

CounterWithTimerDisposal2.razor;

@page "/counter-with-timer-disposal-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;

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

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

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

Para más información, consulte:

IAsyncDisposable asincrónico

Para las tareas de eliminación asincrónicas, use IAsyncDisposable.DisposeAsync.

El siguiente componente:

  • Implementa IAsyncDisposable con la directiva @implementsRazor.
  • Elimina de obj, que es un tipo no administrado que implementa IAsyncDisposable.
  • Se realiza una comprobación de valores Null porque obj se crea en un método de ciclo de vida (no se muestra).
@implements IAsyncDisposable

...

@code {
    ...

    public async ValueTask DisposeAsync()
    {
        if (obj is not null)
        {
            await obj.DisposeAsync();
        }
    }
}

Para más información, consulte:

Asignación de null a objetos eliminados

Normalmente, no es necesario asignar null a objetos eliminados después de llamar a Dispose/DisposeAsync. Entre los casos excepcionales de asignación de null se incluyen los siguientes:

  • Si el tipo de objeto está mal implementado y no tolera que se repitan las llamadas a Dispose/DisposeAsync, asigne null después de la eliminación para omitir correctamente otras llamadas a Dispose/DisposeAsync.
  • Si un proceso de larga duración sigue manteniendo una referencia a un objeto eliminado, la asignación de null permite al recolector de elementos no utilizados liberar el objeto a pesar del proceso de larga duración que contiene una referencia a él.

Se trata de escenarios inusuales. Para los objetos que se implementan correctamente y se comportan con normalidad, no tiene sentido asignar null a objetos eliminados. En los casos excepcionales en los que se debe asignar null a un objeto, se recomienda documentar el motivo y buscar una solución que evite la necesidad de asignar null.

StateHasChanged

Nota:

No se admite la llamada de StateHasChanged en Dispose y DisposeAsync. StateHasChanged se puede invocar como parte de la desactivación del representador, por lo que no se admite la solicitud de actualizaciones de la interfaz de usuario en ese momento.

Controladores de eventos

Cancele siempre la suscripción de los controladores de eventos de .NET. En los ejemplos de formulario de Blazor siguientes se muestra cómo cancelar la suscripción de un controlador de eventos en el método Dispose:

  • Enfoque de campo privado y expresión lambda

    @implements IDisposable
    
    <EditForm ... EditContext="editContext" ...>
        ...
        <button type="submit" disabled="@formInvalid">Submit</button>
    </EditForm>
    
    @code {
        ...
    
        private EventHandler<FieldChangedEventArgs>? fieldChanged;
    
        protected override void OnInitialized()
        {
            editContext = new(model);
    
            fieldChanged = (_, __) =>
            {
                ...
            };
    
            editContext.OnFieldChanged += fieldChanged;
        }
    
        public void Dispose()
        {
            editContext.OnFieldChanged -= fieldChanged;
        }
    }
    
  • Enfoque de método privado

    @implements IDisposable
    
    <EditForm ... EditContext="editContext" ...>
        ...
        <button type="submit" disabled="@formInvalid">Submit</button>
    </EditForm>
    
    @code {
        ...
    
        protected override void OnInitialized()
        {
            editContext = new(model);
            editContext.OnFieldChanged += HandleFieldChanged;
        }
    
        private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
        {
            ...
        }
    
        public void Dispose()
        {
            editContext.OnFieldChanged -= HandleFieldChanged;
        }
    }
    

Para obtener más información, vea la sección Eliminación de componentes con IDisposable y IAsyncDisposable.

Para obtener más información sobre el componente EditForm y los formularios, consulte la Descripción general de los formularios de ASP.NET CoreBlazor y otros artículos sobre formularios en el nodo Formularios.

Funciones, métodos y expresiones anónimos

Cuando se usan métodos, expresiones o funciones de tipo anónimo, no es necesario implementar IDisposable ni cancelar la suscripción de los delegados. Pero el hecho de no cancelar la suscripción de un delegado es un problema cuando el objeto que expone el evento supera la duración del componente que registra el delegado. Si esto ocurre, se produce una pérdida de memoria porque el delegado registrado mantiene activo el objeto original. Por lo tanto, use solo los siguientes enfoques cuando sepa que el delegado de eventos se elimina rápidamente. En caso de duda sobre la duración de los objetos que requieren eliminación, suscríbase a un método de delegados y elimine correctamente el delegado como se muestra en los ejemplos anteriores.

  • Enfoque de método lambda anónimo (no se requiere eliminación explícita):

    private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
    {
        formInvalid = !editContext.Validate();
        StateHasChanged();
    }
    
    protected override void OnInitialized()
    {
        editContext = new(starship);
        editContext.OnFieldChanged += (s, e) => HandleFieldChanged((editContext)s, e);
    }
    
  • Enfoque de expresión lambda anónima (no se requiere eliminación explícita):

    private ValidationMessageStore? messageStore;
    
    [CascadingParameter]
    private EditContext? CurrentEditContext { get; set; }
    
    protected override void OnInitialized()
    {
        ...
    
        messageStore = new(CurrentEditContext);
    
        CurrentEditContext.OnValidationRequested += (s, e) => messageStore.Clear();
        CurrentEditContext.OnFieldChanged += (s, e) => 
            messageStore.Clear(e.FieldIdentifier);
    }
    

    El ejemplo completo del código anterior con expresiones lambda anónimas aparece en el artículo Validación de formulariosBlazor de ASP.NET Core.

Para obtener más información, vea Limpiar recursos no administrados y los temas siguientes sobre la implementación de los métodos Dispose y DisposeAsync.

Trabajo en segundo plano cancelable

Los componentes suelen llevar a cabo un trabajo en segundo plano de ejecución prolongada, como realizar llamadas de red (HttpClient) e interactuar con bases de datos. Es deseable detener el trabajo en segundo plano para conservar los recursos del sistema en diversas situaciones. Por ejemplo, las operaciones asincrónicas en segundo plano no se detienen automáticamente cuando un usuario navega fuera de un componente.

Entre otras de las razones por las que los elementos de trabajo en segundo plano pueden requerir cancelación se incluyen las siguientes:

  • Una tarea en segundo plano en ejecución se inició con datos de entrada o parámetros de procesamiento incorrectos.
  • El conjunto actual de elementos de trabajo en segundo plano en ejecución debe reemplazarse por un nuevo conjunto de elementos de trabajo.
  • La prioridad de las tareas que se están ejecutando debe cambiarse.
  • La aplicación debe cerrarse para que se vuelva a implementar el servidor.
  • Los recursos del servidor se vuelven limitados, lo que requiere la reprogramación de los elementos de trabajo en segundo plano.

Para implementar un patrón de trabajo en segundo plano cancelable en un componente:

En el ejemplo siguiente:

  • await Task.Delay(5000, cts.Token); representa el trabajo asincrónico en segundo plano de ejecución prolongada.
  • BackgroundResourceMethod representa un método en segundo plano de ejecución prolongada que no debe iniciarse si se elimina Resource antes de llamar al método.

BackgroundWork.razor;

@page "/background-work"
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<PageTitle>Background Work</PageTitle>

<h1>Background Work Example</h1>

<p>
    <button @onclick="LongRunningWork">Trigger long running work</button>
    <button @onclick="Dispose">Trigger Disposal</button>
</p>
<p>Study logged messages in the console.</p>
<p>
    If you trigger disposal within 10 seconds of page load, the 
    <code>BackgroundResourceMethod</code> isn't executed.
</p>
<p>
    If disposal occurs after <code>BackgroundResourceMethod</code> is called but before action
    is taken on the resource, an <code>ObjectDisposedException</code> is thrown by 
    <code>BackgroundResourceMethod</code>, and the resource isn't processed.
</p>

@code {
    private Resource resource = new();
    private CancellationTokenSource cts = new();
    private IList<string> messages = [];

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(10000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");

        if (!cts.IsCancellationRequested)
        {
            cts.Cancel();
        }
        
        cts?.Dispose();
        resource?.Dispose();
    }

    private class Resource : IDisposable
    {
        private bool disposed;

        public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
        {
            logger.LogInformation("BackgroundResourceMethod: Start method");

            if (disposed)
            {
                logger.LogInformation("BackgroundResourceMethod: Disposed");
                throw new ObjectDisposedException(nameof(Resource));
            }

            // Take action on the Resource

            logger.LogInformation("BackgroundResourceMethod: Action on Resource");
        }

        public void Dispose() => disposed = true;
    }
}
@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>

@code {
    private Resource resource = new();
    private CancellationTokenSource cts = new();

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(5000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");
        cts.Cancel();
        cts.Dispose();
        resource?.Dispose();
    }

    private class Resource : IDisposable
    {
        private bool disposed;

        public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
        {
            logger.LogInformation("BackgroundResourceMethod: Start method");

            if (disposed)
            {
                logger.LogInformation("BackgroundResourceMethod: Disposed");
                throw new ObjectDisposedException(nameof(Resource));
            }

            // Take action on the Resource

            logger.LogInformation("BackgroundResourceMethod: Action on Resource");
        }

        public void Dispose()
        {
            disposed = true;
        }
    }
}
@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>

@code {
    private Resource resource = new();
    private CancellationTokenSource cts = new();

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(5000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");
        cts.Cancel();
        cts.Dispose();
        resource?.Dispose();
    }

    private class Resource : IDisposable
    {
        private bool disposed;

        public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
        {
            logger.LogInformation("BackgroundResourceMethod: Start method");

            if (disposed)
            {
                logger.LogInformation("BackgroundResourceMethod: Disposed");
                throw new ObjectDisposedException(nameof(Resource));
            }

            // Take action on the Resource

            logger.LogInformation("BackgroundResourceMethod: Action on Resource");
        }

        public void Dispose()
        {
            disposed = true;
        }
    }
}
@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>

@code {
    private Resource resource = new();
    private CancellationTokenSource cts = new();

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(5000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");
        cts.Cancel();
        cts.Dispose();
        resource?.Dispose();
    }

    private class Resource : IDisposable
    {
        private bool disposed;

        public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
        {
            logger.LogInformation("BackgroundResourceMethod: Start method");

            if (disposed)
            {
                logger.LogInformation("BackgroundResourceMethod: Disposed");
                throw new ObjectDisposedException(nameof(Resource));
            }

            // Take action on the Resource

            logger.LogInformation("BackgroundResourceMethod: Action on Resource");
        }

        public void Dispose()
        {
            disposed = true;
        }
    }
}
@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>

@code {
    private Resource resource = new Resource();
    private CancellationTokenSource cts = new CancellationTokenSource();

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(5000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");
        cts.Cancel();
        cts.Dispose();
        resource?.Dispose();
    }

    private class Resource : IDisposable
    {
        private bool disposed;

        public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
        {
            logger.LogInformation("BackgroundResourceMethod: Start method");

            if (disposed)
            {
                logger.LogInformation("BackgroundResourceMethod: Disposed");
                throw new ObjectDisposedException(nameof(Resource));
            }

            // Take action on the Resource

            logger.LogInformation("BackgroundResourceMethod: Action on Resource");
        }

        public void Dispose()
        {
            disposed = true;
        }
    }
}

Eventos de reconexión de Blazor Server

Los eventos de ciclo de vida de los componentes descritos en este artículo funcionan por separado de controladores de eventos de reconexión del lado servidor. Cuando se pierde la conexión SignalR al cliente, solo se interrumpen las actualizaciones de la interfaz de usuario. Dichas actualizaciones se reanudan cuando se restablece la conexión. Para obtener más información sobre los eventos de controlador de circuito y su configuración, vea Guía de BlazorSignalR para ASP.NET Core.

Recursos adicionales

Control de las excepciones detectadas fuera del ciclo de vida de un componente Razor