Bagikan melalui


Pengoptimalan UI ListView dan GridView

Catatan Untuk detail selengkapnya, lihat sesi //build/ Meningkatkan Performa Secara Dramatis saat Pengguna Berinteraksi dengan Data Dalam Jumlah Besar di GridView dan ListView.

Tingkatkan performa ListView dan GridView serta waktu mulai melalui virtualisasi UI, pengurangan elemen, dan pembaruan item yang progresif. Untuk teknik virtualisasi data, lihat Virtualisasi data ListView dan GridView.

Dua faktor utama dalam performa pengumpulan

Memanipulasi koleksi adalah skenario umum. Penampil foto memiliki koleksi foto, pembaca memiliki koleksi artikel/buku/cerita, dan aplikasi belanja memiliki koleksi produk. Topik ini menunjukkan apa yang dapat Anda lakukan untuk membuat aplikasi Anda efisien dalam memanipulasi koleksi.

Ada dua faktor utama dalam performa dalam hal koleksi: satu adalah waktu yang dihabiskan oleh utas UI membuat item; yang lain adalah memori yang digunakan oleh himpunan data mentah dan elemen UI yang digunakan untuk merender data tersebut.

Untuk pengalihan/pengguliran yang lancar, sangat penting bahwa utas UI melakukan pekerjaan yang efisien dan cerdas untuk membuat instans, mengikat data, dan meletakkan item.

Virtualisasi UI

Virtualisasi UI adalah peningkatan terpenting yang dapat Anda lakukan. Ini berarti bahwa elemen UI yang mewakili item dibuat sesuai permintaan. Untuk kontrol item yang terikat ke koleksi 1000 item, itu akan membuang-buang sumber daya untuk membuat UI untuk semua item secara bersamaan, karena tidak semuanya dapat ditampilkan pada saat yang sama. ListView dan GridView (dan kontrol turunan ItemsControl standar lainnya) melakukan virtualisasi UI untuk Anda. Ketika item hampir digulir ke tampilan (beberapa halaman menjauh), kerangka kerja menghasilkan UI untuk item dan menyimpannya dalam cache. Ketika tidak mungkin item akan ditampilkan lagi, kerangka kerja mengklaim ulang memori.

Jika Anda menyediakan templat panel item kustom (lihat ItemsPanel) maka pastikan Anda menggunakan panel virtualisasi seperti ItemsWrapGrid atau ItemsStackPanel. Jika Anda menggunakan VariableSizedWrapGrid, WrapGrid, atau StackPanel, maka Anda tidak akan mendapatkan virtualisasi. Selain itu, peristiwa ListView berikut ini hanya dimunculkan saat menggunakan ItemWrapGrid atau ItemsStackPanel: MemilihGroupHeaderContainer, ChoosingItemContainer, dan ContainerContentChanging.

Konsep viewport sangat penting untuk virtualisasi UI karena kerangka kerja harus membuat elemen yang kemungkinan akan ditampilkan. Secara umum, viewport itemControl adalah sejauh mana kontrol logis. Misalnya, viewport dari ListView adalah lebar dan tinggi elemen ListView. Beberapa panel memungkinkan elemen anak ruang tidak terbatas, contohnya adalah ScrollViewer dan Grid, dengan baris atau kolom berukuran otomatis. Ketika ItemControl virtual ditempatkan di panel seperti itu, dibutuhkan cukup ruang untuk menampilkan semua itemnya, yang mengalahkan virtualisasi. Pulihkan virtualisasi dengan mengatur lebar dan tinggi pada ItemsControl.

Pengurangan elemen per item

Pertahankan jumlah elemen UI yang digunakan untuk merender item Anda ke minimum yang wajar.

Saat kontrol item pertama kali ditampilkan, semua elemen yang diperlukan untuk merender viewport penuh item dibuat. Selain itu, saat item mendekati viewport, kerangka kerja memperbarui elemen UI dalam templat item yang di-cache dengan objek data terikat. Meminimalkan kompleksitas markup di dalam templat terbayar dalam memori dan waktu yang dihabiskan pada utas UI, meningkatkan responsivitas terutama saat panning/scrolling. Templat yang dimaksud adalah templat item (lihat ItemTemplate) dan templat kontrol ListViewItem atau GridViewItem (templat kontrol item, atau ItemContainerStyle). Manfaat bahkan pengurangan kecil dalam jumlah elemen dikalikan dengan jumlah item yang ditampilkan.

Untuk contoh pengurangan elemen, lihat Mengoptimalkan markup XAML Anda.

Templat kontrol default untuk ListViewItem dan GridViewItem berisi elemen ListViewItemPresenter. Penyaji ini adalah elemen tunggal yang dioptimalkan yang menampilkan visual kompleks untuk fokus, pemilihan, dan status visual lainnya. Jika Anda sudah memiliki templat kontrol item kustom (ItemContainerStyle), atau jika di masa mendatang Anda mengedit salinan templat kontrol item, kami sarankan Anda menggunakan ListViewItemPresenter karena elemen tersebut akan memberi Anda keseimbangan optimal antara performa dan penyesuaian dalam sebagian besar kasus. Anda menyesuaikan penyaji dengan mengatur properti di dalamnya. Misalnya, berikut markup yang menghapus tanda centang yang muncul secara default saat item dipilih, dan mengubah warna latar belakang item yang dipilih menjadi oranye.

...
<ListView>
    ...
    <ListView.ItemContainerStyle>
        <Style TargetType="ListViewItem">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ListViewItem">
                        <ListViewItemPresenter SelectionCheckMarkVisualEnabled="False" SelectedBackground="Orange"/>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ListView.ItemContainerStyle>
</ListView>
<!-- ... -->

Ada sekitar 25 properti dengan nama yang menjelaskan sendiri yang mirip dengan SelectionCheckMarkVisualEnabled dan SelectedBackground. Jika jenis penyaji terbukti tidak cukup dapat disesuaikan untuk kasus penggunaan Anda, Anda dapat mengedit salinan ListViewItemExpanded templat atau GridViewItemExpanded kontrol sebagai gantinya. Ini dapat ditemukan di \Program Files (x86)\Windows Kits\10\DesignTime\CommonConfiguration\Neutral\UAP\<version>\Generic\generic.xaml. Ketahuilah bahwa menggunakan templat ini berarti memperdagangkan beberapa performa untuk peningkatan penyesuaian.

Memperbarui item ListView dan GridView secara progresif

Jika Anda menggunakan virtualisasi data, Anda dapat menjaga respons ListView dan GridView tetap tinggi dengan mengonfigurasi kontrol untuk merender elemen UI sementara untuk item yang masih dimuat (tidak berfungsi). Elemen sementara kemudian diganti secara progresif dengan UI aktual sebagai pemuatan data.

Selain itu—di mana pun Anda memuat data dari (disk lokal, jaringan, atau cloud)—pengguna dapat menggeser/menggulir ListView atau GridView dengan cepat sehingga tidak dimungkinkan untuk merender setiap item dengan keakuratan penuh sambil mempertahankan panning/gulir halus. Untuk mempertahankan pengalihan/pengguliran yang halus, Anda dapat memilih untuk merender item dalam beberapa fase selain menggunakan tempat penampung.

Contoh teknik ini sering terlihat di aplikasi tampilan foto: meskipun tidak semua gambar telah dimuat dan ditampilkan, pengguna masih dapat menggeser/menggulir dan berinteraksi dengan koleksi. Atau, untuk item "film", Anda dapat menampilkan judul pada fase pertama, peringkat di fase kedua, dan gambar poster di fase ketiga. Pengguna melihat data terpenting tentang setiap item sedini mungkin, dan itu berarti mereka dapat mengambil tindakan sekaligus. Kemudian info yang kurang penting diisi sesuai waktu yang memungkinkan. Berikut adalah fitur platform yang dapat Anda gunakan untuk mengimplementasikan teknik ini.

Tempat penampung

Fitur visual tempat penampung sementara aktif secara default, dan dikontrol dengan properti ShowScrollingPlaceholders. Selama panning/scrolling cepat, fitur ini memberi pengguna petunjuk visual bahwa ada lebih banyak item yang belum sepenuhnya ditampilkan sambil juga mempertahankan kehalusan. Jika Anda menggunakan salah satu teknik di bawah ini maka Anda dapat mengatur ShowScrollingPlaceholders ke false jika Anda lebih suka tidak memiliki tempat penampung render sistem.

Pembaruan templat data progresif menggunakan x:Phase

Berikut cara menggunakan atribut x:Phase dengan pengikatan {x:Bind} untuk menerapkan pembaruan templat data progresif.

  1. Berikut tampilan sumber pengikatan (ini adalah sumber data yang akan kami ikat).

    namespace LotsOfItems
    {
        public class ExampleItem
        {
            public string Title { get; set; }
            public string Subtitle { get; set; }
            public string Description { get; set; }
        }
    
        public class ExampleItemViewModel
        {
            private ObservableCollection<ExampleItem> exampleItems = new ObservableCollection<ExampleItem>();
            public ObservableCollection<ExampleItem> ExampleItems { get { return this.exampleItems; } }
    
            public ExampleItemViewModel()
            {
                for (int i = 1; i < 150000; i++)
                {
                    this.exampleItems.Add(new ExampleItem(){
                        Title = "Title: " + i.ToString(),
                        Subtitle = "Sub: " + i.ToString(),
                        Description = "Desc: " + i.ToString()
                    });
                }
            }
        }
    }
    
  2. Berikut markup yang DeferMainPage.xaml berisi. Tampilan kisi berisi templat item dengan elemen yang terikat ke properti Judul, Subtitel, dan Deskripsi kelas MyItem. Perhatikan bahwa x:Fase default ke 0. Di sini, item awalnya akan dirender hanya dengan judul yang terlihat. Kemudian elemen subtitel akan terikat data dan dibuat terlihat untuk semua item dan sebagainya sampai semua fase telah diproses.

    <Page
        x:Class="LotsOfItems.DeferMainPage"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:lotsOfItems="using:LotsOfItems"
        mc:Ignorable="d">
    
        <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
            <GridView ItemsSource="{x:Bind ViewModel.ExampleItems}">
                <GridView.ItemTemplate>
                    <DataTemplate x:DataType="lotsOfItems:ExampleItem">
                        <StackPanel Height="100" Width="100" Background="OrangeRed">
                            <TextBlock Text="{x:Bind Title}"/>
                            <TextBlock Text="{x:Bind Subtitle}" x:Phase="1"/>
                            <TextBlock Text="{x:Bind Description}" x:Phase="2"/>
                        </StackPanel>
                    </DataTemplate>
                </GridView.ItemTemplate>
            </GridView>
        </Grid>
    </Page>
    
  3. Jika Anda menjalankan aplikasi sekarang dan menggeser/menggulir dengan cepat melalui tampilan kisi maka Anda akan melihat bahwa saat setiap item baru muncul di layar, pada awalnya dirender sebagai persegi panjang abu-abu gelap (berkat properti ShowScrollingPlaceholders yang default ke true), maka judul muncul, diikuti dengan subtitel, diikuti oleh deskripsi.

Pembaruan templat data progresif menggunakan ContainerContentChanging

Strategi umum untuk peristiwa ContainerContentChanging adalah menggunakan Opacity untuk menyembunyikan elemen yang tidak perlu segera terlihat. Ketika elemen didaur ulang, elemen tersebut akan mempertahankan nilai lamanya sehingga kami ingin menyembunyikan elemen-elemen tersebut sampai kami memperbarui nilai-nilai tersebut dari item data baru. Kami menggunakan properti Fase pada argumen peristiwa untuk menentukan elemen mana yang akan diperbarui dan ditampilkan. Jika fase tambahan diperlukan, kami mendaftarkan panggilan balik.

  1. Kita akan menggunakan sumber pengikatan yang sama seperti untuk x:Phase.

  2. Berikut markup yang MainPage.xaml berisi. Tampilan kisi mendeklarasikan handler ke peristiwa ContainerContentChanging-nya, dan berisi templat item dengan elemen yang digunakan untuk menampilkan properti Judul, Subtitel, dan Deskripsi kelas MyItem. Untuk mendapatkan manfaat performa maksimum menggunakan ContainerContentChanging, kami tidak menggunakan pengikatan dalam markup tetapi kami justru menetapkan nilai secara terprogram. Pengecualian di sini adalah elemen yang menampilkan judul, yang kami anggap berada di fase 0.

    <Page
        x:Class="LotsOfItems.MainPage"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:lotsOfItems="using:LotsOfItems"
        mc:Ignorable="d">
    
        <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
            <GridView ItemsSource="{x:Bind ViewModel.ExampleItems}" ContainerContentChanging="GridView_ContainerContentChanging">
                <GridView.ItemTemplate>
                    <DataTemplate x:DataType="lotsOfItems:ExampleItem">
                        <StackPanel Height="100" Width="100" Background="OrangeRed">
                            <TextBlock Text="{x:Bind Title}"/>
                            <TextBlock Opacity="0"/>
                            <TextBlock Opacity="0"/>
                        </StackPanel>
                    </DataTemplate>
                </GridView.ItemTemplate>
            </GridView>
        </Grid>
    </Page>
    
  3. Terakhir, berikut adalah implementasi penanganan aktivitas ContainerContentChanging . Kode ini juga menunjukkan cara kami menambahkan properti jenis RecordingViewModel ke MainPage untuk mengekspos kelas sumber pengikatan dari kelas yang mewakili halaman markup kami. Selama Anda tidak memiliki pengikatan {Binding} dalam templat data Anda, lalu tandai objek argumen peristiwa seperti yang ditangani pada fase pertama handler untuk mengisyaratkan ke item bahwa ia tidak perlu mengatur konteks data.

    namespace LotsOfItems
    {
        /// <summary>
        /// An empty page that can be used on its own or navigated to within a Frame.
        /// </summary>
        public sealed partial class MainPage : Page
        {
            public MainPage()
            {
                this.InitializeComponent();
                this.ViewModel = new ExampleItemViewModel();
            }
    
            public ExampleItemViewModel ViewModel { get; set; }
    
            // Display each item incrementally to improve performance.
            private void GridView_ContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
            {
                if (args.Phase != 0)
                {
                    throw new System.Exception("We should be in phase 0, but we are not.");
                }
    
                // It's phase 0, so this item's title will already be bound and displayed.
    
                args.RegisterUpdateCallback(this.ShowSubtitle);
    
                args.Handled = true;
            }
    
            private void ShowSubtitle(ListViewBase sender, ContainerContentChangingEventArgs args)
            {
                if (args.Phase != 1)
                {
                    throw new System.Exception("We should be in phase 1, but we are not.");
                }
    
                // It's phase 1, so show this item's subtitle.
                var templateRoot = args.ItemContainer.ContentTemplateRoot as StackPanel;
                var textBlock = templateRoot.Children[1] as TextBlock;
                textBlock.Text = (args.Item as ExampleItem).Subtitle;
                textBlock.Opacity = 1;
    
                args.RegisterUpdateCallback(this.ShowDescription);
            }
    
            private void ShowDescription(ListViewBase sender, ContainerContentChangingEventArgs args)
            {
                if (args.Phase != 2)
                {
                    throw new System.Exception("We should be in phase 2, but we are not.");
                }
    
                // It's phase 2, so show this item's description.
                var templateRoot = args.ItemContainer.ContentTemplateRoot as StackPanel;
                var textBlock = templateRoot.Children[2] as TextBlock;
                textBlock.Text = (args.Item as ExampleItem).Description;
                textBlock.Opacity = 1;
            }
        }
    }
    
  4. Jika Anda menjalankan aplikasi sekarang dan menggeser/menggulir dengan cepat melalui tampilan kisi, Anda akan melihat perilaku yang sama seperti untuk x:Phase.

Daur ulang kontainer dengan koleksi heterogen

Dalam beberapa aplikasi, Anda harus memiliki UI yang berbeda untuk berbagai jenis item dalam koleksi. Ini dapat menciptakan situasi di mana tidak mungkin bagi panel virtualisasi untuk menggunakan kembali/mendaur ulang elemen visual yang digunakan untuk menampilkan item. Membuat ulang elemen visual untuk item selama panning membatalkan banyak kemenangan performa yang disediakan oleh virtualisasi. Namun, sedikit perencanaan dapat memungkinkan panel virtualisasi untuk menggunakan kembali elemen. Pengembang memiliki beberapa opsi tergantung pada skenarionya: peristiwa MemilihItemContainer , atau pemilih templat item. Pendekatan ChoosingItemContainer memiliki performa yang lebih baik.

Peristiwa MemilihItemContainer

MemilihItemContainer adalah peristiwa yang memungkinkan Anda menyediakan item (ListViewItem/GridViewItem) ke ListView/GridView setiap kali item baru diperlukan selama start-up atau daur ulang. Anda dapat membuat kontainer berdasarkan jenis item data yang akan ditampilkan kontainer (ditunjukkan pada contoh di bawah). MemilihItemContainer adalah cara berkinerja lebih tinggi untuk menggunakan templat data yang berbeda untuk item yang berbeda. Penembolokan kontainer adalah sesuatu yang dapat dicapai menggunakan ChoosingItemContainer. Misalnya, jika Anda memiliki lima templat yang berbeda, dengan satu templat terjadi urutan besaran lebih sering daripada yang lain, maka MemilihItemContainer memungkinkan Anda tidak hanya membuat item dengan rasio yang diperlukan tetapi juga untuk menjaga jumlah elemen yang sesuai di-cache dan tersedia untuk daur ulang. MemilihGroupHeaderContainer menyediakan fungsionalitas yang sama untuk header grup.

// Example shows how to use ChoosingItemContainer to return the correct
// DataTemplate when one is available. This example shows how to return different 
// data templates based on the type of FileItem. Available ListViewItems are kept
// in two separate lists based on the type of DataTemplate needed.
private void ListView_ChoosingItemContainer
    (ListViewBase sender, ChoosingItemContainerEventArgs args)
{
    // Determines type of FileItem from the item passed in.
    bool special = args.Item is DifferentFileItem;

    // Uses the Tag property to keep track of whether a particular ListViewItem's 
    // datatemplate should be a simple or a special one.
    string tag = special ? "specialFiles" : "simpleFiles";

    // Based on the type of datatemplate needed return the correct list of 
    // ListViewItems, this could have also been handled with a hash table. These 
    // two lists are being used to keep track of ItemContainers that can be reused.
    List<UIElement> relevantStorage = special ? specialFileItemTrees : simpleFileItemTrees;

    // args.ItemContainer is used to indicate whether the ListView is proposing an 
    // ItemContainer (ListViewItem) to use. If args.Itemcontainer, then there was a 
    // recycled ItemContainer available to be reused.
    if (args.ItemContainer != null)
    {
        // The Tag is being used to determine whether this is a special file or 
        // a simple file.
        if (args.ItemContainer.Tag.Equals(tag))
        {
            // Great: the system suggested a container that is actually going to 
            // work well.
        }
        else
        {
            // the ItemContainer's datatemplate does not match the needed 
            // datatemplate.
            args.ItemContainer = null;
        }
    }

    if (args.ItemContainer == null)
    {
        // see if we can fetch from the correct list.
        if (relevantStorage.Count > 0)
        {
            args.ItemContainer = relevantStorage[0] as SelectorItem;
        }
        else
        {
            // there aren't any (recycled) ItemContainers available. So a new one 
            // needs to be created.
            ListViewItem item = new ListViewItem();
            item.ContentTemplate = this.Resources[tag] as DataTemplate;
            item.Tag = tag;
            args.ItemContainer = item;
        }
    }
}

Pemilih templat item

Pemilih templat item (DataTemplateSelector) memungkinkan aplikasi mengembalikan templat item yang berbeda saat runtime berdasarkan jenis item data yang akan ditampilkan. Ini membuat pengembangan lebih produktif, tetapi membuat virtualisasi UI lebih sulit karena tidak setiap templat item dapat digunakan kembali untuk setiap item data.

Saat mendaur ulang item (ListViewItem/GridViewItem), kerangka kerja harus memutuskan apakah item yang tersedia untuk digunakan dalam antrean daur ulang (antrean daur ulang adalah cache item yang saat ini tidak digunakan untuk menampilkan data) memiliki templat item yang akan cocok dengan yang diinginkan oleh item data saat ini. Jika tidak ada item dalam antrean daur ulang dengan templat item yang sesuai, maka item baru dibuat, dan templat item yang sesuai dibuat untuknya. Jika, di sisi lain, antrean daur ulang berisi item dengan templat item yang sesuai, item tersebut dihapus dari antrean daur ulang dan digunakan untuk item data saat ini. Pemilih templat item berfungsi dalam situasi di mana hanya sejumlah kecil templat item yang digunakan dan ada distribusi datar di seluruh kumpulan item yang menggunakan templat item yang berbeda.

Ketika ada distribusi item yang tidak merata yang menggunakan templat item yang berbeda, templat item baru kemungkinan perlu dibuat selama panning, dan ini meniadakan banyak keuntungan yang disediakan oleh virtualisasi. Selain itu, pemilih templat item hanya mempertimbangkan lima kandidat yang mungkin saat mengevaluasi apakah kontainer tertentu dapat digunakan kembali untuk item data saat ini. Jadi, Anda harus mempertimbangkan dengan cermat apakah data Anda sesuai untuk digunakan dengan pemilih templat item sebelum menggunakannya di aplikasi Anda. Jika koleksi Anda sebagian besar homogen, pemilih mengembalikan jenis yang sama paling banyak (mungkin semua) dari waktu. Ketahuilah harga yang Anda bayar untuk pengecualian langka untuk homogenitas tersebut, dan pertimbangkan apakah menggunakan ChoosingItemContainer (atau kontrol dua item) lebih disukai.