Zachování relací prvků, komponent a modelů v ASP.NET Core Blazor

Poznámka:

Toto není nejnovější verze tohoto článku. Aktuální verzi najdete ve verzi .NET 8 tohoto článku.

Upozorňující

Tato verze ASP.NET Core se už nepodporuje. Další informace najdete v tématu .NET a .NET Core Zásady podpory. Aktuální verzi najdete ve verzi .NET 8 tohoto článku.

Důležité

Tyto informace se týkají předběžného vydání produktu, který může být podstatně změněn před komerčním vydáním. Microsoft neposkytuje žádné záruky, výslovné ani předpokládané, týkající se zde uváděných informací.

Aktuální verzi najdete ve verzi .NET 8 tohoto článku.

Tento článek vysvětluje, jak pomocí atributu @key direktiv zachovat relace elementu, komponenty a modelu při vykreslování a následné změně prvků nebo komponent.

Použití atributu direktivy @key

Při vykreslování seznamu prvků nebo součástí a prvků nebo součástí se následně musí rozhodnout, Blazor které z předchozích prvků nebo součástí jsou zachovány a jak se mají objekty modelu mapovat na ně. Obvykle je tento proces automatický a dostatečný pro obecné vykreslování, ale často existují případy, kdy řízení procesu pomocí atributu @key direktivy vyžaduje.

Podívejte se na následující příklad, který ukazuje problém mapování kolekce, který je vyřešen pomocí @key.

Pro následující komponenty:

  • Komponenta Details přijímá data (Data) z nadřazené komponenty, která se zobrazí v elementu <input> . Jednotlivé zobrazené elementy <input> můžou získat fokus stránky, když uživatel vybere některý z elementů <input>.
  • Nadřazená komponenta vytvoří seznam objektů osob pro zobrazení pomocí Details komponenty. Každé tři sekundy se do kolekce přidá nová osoba.

Tato ukázka umožňuje:

  • Vybrat element <input> z několika vykreslených komponent Details.
  • Zkoumat chování fokusu stránky s tím, jak se kolekce osob automaticky zvětšuje.

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

Každá iterace přidání osoby v OnTimerCallback následující nadřazené komponentě vede k Blazor opětovnému sestavení celé kolekce. Fokus stránky zůstane v elementech <input> na pozici se stejným indexem, takže se při každém přidání uživatele posune. Posun fokusu mimo uživatelem vybraný element není žádoucí. Po ukázce špatného chování na následující komponentě se k vylepšení uživatelského prostředí používá atribut direktivy @key.

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

Obsah kolekce people se změní při vložení, odstranění nebo změně pořadí položek. Opětovné vykreslování může mít za následek viditelné změny chování. Například při každém vložení osoby do people kolekce dojde ke ztrátě fokusu uživatele.

Proces mapování elementů nebo komponent na kolekci je možné řídit pomocí atributu direktivy @key. Použitím atributu @key se zajistí zachování elementů nebo komponent na základě hodnoty klíče. Pokud komponenta Details ve výše uvedeném příkladu jako klíč používá položku person, Blazor ignoruje vykreslování komponent Details, které se nezměnily.

Chcete-li upravit nadřazenou komponentu @key tak, aby používala atribut direktivy people s kolekcí, aktualizujte <Details> prvek na následující:

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

Při změně kolekce people se zachová přidružení mezi instancemi Details a instancemi person. Když se na začátek kolekce vloží objekt Person, na odpovídající pozici se vloží jedna nová instance Details. Ostatní instance zůstanou beze změny. Proto se při přidávání osob do kolekce neztratí fokus uživatele.

Stejné chování při použití atributu direktivy @key vykazují i ostatní aktualizace kolekcí:

  • Pokud se z kolekce odstraní instance, z uživatelského rozhraní se odebere pouze odpovídající instance komponenty. Ostatní instance zůstanou beze změny.
  • Pokud dojde ke změně pořadí položek kolekce, v uživatelském rozhraní se zachovají odpovídající instance komponenty a změní se jejich pořadí.

Důležité

Klíče jsou lokální pro každý element nebo každou komponentu kontejneru. Klíče se neporovnávají globálně napříč dokumentem.

Kdy použít atribut @key

Obvykle dává smysl použít atribut @key vždy, když se vykresluje seznam (například v bloku foreach) a pro definování atributu @key existuje vhodná hodnota.

Atribut @key můžete použít také k zachování podstromu elementů nebo komponent, když nedojde ke změně objektu, jak ukazují následující příklady.

Příklad 1:

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

Příklad 2:

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

Pokud dojde ke změně instance person, atribut direktivy @key přinutí Blazor, aby provedl následující:

  • Zahodil celý element <li> nebo <div> a jejich potomky.
  • Znovu sestavil podstrom v rámci uživatelského rozhraní s novými elementy a komponentami.

To je užitečné k zajištění, aby se při změně kolekce v rámci podstromu nezachoval žádný stav uživatelského rozhraní.

Obor atributu @key

Atribut direktivy @key má obor vymezený na podřízené elementy nadřazeného elementu na stejné úrovni.

Představte si následující příklad. Klíče first a second se porovnávají mezi sebou ve stejném oboru vnějšího elementu <div>:

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

Následující příklad ukazuje klíče first a second ve vlastních oborech, které spolu nesouvisí a vzájemně se neovlivňují. Obor každého atributu @key se vztahuje na vlastní nadřazený element <div>, nikoli na ostatní nadřazené elementy <div>:

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

V případě výše uvedené komponenty Details následující příklady vykreslí data person ve stejném oboru @key a ukazují obvyklé případy použití atributu @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>

V následujících příkladech je obor atributu @key vymezený pouze na element <div> nebo <li> kolem každé instance komponenty Details. Proto se pro data person každého člena kolekce people nepoužívá jako klíč každá instance person napříč vykreslenými komponentami Details. Při používání atributu @key se vyhněte následujícím vzorům:

@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>

Kdy nepoužívat atribut @key

Vykreslování s použitím atributu @key má dopad na výkon. Dopad na výkon není velký, ale přesto používejte atribut @key pouze v případě, že je zachování prvku nebo komponenty pro aplikaci přínosné.

I když nepoužijete atribut @key, Blazor zachovává instance podřízených elementů a komponent v co největší míře. Jedinou výhodou použití atributu @key je to, že máte kontrolu nad tím, jak se instance modelu mapují na zachované instance komponent, namísto toho, aby mapování vybírala architektura Blazor.

Jaké hodnoty použít pro atribut @key

Obecně je vhodné pro atribut @key zadat některou z následujících hodnot:

  • Instance objektů modelu. Například ve výše uvedeném příkladu se použila instance objektu Person (person). Tím se zajistí zachování na základě rovnosti odkazů na objekty.
  • Jedinečné identifikátory. Jedinečné identifikátory můžou vycházet například z hodnot primárního klíče typu int, string nebo Guid.

Ujistěte se, že hodnoty použité pro atribut @key nejsou v konfliktu. Pokud se v rámci stejného nadřazeného elementu zjistí kolidující hodnoty, Blazor vyvolá výjimku, protože nedokáže deterministicky mapovat staré elementy nebo komponenty na nové elementy nebo komponenty. Používejte pouze jedinečné hodnoty, například instance objektů nebo hodnoty primárního klíče.