Manter relações de elemento, componente e modelo no ASP.NET Core Blazor
Observação
Esta não é a versão mais recente deste artigo. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.
Aviso
Esta versão do ASP.NET Core não tem mais suporte. Para obter mais informações, confira .NET e a Política de Suporte do .NET Core. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.
Importante
Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.
Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.
Este artigo explica como usar o atributo de diretiva @key
para reter relacionamentos de elemento, componente e modelo ao renderizar e os elementos ou componentes posteriormente alterados.
Uso do atributo de diretiva @key
Ao renderizar uma lista de elementos ou componentes e os elementos ou componentes serem alterados posteriormente, Blazor precisa decidir qual dos elementos ou componentes anteriores são retidos e como os objetos de modelo devem ser mapeados para eles. Normalmente, esse processo é automático e suficiente para renderização geral, mas geralmente há casos em que o controle do processo usando o atributo de diretiva @key
é necessário.
Considere o exemplo a seguir que demonstra um problema de mapeamento de coleção resolvido usando @key
.
Para os seguintes componentes:
- O componente
Details
recebe dados (Data
) do componente pai, que são exibidos em um elemento<input>
. Qualquer elemento<input>
exibido pode receber o foco da página do usuário ao selecionar um dos elementos<input>
. - O componente cria uma lista de objetos de pessoa para exibição usando o componente
Details
. A cada três segundos, uma nova pessoa é adicionada à coleção.
Essa demonstração permite:
- Selecionar um
<input>
entre vários componentesDetails
renderizados. - Estudar o comportamento do foco da página à medida que a coleção de pessoas cresce automaticamente.
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; }
}
No componente pai a seguir, cada iteração de adicionar uma pessoa em OnTimerCallback
resulta na recompilação por Blazor de toda a coleção. O foco da página permanece na mesma posição de índice dos elementos <input>
, portanto, o foco muda sempre que uma pessoa é adicionada. Desviar o foco do que o usuário selecionou não é um comportamento desejável. Depois de demonstrar o comportamento ruim com o componente a seguir, o atributo de diretiva @key
é usado para aprimorar a experiência do usuário.
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; }
}
}
O conteúdo da coleção people
é alterado com entradas inseridas, excluídas ou reordenadas. A nova renderização pode levar a diferenças de comportamento visíveis. Por exemplo, sempre que uma pessoa é inserida na coleção people
, o foco do usuário é perdido.
O processo de mapeamento de elementos ou componentes para uma coleção pode ser controlado com o atributo de diretiva @key
. Uso de @key
garante a preservação de elementos ou componentes com base no valor da chave. Se o componente Details
no exemplo anterior for inserido no item person
, Blazor ignorará a renderização de componentes Details
que não foram alterados.
Para modificar o componente pai para usar o atributo de diretiva @key
com a coleção people
, atualize o elemento <Details>
para o seguinte:
<Details @key="person" Data="@person.Data" />
Quando a coleção people
é alterada, a associação entre instâncias de Details
e de person
é mantida. Quando um Person
é inserido no início da coleção, uma nova instância de Details
é inserida nessa posição correspondente. Outras instâncias permanecem inalteradas. Assim, o foco do usuário não é perdido à medida que pessoas são adicionadas à coleção.
Outras atualizações de coleção exibem o mesmo comportamento quando o atributo de diretiva @key
é usado:
- Se uma instância for excluída da coleção, somente a instância de componente correspondente será removida da interface do usuário. Outras instâncias permanecem inalteradas.
- Se as entradas da coleção forem reordenadas, as instâncias de componente correspondentes serão preservadas e reordenadas na interface do usuário.
Importante
As chaves são locais para cada elemento ou componente de contêiner. Elas não são comparadas globalmente no documento.
Quando usar @key
Normalmente, faz sentido usar @key
sempre que uma lista é renderizada (por exemplo, em um bloco foreach
) e existe um valor adequado para definir o @key
.
Você também pode usar @key
para preservar uma subárvore de elemento ou componente quando um objeto não é alterado, como mostram os exemplos a seguir.
Exemplo 1:
<li @key="person">
<input value="@person.Data" />
</li>
Exemplo 2:
<div @key="person">
@* other HTML elements *@
</div>
Se uma instância de person
for alterada, a diretiva de atributo @key
forçará Blazor a:
- Descartar todo o
<li>
ou<div>
e seus descendentes. - Recompilar a subárvore dentro da interface do usuário com novos elementos e componentes.
Isso é útil para garantir que nenhum estado da interface do usuário seja preservado quando a coleção for alterada em uma subárvore.
Escopo de @key
A diretiva de atributo @key
tem como escopo os próprios irmãos dentro do elemento pai.
Considere o exemplo a seguir. As chaves first
e second
são comparadas entre si dentro do mesmo escopo do elemento <div>
externo:
<div>
<div @key="first">...</div>
<div @key="second">...</div>
</div>
O exemplo a seguir demonstra as chaves first
e second
em nos próprios escopos, não relacionadas uma à outra e sem influência uma sobre a outra. Cada escopo de @key
se aplica apenas ao elemento <div>
pai, e não entre os elementos <div>
pai:
<div>
<div @key="first">...</div>
</div>
<div>
<div @key="second">...</div>
</div>
Para o componente Details
mostrado anteriormente, os seguintes exemplos renderizam dados person
dentro do mesmo escopo @key
e demonstram 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>
Os exemplos a seguir só têm escopo @key
para o elemento <div>
ou <li>
que envolve cada instância do componente Details
. Portanto, os dados person
de cada membro da coleção people
não são inseridos em cada instância de person
entre os componentes Details
renderizados. Evite os seguintes padrões ao usar @key
:
@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>
Quando não usar @key
Há um custo quanto ao desempenho ao renderizar com @key
. O custo de desempenho não é grande, mas só especifique @key
se a preservação do elemento ou componente beneficiar o aplicativo.
Mesmo que @key
não seja usado, Blazor preserva o máximo possível as instâncias de componente e de elemento filho. A única vantagem de usar @key
é o controle sobre como as instâncias de modelo são mapeadas para as instâncias de componente preservadas, em vez de Blazor selecionar o mapeamento.
Valores a serem usados para @key
Geralmente, faz sentido fornecer um dos seguintes valores para @key
:
- Instâncias de objetos de modelo. Por exemplo, a instância
Person
(person
) foi usada no exemplo anterior. Isso garante a preservação baseada na igualdade de referência do objeto. - Identificadores exclusivos. Por exemplo, identificadores exclusivos podem ser baseados nos valores de chave primária do tipo
int
,string
ouGuid
.
Certifique-se de que os valores usados para @key
não entrem em conflito. Se valores conflitantes forem detectados dentro do mesmo elemento pai, Blazor gerará uma exceção porque não poderá mapear de modo determinístico elementos ou componentes antigos para elementos ou componentes novos. Use apenas valores distintos, como instâncias de objeto ou valores de chave primária.