Conservación de las relaciones de elementos, componentes y modelos en ASP.NET Core Blazor

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.

Este artículo explica cómo usar el atributo de directiva @key para conservar las relaciones de elementos, componentes y modelos al representar cuando los elementos o componentes cambian en consecuencia.

Uso del atributo de directiva @key

Cuando se representa una lista de elementos o componentes, y esos elementos o componentes cambian en consecuencia, Blazor debe decidir cuáles de los elementos o componentes anteriores se pueden conservar y cómo asignarles objetos de modelo. Normalmente, este proceso es automático y suficiente para la representación general, pero a menudo hay casos en los que se requiere controlar el proceso mediante el atributo de directiva @key.

Observe el ejemplo siguiente, donde se muestra un problema de asignación de recopilación que se resuelve mediante @key.

Para los siguientes componentes:

  • El componente Details recibe datos (Data) del componente primario, que se muestra en un elemento <input>. Cualquier elemento <input> determinado que se muestra puede recibir el foco de la página del usuario cuando selecciona uno de los elementos <input>.
  • El componente primario crea una lista de objetos Person para mostrar mediante el componente Details. Cada tres segundos, se agrega una nueva persona a la colección.

Esta demostración le permite:

  • Seleccionar un elemento <input> de entre varios componentes Details representados.
  • Estudiar el comportamiento del foco de la página a medida que crece automáticamente la colección People.

Details.razor:

<input value="@Data" />

@code {
    [Parameter]
    public string? Data { get; set; }
}
<input value="@Data" />

@code {
    [Parameter]
    public string? Data { get; set; }
}
<input value="@Data" />

@code {
    [Parameter]
    public string? Data { get; set; }
}
<input value="@Data" />

@code {
    [Parameter]
    public string Data { get; set; }
}
<input value="@Data" />

@code {
    [Parameter]
    public string Data { get; set; }
}

En el componente primario siguiente, cada iteración de agregar a una persona en OnTimerCallback da lugar a la recompilación de Blazor de toda la colección. El foco de la página permanece en la misma posición de índice de los elementos <input>, por lo que el foco cambia cada vez que se agrega a una persona. Desplazar el foco fuera de lo que seleccionó el usuario no es un comportamiento deseable. Tras demostrar el comportamiento deficiente con el componente siguiente, el atributo de directiva @key se usa para mejorar la experiencia del usuario.

People.razor:

@page "/people"
@using System.Timers
@implements IDisposable

<PageTitle>People</PageTitle>

<h1>People Example</h1>

@foreach (var person in people)
{
    <Details Data="@person.Data" />
}

@code {
    private Timer timer = new Timer(3000);

    public List<Person> people =
        new()
        {
            { new Person { Data = "Person 1" } },
            { new Person { Data = "Person 2" } },
            { new Person { Data = "Person 3" } }
        };

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

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            people.Insert(0,
                new Person
                {
                    Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
                });
            StateHasChanged();
        });
    }

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

    public class Person
    {
        public string? Data { get; set; }
    }
}

PeopleExample.razor:

@page "/people-example"
@using System.Timers
@implements IDisposable

@foreach (var person in people)
{
    <Details Data="person.Data" />
}

@code {
    private Timer timer = new Timer(3000);

    public List<Person> people =
        new()
        {
            { new Person { Data = "Person 1" } },
            { new Person { Data = "Person 2" } },
            { new Person { Data = "Person 3" } }
        };

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

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            people.Insert(0,
                new Person
                {
                    Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
                });
            StateHasChanged();
        });
    }

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

    public class Person
    {
        public string? Data { get; set; }
    }
}

PeopleExample.razor:

@page "/people-example"
@using System.Timers
@implements IDisposable

@foreach (var person in people)
{
    <Details Data="person.Data" />
}

@code {
    private Timer timer = new Timer(3000);

    public List<Person> people =
        new()
        {
            { new Person { Data = "Person 1" } },
            { new Person { Data = "Person 2" } },
            { new Person { Data = "Person 3" } }
        };

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

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            people.Insert(0,
                new Person
                {
                    Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
                });
            StateHasChanged();
        });
    }

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

    public class Person
    {
        public string? Data { get; set; }
    }
}

PeopleExample.razor:

@page "/people-example"
@using System.Timers
@implements IDisposable

@foreach (var person in people)
{
    <Details Data="person.Data" />
}

@code {
    private Timer timer = new Timer(3000);

    public List<Person> people =
        new()
        {
            { new Person { Data = "Person 1" } },
            { new Person { Data = "Person 2" } },
            { new Person { Data = "Person 3" } }
        };

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

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            people.Insert(0,
                new Person
                {
                    Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
                });
            StateHasChanged();
        });
    }

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

    public class Person
    {
        public string Data { get; set; }
    }
}

PeopleExample.razor:

@page "/people-example"
@using System.Timers
@implements IDisposable

@foreach (var person in people)
{
    <Details Data="person.Data" />
}

@code {
    private Timer timer = new Timer(3000);

    public List<Person> people =
        new List<Person>()
        {
            { new Person { Data = "Person 1" } },
            { new Person { Data = "Person 2" } },
            { new Person { Data = "Person 3" } }
        };

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

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            people.Insert(0,
                new Person
                {
                    Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
                });
            StateHasChanged();
        });
    }

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

    public class Person
    {
        public string Data { get; set; }
    }
}

El contenido de la colección people cambia porque se inserten, eliminen o reordenen entradas. La nueva representación puede dar lugar a diferencias de comportamiento visibles. Por ejemplo, cada vez que se inserta una persona en la colección people, se pierde el foco del usuario.

El proceso de asignación de elementos o componentes a una colección se puede controlar con el atributo de directiva @key. El uso de @key garantiza la conservación de elementos o componentes en función del valor de la clave. Si el componente Details del ejemplo anterior se codifica en el elemento person, Blazor omite la nueva representación de componentes Details que no han cambiado.

Para modificar el componente primario para usar el atributo de directiva @key con la colección people, actualice el elemento <Details> a lo siguiente:

<Details @key="person" Data="@person.Data" />

Cuando la colección people cambia, la asociación entre las instancias de Details y las de person se mantiene. Cuando se inserta un elemento Person al principio de la colección, se inserta una nueva instancia Details en la posición correspondiente. Las demás instancias permanecerán inalteradas. Por lo tanto, el foco del usuario no se pierde a medida que se agregan personas a la colección.

Otras actualizaciones de colección muestran el mismo comportamiento cuando se usa el atributo de directiva @key:

  • Si una instancia se elimina de la colección, solo se quitará de la interfaz de usuario la instancia de componente correspondiente. Las demás instancias permanecerán inalteradas.
  • Si las entradas de colección se reordenan, las instancias de componente correspondientes se conservarán y reordenarán en la interfaz de usuario.

Importante

Las claves son locales de cada componente o elemento contenedor. Las claves no se comparan globalmente en todo el documento.

Cuándo debe usarse @key

Normalmente, usar @key tiene sentido cada vez que una lista se represente (por ejemplo, en un bloque foreach) y haya un valor adecuado para definir el elemento @key.

También puede usar @key para conservar un subárbol de elemento o componente cuando un objeto no cambia, como se muestra en los ejemplos siguientes.

Ejemplo 1:

<li @key="person">
    <input value="@person.Data" />
</li>

Ejemplo 2:

<div @key="person">
    @* other HTML elements *@
</div>

Si una instancia person cambia, la directiva de atributo @key fuerza a Blazor a:

  • Descartar los elementos <li> o <div> enteros y sus descendientes.
  • Volver a generar el subárbol dentro de la interfaz de usuario con nuevos elementos y componentes.

Esto es útil para garantizar que no se conserva ningún estado de la interfaz de usuario cuando la colección cambia dentro de un subárbol.

Ámbito de @key

La directiva de atributo @key tiene como ámbito los otros elementos de su elemento primario.

Considere el ejemplo siguiente. Las claves first y second se comparan entre sí en el mismo ámbito del elemento externo <div>:

<div>
    <div @key="first">...</div>
    <div @key="second">...</div>
</div>

En el ejemplo siguiente se muestran las claves first y second en sus propios ámbitos, no relacionadas entre sí y sin que una influya en la otra. Cada ámbito @key solo se aplica a su elemento <div> primario en lugar de en los elementos <div> primarios:

<div>
    <div @key="first">...</div>
</div>
<div>
    <div @key="second">...</div>
</div>

Para el componente Details mostrado anteriormente, los ejemplos siguientes representan datos de person dentro del mismo ámbito @key y muestran casos de uso típicos para @key:

<div>
    @foreach (var person in people)
    {
        <Details @key="person" Data="@person.Data" />
    }
</div>
@foreach (var person in people)
{
    <div @key="person">
        <Details Data="@person.Data" />
    </div>
}
<ol>
    @foreach (var person in people)
    {
        <li @key="person">
            <Details Data="@person.Data" />
        </li>
    }
</ol>

Los ejemplos siguientes solo tienen como ámbito de @key el elemento <div> o <li> que rodea cada instancia del componente Details. Por lo tanto, los datos de person para cada miembro de la colección peopleno tienen clave en cada instancia de person en los componentes Details representados. Al usar @key, evite los patrones siguientes:

@foreach (var person in people)
{
    <div>
        <Details @key="person" Data="@person.Data" />
    </div>
}
<ol>
    @foreach (var person in people)
    {
        <li>
            <Details @key="person" Data="@person.Data" />
        </li>
    }
</ol>

Cuándo no debe usarse @key

Las representaciones con @key repercuten en el rendimiento. El rendimiento no se ve especialmente afectado, pero pese a ello debemos especificar @key únicamente cuando mantener los componentes o elementos suponga un beneficio para la aplicación.

Aun cuando @key no se use, Blazor conserva las instancias de componentes y elemento secundarios lo máximo posible. La única ventaja de utilizar @key es el control sobre cómo se asignan instancias de modelo a las instancias de componente conservadas, en lugar de Blazor, que selecciona la asignación.

Valores que se pueden usar para @key

Por lo general, lo lógico es proporcionar uno de los siguientes valores en @key:

  • Instancias de objeto de modelo. Por ejemplo, la instancia Person (person) se usó en el ejemplo anterior. Esto garantiza la conservación en función de la igualdad de las referencias de objetos.
  • Identificadores únicos. Por ejemplo, los identificadores únicos pueden basarse en valores de clave principal de tipo int, string o Guid.

Asegúrese de que los valores usados en @key no entran en conflicto. Si se detectan valores en conflicto en el mismo elemento primario, Blazor produce una excepción porque no puede asignar de forma determinista elementos o componentes antiguos a nuevos elementos o componentes. Use exclusivamente valores distintos, como instancias de objeto o valores de clave principal.