Mantenere le relazioni tra elementi, componenti e modelli in ASP.NET Core Blazor

Nota

Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Avviso

Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere Criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 8 di questo articolo.

Importante

Queste informazioni si riferiscono a un prodotto non definitive che può essere modificato in modo sostanziale prima che venga rilasciato commercialmente. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.

Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Questo articolo illustra come usare l'attributo @key di direttiva per mantenere le relazioni tra elementi, componenti e modelli durante il rendering e gli elementi o i componenti vengono successivamente modificati.

Uso dell'attributo della @key direttiva

Quando si esegue il rendering di un elenco di elementi o componenti e gli elementi o i componenti successivamente modificati, Blazor è necessario decidere quali elementi o componenti precedenti vengono mantenuti e come gli oggetti modello devono essere mappati a essi. In genere, questo processo è automatico e sufficiente per il rendering generale, ma spesso ci sono casi in cui è necessario controllare il processo usando l'attributo @key della direttiva.

Si consideri l'esempio seguente che illustra un problema di mapping della raccolta risolto tramite @key.

Per i componenti seguenti:

  • Il Details componente riceve i dati (Data) dal componente padre, che viene visualizzato in un <input> elemento . Qualsiasi elemento <input> visualizzato può ricevere lo stato attivo della pagina dall'utente quando seleziona uno degli elementi <input>.
  • Il componente padre crea un elenco di oggetti person da visualizzare usando il Details componente . Ogni tre secondi, una nuova persona viene aggiunta alla raccolta.

Questa dimostrazione consente di:

  • Selezionare un elemento <input> tra diversi componenti Details di cui è stato eseguito il rendering.
  • Studiare il comportamento dello stato attivo della pagina man mano che la raccolta persone aumenta 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; }
}

Nel componente padre seguente, ogni iterazione dell'aggiunta di una persona comporta OnTimerCallback Blazor la ricompilazione dell'intera raccolta. Lo stato attivo della pagina rimane posizionato sullo stesso indice degli elementi <input>, quindi lo stato attivo si sposta ogni volta che viene aggiunta una persona. Lo spostamento dello stato attivo dall'elemento selezionato dall'utente non è un comportamento auspicabile. Dopo aver dimostrato il comportamento non ottimale con il componente seguente, l'attributo della direttiva @key viene usato per migliorare l'esperienza dell'utente.

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

Il contenuto della raccolta people cambia in base alle voci inserite, eliminate o riordinate. Il rerendering può causare differenze di comportamento visibili. Ad esempio, ogni volta che una persona viene inserita nella people raccolta, lo stato attivo dell'utente viene perso.

Il processo di mapping di elementi o componenti a una raccolta può essere controllato con l'attributo della direttiva @key. L'uso di @key garantisce la conservazione di elementi o componenti in base al valore della chiave. Se il componente Details nell'esempio precedente è collegato con una chiave nell'elemento person, Blazor ignora la ripetizione del rendering dei componenti Details che non sono stati modificati.

Per modificare il componente padre in modo da usare l'attributo @key di direttiva con la people raccolta, aggiornare l'elemento <Details> nel modo seguente:

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

Quando la raccolta people viene modificata, l'associazione tra le istanze di Details e le istanze di person viene mantenuta. Quando un elemento Person viene inserito all'inizio della raccolta, una nuova istanza di Details viene inserita nella posizione corrispondente. Le altre istanze vengono lasciate invariate. Di conseguenza, lo stato attivo dell'utente non viene perso man mano che le persone vengono aggiunte alla raccolta.

Gli altri aggiornamenti della raccolta presentano lo stesso comportamento quando viene usato l'attributo della direttiva @key:

  • Se un'istanza viene eliminata dalla raccolta, dall'interfaccia utente viene rimossa solo l'istanza del componente corrispondente. Le altre istanze vengono lasciate invariate.
  • Se le voci della raccolta vengono riordinate, le istanze dei componenti corrispondenti vengono mantenute e riordinate nell'interfaccia utente.

Importante

Le chiavi sono locali per ogni elemento o componente del contenitore. Le chiavi non vengono confrontate a livello globale nel documento.

Quando usare @key

In genere, è consigliabile usare @key ogni volta che viene eseguito il rendering di un elenco (ad esempio, in un blocco foreach) ed esiste un valore appropriato per definire @key.

È anche possibile usare @key per mantenere un sottoalbero di elementi o componenti quando un oggetto non cambia, come illustrato negli esempi seguenti.

Esempio 1:

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

Esempio 2:

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

Se un'istanza di person cambia, la direttiva dell'attributo @key forza Blazor a:

  • Rimuovere completamente <li> o <div> e i discendenti.
  • Ricompilare il sottoalbero all'interno dell'interfaccia utente con nuovi elementi e componenti.

È utile per garantire che non venga mantenuto alcun stato dell'interfaccia utente quando la raccolta cambia all'interno di un sottoalbero.

Ambito di @key

La direttiva dell'attributo @key è inclusa nell'ambito degli elementi di pari livello all'interno dell'elemento padre.

Si consideri l'esempio seguente. Le chiavi first e second vengono confrontate tra loro nello stesso ambito dell'elemento <div> esterno:

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

L'esempio seguente mostra le chiavi first e second nei propri ambiti, non correlate tra loro e senza influenza reciproca. Ogni ambito @key si applica solo al relativo elemento <div> padre, non a tutti gli elementi <div> padre:

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

Per il componente Details illustrato in precedenza, gli esempi seguenti eseguono il rendering dei dati person nello stesso ambito @key e mostrano i casi d'uso tipici per @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>

Negli esempi seguenti l'ambito di @key viene limitato all'elemento <div> o <li> che circonda ogni istanza del componente Details. I dati person per ogni membro della raccolta people non vengono quindi collegati con una chiave in ogni istanza di person nei componenti Details di cui è stato eseguito il rendering. Evitare i modelli seguenti quando si usa @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 evitare l'uso di @key

Il rendering con @key comporta un costo in termini di prestazioni. Il costo in termini di prestazioni non è elevato, ma specificare @key solo se la conservazione dell'elemento o del componente costituisce un vantaggio per l'app.

Anche se non si usa @key, Blazor mantiene il più possibile le istanze dell'elemento figlio e del componente. L'unico vantaggio derivante dall'uso di @key è il controllo su come le istanze del modello vengono associate alle istanze del componente conservate invece di lasciare che sia Blazor a selezionare il mapping.

Valori da usare per @key

In genere, è consigliabile specificare uno dei valori seguenti per @key:

  • Istanze di oggetti modello. Ad esempio, l'istanza di Person (person) è stata usata nell'esempio precedente. Ciò garantisce la conservazione in base all'uguaglianza dei riferimenti agli oggetti.
  • Identificatori univoci. Ad esempio, gli identificatori univoci possono essere basati sui valori di chiave primaria di tipo int, string o Guid.

Assicurarsi che i valori usati per @key non siano in conflitto. Se vengono rilevati valori in conflitto all'interno dello stesso elemento padre, Blazor genera un'eccezione perché non può eseguire in modo deterministico il mapping di elementi o componenti precedenti a nuovi elementi o componenti. Usare solo valori distinti, ad esempio istanze di oggetti o valori di chiave primaria.