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.

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 componentes Details 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; }
}

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; }
    }
}

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 peoplenã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 ou Guid.

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.