Zachowywanie relacji między elementami, składnikami i modelami w usłudze ASP.NET Core Blazor
Uwaga
Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu platformy .NET 9.
Ważne
Te informacje odnoszą się do produktu w wersji wstępnej, który może zostać znacząco zmodyfikowany, zanim zostanie wydany komercyjnie. Firma Microsoft nie udziela żadnych gwarancji, jawnych lub domniemanych, w odniesieniu do informacji podanych w tym miejscu.
Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu platformy .NET 9.
W tym artykule wyjaśniono, jak za pomocą atrybutu @key
dyrektywy zachować relacje elementów, składników i modelu podczas renderowania, a następnie zmieniać elementy lub składniki.
Użycie atrybutu @key
dyrektywy
Podczas renderowania listy elementów lub składników, a następnie zmieniania elementów lub składników, należy zdecydować, Blazor które z poprzednich elementów lub składników są zachowywane i jak obiekty modelu powinny być mapowane na nie. Zwykle ten proces jest automatyczny i wystarczający do ogólnego renderowania, ale często zdarza się, że kontrola procesu przy użyciu atrybutu @key
dyrektywy jest wymagana.
Rozważmy poniższy przykład, który demonstruje problem z mapowaniem kolekcji, który został rozwiązany przy użyciu polecenia @key
.
W przypadku następujących składników:
- Składnik
Details
odbiera dane (Data
) ze składnika nadrzędnego, który jest wyświetlany w elemencie<input>
. Użytkownik może ustawić fokus strony na dowolnym wyświetlanym elemencie<input>
, gdy wybierze jeden z elementów<input>
. - Składnik nadrzędny tworzy listę obiektów osób do wyświetlania
Details
przy użyciu składnika. Co trzy sekundy do kolekcji jest dodawana nowa osoba.
Ten pokaz umożliwia:
- Wybranie elementu
<input>
spośród kilku renderowanych składnikówDetails
. - Zbadanie zachowania fokusu strony, gdy kolekcja osób automatycznie powiększa się.
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; }
}
W poniższym składniku nadrzędnym każda iteracja dodawania osoby powoduje OnTimerCallback
Blazor ponowne skompilowanie całej kolekcji. Fokus strony pozostaje na tej samej pozycji indeksu elementów <input>
, więc przesuwa się za każdym razem, gdy dodawana jest osoba. Przenoszenie fokusu poza element wybrany przez użytkownika nie jest pożądanym zachowaniem. Po zademonstrowaniu niewłaściwego zachowania z użyciem poniższego składnika stosowany jest atrybut dyrektywy @key
w celu poprawienia obsługi użytkownika.
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; }
}
}
Zawartość kolekcji people
zmienia się w przypadku wstawiania, usuwania lub zmieniania kolejności wpisów. Ponowne renderowanie może prowadzić do widocznych różnic w zachowaniu. Na przykład za każdym razem, gdy osoba zostanie wstawiona do people
kolekcji, fokus użytkownika zostanie utracony.
Proces mapowania elementów lub składników na kolekcję można kontrolować za pomocą atrybutu dyrektywy @key
. Użycie atrybutu @key
gwarantuje zachowywanie elementów lub składników na podstawie wartości klucza. Jeśli klucz składnika Details
w powyższym przykładzie jest określany przy użyciu elementu person
, platforma Blazor ignoruje ponowne renderowanie składników Details
, które nie uległy zmianie.
Aby zmodyfikować składnik nadrzędny tak, aby używał atrybutu @key
dyrektywy w people
kolekcji, zaktualizuj <Details>
element do następującego:
<Details @key="person" Data="@person.Data" />
Gdy zmienia się kolekcja people
, zachowywane jest skojarzenie między wystąpieniami elementów Details
i person
. Gdy element Person
jest wstawiany na początku kolekcji, jedno nowe wystąpienie elementu Details
jest wstawiane na odpowiedniej pozycji. Inne wystąpienia pozostają bez zmian. Dlatego fokus użytkownika nie jest tracony, gdy osoby są dodawane do kolekcji.
Inne aktualizacje kolekcji działają tak samo, gdy jest używany atrybut dyrektywy @key
:
- Jeśli wystąpienie zostanie usunięte z kolekcji, tylko odpowiednie wystąpienie składnika zostanie usunięte z interfejsu użytkownika. Inne wystąpienia pozostają bez zmian.
- Gdy kolejność wpisów kolekcji ulega zmianie, odpowiadające im wystąpienia składników są zachowywane, a ich kolejność w interfejsie użytkownika jest zmieniana.
Ważne
Klucze są lokalne dla każdego elementu kontenera lub składnika. Klucze nie są porównywane globalnie w całym dokumencie.
Kiedy używać atrybutu @key
Zazwyczaj warto używać atrybutu @key
zawsze, gdy jest renderowana lista (np. w bloku foreach
) i istnieje odpowiednia wartość do zdefiniowania atrybutu @key
.
Można też używać atrybutu @key
, aby zachowywać poddrzewo elementu lub składnika, gdy dany obiekt nie zmienia się, jak pokazują poniższe przykłady.
Przykład 1:
<li @key="person">
<input value="@person.Data" />
</li>
Przykład 2:
<div @key="person">
@* other HTML elements *@
</div>
Jeśli zmieni się wystąpienie elementu person
, atrybut dyrektywy @key
wymusza na platformie Blazor:
- Odrzucenie całego elementu
<li>
lub<div>
i jego elementów podrzędnych. - Ponowne utworzenie poddrzewa w interfejsie użytkownika przy użyciu nowych elementów i składników.
Jest to przydatne, aby zagwarantować, że żaden stan interfejsu użytkownika nie zostanie zachowany, gdy kolekcja zmieni się w obrębie poddrzewa.
Zakres atrybutu @key
Zakresem atrybutu dyrektywy @key
są jego elementy równorzędne w obrębie jego elementu nadrzędnego.
Rozważmy następujący przykład. Klucze first
i second
są porównywane ze sobą w tym samym zakresie zewnętrznego elementu <div>
:
<div>
<div @key="first">...</div>
<div @key="second">...</div>
</div>
Poniższy przykład przedstawia klucze first
i second
w ich własnych zakresach, niepowiązanych ze sobą i niemających na siebie wpływu. Zakres każdego atrybutu @key
dotyczy tylko jego nadrzędnego elementu <div>
, a nie wszystkich nadrzędnych elementów <div>
:
<div>
<div @key="first">...</div>
</div>
<div>
<div @key="second">...</div>
</div>
Dla przedstawionego wcześniej składnika Details
poniższe przykłady renderują dane elementów person
w zakresie tego samego atrybutu @key
oraz pokazują typowe przypadki użycia atrybutu @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>
W poniższych przykładach zakres atrybutu @key
obejmuje tylko element <div>
lub <li>
, który otacza każde wystąpienie składnika Details
. Dlatego dane elementów person
dla każdego elementu członkowskiego kolekcji people
nie są określane przy użyciu kluczy każdego wystąpienia elementu person
we wszystkich renderowanych składnikach Details
. Podczas używania atrybutu @key
należy unikać następujących wzorców:
@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>
Kiedy nie używać atrybutu @key
Renderowanie przy użyciu atrybutu @key
obniża wydajność. Spadek wydajności nie jest duży, ale atrybut @key
należy określać tylko wtedy, gdy zachowywanie elementu lub składnika daje korzyści dla aplikacji.
Nawet jeśli atrybut @key
nie jest używany, platforma Blazor zachowuje wystąpienia elementów i składników podrzędnych, na ile to możliwe. Jedyną zaletą używania atrybutu @key
jest kontrola nad tym, jak wystąpienia modelu są mapowane na zachowywane wystąpienia składników, zamiast wybierania mapowania przez platformę Blazor.
Wartości, których należy używać dla atrybutu @key
Ogólnie rzecz biorąc, dla atrybutu @key
odpowiednia jest jedna z następujących wartości:
- Wystąpienia modelu obiektów. Na przykład we wcześniejszym przykładzie użyto wystąpienia klasy
Person
(person
). Zapewnia to zachowywanie elementów oparte na równości odwołań do obiektów. - Unikatowe identyfikatory. Na przykład unikatowe identyfikatory mogą być oparte na wartościach klucza podstawowego typu
int
,string
lubGuid
.
Upewnij się, że wartości używane dla atrybutu @key
nie kolidują ze sobą. Jeśli zostaną wykryte kolidujące wartości w obrębie tego samego elementu nadrzędnego, platforma Blazor zgłosi wyjątek, ponieważ nie będzie mogła deterministycznie mapować starych elementów lub składników na nowe elementy lub składniki. Używaj tylko unikatowych wartości, takich jak wystąpienia obiektów lub wartości klucza podstawowego.