Pertahankan hubungan elemen, komponen, dan model di ASP.NET Core Blazor

Catatan

Ini bukan versi terbaru dari artikel ini. Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.

Penting

Informasi ini berkaitan dengan produk pra-rilis yang mungkin dimodifikasi secara substansial sebelum dirilis secara komersial. Microsoft tidak memberikan jaminan, tersirat maupun tersurat, sehubungan dengan informasi yang diberikan di sini.

Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.

Artikel ini menjelaskan cara menggunakan @key atribut direktif untuk mempertahankan elemen, komponen, dan hubungan model saat penyajian dan elemen atau komponen kemudian berubah.

Penggunaan atribut direktif @key

Saat merender daftar elemen atau komponen dan elemen atau komponen yang kemudian berubah, Blazor harus memutuskan elemen atau komponen mana yang dipertahankan sebelumnya dan bagaimana objek model harus memetakannya. Biasanya, proses ini otomatis dan cukup untuk penyajian umum, tetapi sering ada kasus di mana mengontrol proses menggunakan atribut direktif @key diperlukan.

Pertimbangkan contoh berikut yang menunjukkan masalah pemetaan koleksi yang diselesaikan dengan menggunakan @key.

Untuk komponen berikut:

  • Komponen Details menerima data (Data) dari komponen induk, yang ditampilkan dalam <input> elemen. Setiap elemen <input> yang ditampilkan dapat menerima fokus halaman dari pengguna saat mereka memilih salah satu elemen <input>.
  • Komponen induk membuat daftar objek orang untuk ditampilkan menggunakan Details komponen. Setiap tiga detik, orang baru ditambahkan ke koleksi.

Demonstrasi ini memungkinkan Anda untuk:

  • Memilih <input> dari beberapa komponen Details yang dirender.
  • Mempelajari perilaku fokus halaman saat koleksi orang bertambah secara otomatis.

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

Dalam komponen induk berikut, setiap iterasi penambahan seseorang menghasilkan OnTimerCallbackBlazor pembangunan kembali seluruh koleksi. Fokus halaman tetap pada posisi indeks yang sama dari elemen <input>, sehingga fokus bergeser setiap kali ada orang yang ditambahkan. Menggeser fokus dari apa yang dipilih pengguna bukanlah perilaku yang diinginkan. Setelah menunjukkan perilaku buruk dengan komponen berikut, atribut arahan @key digunakan untuk meningkatkan pengalaman pengguna.

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

Konten kumpulan people berubah dengan entri yang disisipkan, dihapus, atau diurutkan ulang. Perenderan ulang dapat menyebabkan perbedaan perilaku yang terlihat. Misalnya, setiap kali seseorang dimasukkan ke dalam people koleksi, fokus pengguna hilang.

Proses pemetaan elemen atau komponen ke koleksi dapat dikontrol dengan atribut arahan @key. Penggunaan @key menjamin preservasi elemen atau komponen berdasarkan nilai kunci. Jika komponen Details dalam contoh sebelumnya dikunci pada item person, Blazor mengabaikan perenderan ulang komponen Details yang tidak berubah.

Untuk memodifikasi komponen induk untuk menggunakan atribut direktif @key dengan people koleksi, perbarui <Details> elemen ke yang berikut:

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

Saat koleksi people berubah, hubungan antara instans Details dan instans person dipertahankan. Saat Person disisipkan di awal koleksi, satu instans Details baru disisipkan pada posisi yang sesuai. Instans lain dibiarkan tidak berubah. Oleh karena itu, fokus pengguna tidak hilang saat orang ditambahkan ke koleksi.

Pembaruan koleksi lainnya menunjukkan perilaku yang sama ketika atribut arahan @key digunakan:

  • Jika sebuah instans dihapus dari koleksi, hanya instans komponen terkait yang dihapus dari antarmuka pengguna. Instans lain dibiarkan tidak berubah.
  • Jika entri koleksi diurutkan ulang, instans komponen terkait akan dipertahankan dan diurutkan ulang di antarmuka pengguna.

Penting

Kunci bersifat lokal untuk setiap elemen atau komponen kontainer. Kunci tidak dibandingkan secara global di seluruh dokumen.

Kapan harus menggunakan @key

Biasanya, masuk akal untuk menggunakan @key setiap kali daftar dirender (misalnya, dalam blok foreach) dan ada nilai yang sesuai untuk mendefinisikan @key.

Anda juga dapat menggunakan @key untuk mempertahankan elemen atau komponen sub-pohon saat objek tidak berubah, seperti yang ditunjukkan contoh berikut.

Contoh 1:

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

Contoh 2:

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

Jika instans person berubah, arahan atribut @key memaksa Blazor untuk:

  • Membuang seluruh <li> atau <div> dan keturunannya.
  • Membangun kembali sub-pohon di dalam antarmuka pengguna dengan elemen dan komponen baru.

Ini berguna untuk menjamin bahwa tidak ada status antarmuka pengguna yang dipertahankan saat koleksi berubah dalam sub-pohon.

Cakupan @key

Arahan atribut @key dicakupkan ke saudaranya sendiri di dalam induknya.

Pertimbangkan contoh berikut. Kunci first dan second dibandingkan dengan satu sama lain dalam cakupan elemen <div> luar yang sama:

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

Contoh berikut menunjukkan kunci first dan second dalam cakupannya masing-masing, yang tidak terkait dengan satu sama lain dan tidak memiliki pengaruh terhadap satu sama lain. Setiap cakupan @key hanya berlaku untuk elemen <div> induknya, bukan di seluruh elemen <div> induknya:

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

Untuk komponen Details yang ditampilkan sebelumnya, contoh berikut merender data person dalam cakupan @key yang sama dan menunjukkan kasus penggunaan umum untuk @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>

Contoh berikut hanya mencakup @key ke elemen <div> atau <li> yang mengelilingi setiap instans komponen Details. Oleh karena itu, data person untuk setiap anggota koleksi peopletidak dikunci pada setiap instans person di seluruh komponen Details yang dirender. Hindari pola berikut saat menggunakan @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>

Kapan tidak seharusnya menggunakan @key

Ada biaya performa saat merender dengan @key. Biaya performa tidak besar, tetapi hanya tentukan @key jika mempertahankan elemen atau komponen menguntungkan aplikasi.

Meskipun @key tidak digunakan, Blazor mempertahankan instans elemen dan komponen turunan sebanyak mungkin. Satu-satunya keuntungan menggunakan @key adalah kontrol atas bagaimana instans model dipetakan ke instans komponen yang dipertahankan, alih-alih Blazor yang memilih pemetaan.

Nilai yang akan digunakan untuk @key

Secara umum, masuk akal untuk memberikan salah satu nilai berikut untuk @key:

  • Instans objek model. Misalnya, instans Person (person) digunakan pada contoh sebelumnya. Ini memastikan preservasi berdasarkan kesetaraan referensi objek.
  • Pengidentifikasi unik. Misalnya, pengidentifikasi unik dapat didasarkan pada nilai kunci primer dari jenis int, string, atau Guid.

Pastikan bahwa nilai yang digunakan untuk @key tidak bentrok. Jika terdeteksi nilai yang bentrok dalam elemen induk yang sama, Blazor akan menampilkan pengecualian karena tidak dapat secara deterministik memetakan elemen atau komponen lama ke elemen atau komponen baru. Hanya gunakan nilai yang berbeda, seperti instans objek atau nilai kunci primer.