Partilhar via


Manter as relações entre elementos, componentes e modelos no ASP.NET Core Blazor

Observação

Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão .NET 10 deste artigo.

Advertência

Esta versão do ASP.NET Core não é mais suportada. Para obter mais informações, consulte a Política de suporte do .NET e do .NET Core. Para a versão atual, consulte a versão .NET 9 deste artigo.

Este artigo explica como usar o @key atributo diretivo para manter as relações entre elementos, componentes e modelos ao renderizar e os elementos ou componentes mudam posteriormente.

Utilização do @key atributo diretivo

Ao renderizar uma lista de elementos ou componentes e os elementos ou componentes mudam posteriormente, Blazor é necessário decidir quais dos elementos ou componentes anteriores são mantidos e como os objetos do modelo devem mapear para eles. Normalmente, este processo é automático e suficiente para renderização geral, mas há frequentemente casos em que é necessário controlar o processo usando o @key atributo diretivo.

Considere o seguinte exemplo que demonstra um problema de mapeamento de coleções resolvido usando @key.

Para os seguintes componentes:

  • O Details componente recebe dados (Data) do componente pai, que é exibido num <input> elemento. Os elementos <input> apresentados podem receber o foco da página do utilizador quando este seleciona um dos elementos <input>.
  • O componente pai 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.

Esta demonstração permite-lhe:

  • Escolha um <input> dentre vários componentes renderizados Details.
  • Estude 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 seguinte, cada vez que uma pessoa é adicionada OnTimerCallback, resulta na Blazor reconstrução de toda a coleção. O foco da página mantém-se na mesma posição de índice dos <input> elementos, pelo que o foco muda cada vez que uma pessoa é adicionada. Desviar o foco do que o utilizador selecionou não é um comportamento desejável. Após demonstrar o mau comportamento com o componente seguinte, o @key atributo diretivo é usado para melhorar a experiência do utilizador.

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 people coleção muda com entradas inseridas, eliminadas ou reordenadas. A rerenderização pode levar a diferenças visíveis de comportamento. Por exemplo, cada vez que uma pessoa é inserida na people coleção, o foco do utilizador perde-se.

O processo de mapeamento de elementos ou componentes para uma coleção pode ser controlado com o @key atributo diretivo. O uso de @key garante a preservação de elementos ou componentes com base no valor da chave. Se o Details componente no exemplo anterior estiver indexado no person item, Blazor ignora a rerenderização Details dos componentes que não mudaram.

Para modificar o componente pai para usar o @key atributo diretivo com a people coleção, atualize o <Details> elemento para o seguinte:

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

Quando a people coleção muda, a associação entre Details instâncias e person instâncias é mantida. Quando a Person é inserida no início da coleção, uma nova instância de Details é inserida na posição correspondente. Outros casos permanecem inalterados. Assim, o foco do utilizador não se perde à medida que as pessoas são adicionadas à coleção.

Outras atualizações da coleção apresentam o mesmo comportamento quando o @key atributo diretivo é utilizado:

  • Se uma instância for eliminada da coleção, apenas a instância de componente correspondente é removida da interface. Outros casos permanecem inalterados.
  • Se as entradas da coleção forem reordenadas, as instâncias dos componentes correspondentes são preservadas e reordenadas na interface.

Importante

As chaves são locais para cada elemento ou componente do contentor. As chaves não são comparadas globalmente em todo o documento.

Quando usar @key

Normalmente, faz sentido usar @key sempre que uma lista é renderizada (por exemplo, num foreach bloco) e existe um valor adequado para definir o @key.

Também pode usar @key para preservar uma subárvore de elemento ou componente quando um objeto não muda, como mostram os exemplos seguintes.

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 mudar, a diretiva de atributo @key obriga Blazor a:

  • Remova todo o <li> ou <div> e os seus descendentes.
  • Reconstrói a subárvore dentro da interface com novos elementos e componentes.

Isto é útil para garantir que nenhum estado da interface é preservado quando a coleção muda dentro de uma subárvore.

Âmbito de @key

A diretiva de atributo @key é delimitada aos seus elementos irmãos dentro do seu elemento pai.

Considere o exemplo a seguir. As chaves first e second são comparadas entre si, dentro do mesmo âmbito do elemento exterior <div>.

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

O exemplo seguinte demonstra as chaves first e second nos seus próprios âmbitos, sem relação entre si e sem influência mútua. Cada @key âmbito aplica-se apenas ao seu elemento pai <div> , não entre os elementos pais <div> :

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

Para o componente Details apresentado anteriormente, os seguintes exemplos renderizam dados person dentro do mesmo escopo @key e demonstram casos típicos de uso 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 seguintes limitam-se apenas ao elemento @key ou <div> que envolve cada instância de componente <li>. Portanto, os dados de cada membro da coleção personpeoplenão são identificados por chave em cada instância person, através dos componentes renderizados Details. Evite os seguintes padrões ao utilizar @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

Existe um custo de desempenho ao renderizar com @key. O custo de desempenho não é elevado, mas especifique @key apenas se preservar o elemento ou componente beneficia a aplicação.

Mesmo que @key não seja usado, Blazor preserva as instâncias de elementos e componentes filhos tanto quanto possível. A única vantagem de usar @key é o controlo sobre como as instâncias do modelo são mapeadas para as instâncias do componente preservado, em vez de Blazor selecionar o mapeamento.

Valores a usar para @key

Geralmente, faz sentido fornecer um dos seguintes valores para @key:

  • Modelar instâncias de objetos. Por exemplo, a Person instância (person) foi usada no exemplo anterior. Isto assegura a preservação com base na igualdade das referências de objetos.
  • Identificadores únicos. Por exemplo, identificadores únicos podem ser baseados em valores primários de chave do tipo int, string, ou Guid.

Assegura-te de que os valores usados para @key não entram em conflito. Se forem detetados valores em conflito dentro do mesmo elemento pai, Blazor lança uma exceção porque não pode mapear deterministicamente elementos ou componentes antigos para novos elementos ou componentes. Use apenas valores distintos, como instâncias de objeto ou valores de chave primária.