Optimalizace výkonu ListView a GridView

Poznámka Další podrobnosti najdete ve relaci //build/ "Výrazně zvyšte výkon, když uživatelé pracují s velkými objemy dat v GridView a ListView".

Vylepšení výkonu a doby spouštění ListView a GridView prostřednictvím virtualizace uživatelského rozhraní, redukce prvků a postupné aktualizace položek. Techniky virtualizace dat naleznete v tématu ListView a GridView virtualizace dat pro WinUI.

Dva klíčové faktory výkonu kolekce

Manipulace s kolekcemi je běžný scénář. Prohlížeč fotografií má kolekce fotek, čtenář má kolekce článků, knih nebo příběhů a nákupní aplikace obsahuje kolekce produktů. Toto téma ukazuje, co můžete udělat, aby aplikace WinUI byla efektivní při manipulaci s kolekcemi.

Pokud jde o kolekce, existují dva klíčové faktory výkonu: jednou je doba strávená vláknem uživatelského rozhraní, která vytváří položky; druhá je paměť používaná nezpracovanou sadou dat i prvky uživatelského rozhraní sloužícími k vykreslení dat.

Pro hladké posouvání a procházení je důležité, aby vlákno uživatelského rozhraní efektivně a chytře provádělo vytváření instancí, vazbu dat a rozložení položek.

Virtualizace uživatelského rozhraní

Virtualizace uživatelského rozhraní je nejdůležitějším vylepšením, které můžete udělat. To znamená, že prvky uživatelského rozhraní představující položky se vytvářejí na vyžádání. U ovládacího prvku položky vázaného na kolekci 1000 položek by to bylo plýtvání prostředky k vytvoření uživatelského rozhraní pro všechny položky najednou, protože se nedají zobrazit ve stejnou dobu. ListView a GridView (a další standardní ovládací prvky odvozené od ItemsControl) provádějí virtualizaci uživatelského rozhraní za vás. Když se položky blíží posouvání do zobrazení (několik stránek pryč), rámec vygeneruje uživatelské rozhraní pro položky a uloží je do mezipaměti. Pokud je nepravděpodobné, že se položky znovu zobrazí, architektura uvolní paměť.

Pokud zadáte vlastní šablonu panelu položek (viz ItemsPanel), ujistěte se, že používáte virtualizační panel, například ItemsWrapGrid nebo ItemsStackPanel. Pokud používáte VariableSizedWrapGrid, WrapGrid nebo StackPanel, nebudete dostávat virtualizaci. Kromě toho jsou vyvolány následující Události ListView pouze při použití ItemsWrapGrid nebo ItemsStackPanel: ChoosingGroupHeaderContainer, ChoosingItemContainer a ContainerContentChanging. Pro vlastní rozložení v sadě Windows App SDK je moderní ekvivalent implementace založená na virtualizingLayout, pokud předdefinované panely položek nevyhovují vašim potřebám.

Koncept oblasti zobrazení je pro virtualizaci uživatelského rozhraní kritický, protože architektura musí vytvořit prvky, které se budou pravděpodobně zobrazovat. Obecně platí, že oblast zobrazení ItemsControl je rozsah logického ovládacího prvku. Například oblast zobrazení ListView je šířka a výška ListView elementu. Některé panely umožňují podřízeným prvkům neomezený prostor, například ScrollViewer a Mřížka s řádky nebo sloupci s automatickou velikostí. Když je virtualizovaný ItemsControl umístěn do panelu tohoto typu, potřebuje tolik místa, že to umožní zobrazení všech jeho položek, což znemožní virtualizaci. Obnovte virtualizaci nastavením šířky a výšky na ItemsControl.

Snížení počtu prvků na položku

Udržujte počet prvků uživatelského rozhraní, které se používají k vykreslení položek na přiměřené minimum.

Při prvním zobrazení ovládacího prvku položky se vytvoří všechny prvky potřebné k vykreslení oblasti zobrazení plné položek. Jakmile se položky přiblíží k oblasti zobrazení, framework také aktualizuje prvky uživatelského rozhraní v šablonách položek v mezipaměti pomocí dat z připojených datových objektů. Minimalizace složitosti značení uvnitř šablon se vyplatí v paměti a čase stráveném na vlákně uživatelského rozhraní, což zlepšuje rychlost odezvy zejména při posouvání a scrollování. Příslušné šablony jsou šablona položky (viz ItemTemplate) a šablona ovládacího prvku ListViewItem nebo GridViewItem (šablona ovládacího prvku položky nebo ItemContainerStyle). Výhoda i malého snížení počtu prvků se vynásobí počtem zobrazených položek.

Příklady snížení počtu prvků najdete v tématu Optimalizace načítání XAML pro WinUI a Sadu Windows App SDK.

Výchozí šablony ovládacích prvků pro ListViewItem a GridViewItem obsahují ListViewItemPresenter element. Tento prvek je jednoduchý optimalizovaný komponent, který zobrazuje složité vizuální prvky pro zaostření, výběr a další vizuální stavy. Pokud už máte vlastní šablony ovládacích prvků položek (ItemContainerStyle), nebo pokud v budoucnu upravujete kopii šablony ovládacího prvku položky, doporučujeme použít ListViewItemPresenter , protože tento prvek poskytuje optimální rovnováhu mezi výkonem a přizpůsobitelností ve většině případů. Prezentujícího přizpůsobíte nastavením vlastností. Například, zde je značkovací kód, který odebere zaškrtnutí, které je ve výchozím nastavení viditelné po výběru položky, a změní barvu pozadí vybrané položky na oranžovou.

...
<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>
<!-- ... -->

Existuje asi 25 vlastností s názvy, které se samy popisují, jako například SelectionCheckMarkVisualEnabled a SelectedBackground. Pokud typy prezentérů nejsou dostatečně přizpůsobitelné pro váš případ použití, můžete místo toho upravit kopii šablony ovládacího prvku ListViewItemExpanded nebo GridViewItemExpanded. V aplikaci WinUI se podívejte na generic.xaml soubor, který se dodává s balíčkem Windows App SDK pro aktuální výchozí šablony. Mějte na paměti, že použití těchto šablon může znamenat kompromis v oblasti výkonu za účelem zvýšené přizpůsobitelnosti.

Postupné aktualizace položek ListView a GridView

Pokud používáte virtualizaci dat, můžete zachovat rychlost odezvy ListView a GridView tak, že nakonfigurujete ovládací prvek tak, aby vykreslovat dočasné prvky uživatelského rozhraní pro položky, které se stále stahují. Dočasné prvky se pak postupně nahradí skutečným uživatelským rozhraním při načítání dat.

Také – bez ohledu na to, zda načítáte data z místního disku, sítě, nebo cloudu – uživatel může posouvat nebo rolovat ListView nebo GridView tak rychle, že není možné vykreslit každou položku s plnou přesností při zachování plynulého pohybu. Pokud chcete zachovat hladké posouvání a rolování, můžete se kromě použití zástupných symbolů rozhodnout vykreslit položku ve více fázích.

Příkladem těchto technik je často vidět v aplikacích pro prohlížení fotek: i když nebyly načteny a zobrazeny všechny obrázky, uživatel může stále posouvat, rolovat a interakci s kolekcí. Nebo můžete u položky filmu zobrazit název v první fázi, hodnocení ve druhé fázi a obrázek plakátu ve třetí fázi. Uživatel uvidí nejdůležitější data o každé položce co nejdříve a to znamená, že může provést akci najednou. Méně důležité informace se vyplní, jak čas dovolí. Tady jsou funkce platformy, které můžete použít k implementaci těchto technik.

Zástupné symboly

Funkce dočasných zástupných vizuálů je ve výchozím nastavení zapnutá a řídí se vlastností ShowsScrollingPlaceholders . Během rychlého posouvání a panorámování tato funkce uživateli naznačuje, že je k dispozici více položek k zobrazení a zároveň zachovává plynulost. Pokud použijete některou z níže uvedených technik, můžete nastavit ShowsScrollingPlaceholders na false, pokud nechcete, aby systém vykresloval zástupné symboly.

Progresivní aktualizace šablon dat pomocí x:Phase

Atribut x:Phase pokračuje v práci ve WinUI a zůstává dobrým způsobem, jak postupně vykreslit obsah položky.

Tady je postup použití atributu x:Phase s vazbami {x:Bind} k implementaci progresivních aktualizací šablon dat.

  1. Takto vypadá zdroj vazby (toto je zdroj dat, ke kterému se budeme svázat).

    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. Tady je kód, který DeferMainPage.xaml obsahuje. Zobrazení mřížky obsahuje šablonu položky s prvky vázanými na vlastnosti Title, Subtitle a Description třídy MyItem . Všimněte si, že výchozí hodnota x:Phase je 0. Tady se položky zpočátku vykreslují jenom s viditelným názvem. Potom je prvek titulku svázán s daty a zobrazen pro všechny položky, a stejným způsobem pokračuje, dokud neproběhnou všechny fáze.

    <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. Pokud teď aplikaci spustíte a posunete nebo se rychle posunete v zobrazení mřížky, všimnete si, že když se každá nová položka zobrazí na obrazovce, zobrazí se nejprve jako tmavě šedý obdélník (díky vlastnosti ShowScrollingPlaceholders výchozí hodnota true), zobrazí se název následovaný podnadpisem a popisem.

Progresivní aktualizace šablon dat pomocí ContainerContentChanging

Obecná strategie pro událost ContainerContentChanging je použití neprůhlednosti ke skrytí prvků, které nemusí být okamžitě viditelné. Když jsou prvky recyklovány, zachovávají si jejich staré hodnoty, takže tyto prvky chceme skrýt, dokud tyto hodnoty neaktualizujeme z nové datové položky. Vlastnost Phase pro argumenty události používáme k určení prvků, které se mají aktualizovat a zobrazit. Pokud jsou potřeba další fáze, zaregistrujeme zpětné volání.

  1. Použijeme stejný zdroj vazby jako pro x:Phase.

  2. Tady je kód, který MainPage.xaml obsahuje. Zobrazení mřížky deklaruje obslužnou rutinu pro událost ContainerContentChanging a obsahuje šablonu položky s prvky použitými k zobrazení vlastnosti Title, Subtitle a Description třídy MyItem . Abychom získali maximální výhody výkonu při používání ContainerContentChanging, nepoužíváme vazby v kódu a místo toho přiřazujeme hodnoty programově. Výjimkou je prvek, který zobrazuje název, který považujeme za fázi 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. Nakonec zde je implementace obslužné rutiny události ContainerContentChanging . Tento kód také ukazuje, jak přidáme vlastnost typu ExampleItemViewModel do MainPage, abychom zviditelnili zdrojovou vazbu z třídy, která představuje naši stránku značek. Pokud v šabloně dat nemáte žádné vazby{Binding}, označte objekt argumentů události jako vyřízený v první fázi obslužné rutiny, aby položka věděla, že nemusí nastavovat kontext dat.

    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. Pokud teď aplikaci spustíte a posunete nebo se rychle posunete v zobrazení mřížky, uvidíte stejné chování jako u x:Phase.

Recyklace kontejnerů s heterogenními kolekcemi

V některých aplikacích musíte mít jiné uživatelské rozhraní pro různé typy položek v kolekci. To může vytvořit situaci, kdy není možné virtualizovat panely opakovaně nebo recyklovat vizuální prvky použité k zobrazení položek. Opětovné vykreslování vizuálních prvků pro položku během posouvání zvrátí mnohá zvýšení výkonu poskytovaná virtualizací. Trocha plánování ale může umožnit virtualizaci panelů pro opětovné použití prvků. Vývojáři mají několik možností v závislosti na svém scénáři: událost ChoosingItemContainer nebo selektor šablony položky. Přístup ChoosingItemContainer má lepší výkon.

Událost ChoosingItemContainer

ChoosingItemContainer je událost, která umožňuje zadat položku (ListViewItem nebo GridViewItem) ListView nebo GridView vždy, když je potřeba při spuštění nebo recyklaci nové položky. Kontejner můžete vytvořit na základě typu datové položky, která se kontejner zobrazí, jak je znázorněno v následujícím příkladu. VolbaItemContainer je výkonnější způsob, jak používat různé šablony dat pro různé položky. Ukládání kontejnerů do mezipaměti je něco, čeho lze dosáhnout pomocí ChoosingItemContainer. Pokud máte například pět různých šablon, přičemž jedna šablona se vyskytuje mnohem častěji než ostatní, pak ChoosingItemContainer umožňuje nejen vytvářet položky v požadovaných poměrech, ale také udržovat odpovídající počet prvků uložených v mezipaměti a k dispozici pro recyklaci. VýběrGroupHeaderContainer poskytuje stejnou funkčnost pro záhlaví skupin.

// 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 is not null, 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;
        }
    }
}

Selektor šablony položek

Selektor šablony položky (DataTemplateSelector) umožňuje aplikaci vrátit jinou šablonu položky za běhu na základě typu datové položky, která se zobrazí. Díky tomu je vývoj produktivnější, ale virtualizace uživatelského rozhraní je obtížnější, protože ne všechny šablony položek je možné znovu použít pro každou položku dat.

Při recyklaci položky (ListViewItem nebo GridViewItem) musí architektura rozhodnout, jestli položky, které jsou k dispozici pro použití ve frontě recyklace, mají šablonu položky, která bude odpovídat té, kterou aktuální datová položka vyžaduje. Pokud ve frontě recyklace nejsou žádné položky s příslušnou šablonou položky, vytvoří se nová položka a instancuje se pro ni příslušná šablona položky. Pokud na druhé straně fronta recyklace obsahuje položku s příslušnou šablonou položky, pak se tato položka odebere z fronty recyklace a použije se pro aktuální položku dat. Selektor šablony položek funguje v situacích, kdy se používá pouze malý počet šablon položek a v celé kolekci položek, které používají různé šablony položek, existuje plochá distribuce.   Pokud existuje nerovnoměrná distribuce položek, které používají různé šablony položek, bude pravděpodobně potřeba vytvořit nové šablony položek během posouvání a tím se neguje mnoho výhod poskytovaných virtualizací. Kromě toho selektor šablony položek při vyhodnocování toho, zda je možné použít konkrétní kontejner pro aktuální datovou položku, považuje pouze pět možných kandidátů. Proto byste měli pečlivě zvážit, jestli jsou vaše data vhodná pro použití selektorem šablony položek, než je použijete v aplikaci WinUI. Pokud je kolekce většinou homogenní, selektor vrací stejný typ nejčastěji nebo vždy. Jen si uvědomte cenu, kterou platíte za vzácné výjimky této homogenity, a zvažte, zda dáte přednost použití ChoosingItemContainer nebo dvou řídicích prvků pro položky.