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 componentiDetails
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
oGuid
.
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.