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 de .NET 9 de este artículo.
Advertencia
Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulta la Directiva de soporte técnico de .NET y .NET Core. Para la versión actual, consulta la versión .NET 8 de este artículo.
Importante
Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.
Para la versión actual, consulte la versión de .NET 9 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 componentesDetails
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; }
}
<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; }
}
}
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 people
no 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
oGuid
.
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.