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ą, zapoznaj się z wersją tego artykułu platformy .NET 8.

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ą, zapoznaj się z wersją tego artykułu platformy .NET 8.

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ów Details.
  • 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; }
}

W poniższym składniku nadrzędnym każda iteracja dodawania osoby powoduje OnTimerCallbackBlazor 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; }
    }
}

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 peoplenie 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 lub Guid.

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.