Dela via


Optimera Prestanda för ListView och GridView

Observera Mer information finns i sessionen //build/ Dramatiskt öka prestanda när användare interagerar med stora mängder data i GridView och ListView.

Förbättra Prestanda och starttid för ListView och GridView genom virtualisering av användargränssnittet, minskning av element och progressiv uppdatering av objekt. Datavirtualiseringstekniker finns i ListView- och GridView-datavirtualisering för WinUI.

Två viktiga faktorer i insamlingsprestanda

Att manipulera samlingar är ett vanligt scenario. En fotovisare har samlingar av foton, en läsare har samlingar av artiklar, böcker eller berättelser, och en shoppingapp har samlingar av produkter. Det här avsnittet visar vad du kan göra för att göra WinUI-appen effektiv när du manipulerar samlingar.

Det finns två viktiga faktorer i prestanda när det gäller samlingar: en är den tid som används av användargränssnittstråden som skapar objekt; Det andra är det minne som används av både rådatauppsättningen och de gränssnittselement som används för att återge dessa data.

För smidig panorering och rullning är det viktigt att användargränssnittstråden gör ett effektivt och smart jobb med att instansiera, databindning och lägga ut objekt.

Virtualisering av användargränssnitt

Virtualisering av användargränssnittet är den viktigaste förbättringen du kan göra. Det innebär att gränssnittselement som representerar objekten skapas på begäran. För en objektkontroll som är bunden till en samling med 1 000 objekt skulle det vara slöseri med resurser att skapa användargränssnittet för alla objekt samtidigt eftersom alla inte kan visas samtidigt. ListView och GridView (och andra vanliga ItemsControl-härledda kontroller) utför UI-virtualisering åt dig. När objekt är nära att rullas in i vyn (några sidor bort) genererar ramverket användargränssnittet för objekten och cachelagrar dem. När det är osannolikt att objekten visas igen återtar ramverket minnet.

Om du anger en panelmall för anpassade objekt (se ItemsPanel) kontrollerar du att du använder en virtualiseringspanel som ItemsWrapGrid eller ItemsStackPanel. Om du använder VariableSizedWrapGrid, WrapGrid eller StackPanel får du inte virtualisering. Dessutom utlöses följande ListView-händelser endast när du använder ett ItemsWrapGrid eller en ItemsStackPanel: ChoosingGroupHeaderContainer, ChoosingItemContainer och ContainerContentChanging. För anpassade layouter i Windows App SDK är den moderna motsvarigheten en VirtualizingLayout-baserad implementering när de inbyggda objektpanelerna inte uppfyller dina behov.

Begreppet viewport är viktigt för användargränssnittsvirtualisering eftersom ramverket måste skapa de element som sannolikt kommer att visas. I allmänhet är visningsporten för en ItemsControl omfattningen av den logiska kontrollen. Visningsporten för en ListView är till exempel bredden och höjden på ListView-elementet . Vissa paneler tillåter obegränsat utrymme för underordnade element, till exempel ScrollViewer och ett rutnät med rader eller kolumner i automatisk storlek. När en virtualiserad ItemsControl placeras i en panel som den, krävs det tillräckligt med utrymme för att visa alla dess objekt, vilket förhindrar virtualisering. Återställ virtualisering genom att ange en bredd och höjd på ItemsControl.

Minskning av element per objekt

Behåll antalet gränssnittselement som används för att återge dina objekt till ett rimligt minimum.

När en objektkontroll först visas skapas alla element som behövs för att återge en visningsport full av objekt. När objekt närmar sig viewport uppdaterar ramverket även användargränssnittselementen i cachelagrade objektmallar med bundna dataobjekt. Att minimera komplexiteten i markeringen i mallar lönar sig i minnet och i tiden som spenderas på användargränssnittstråden, vilket förbättrar svarstiden, särskilt vid panorering och rullning. Mallarna i fråga är objektmallen (se ItemTemplate) och kontrollmallen för en ListViewItem eller en GridViewItem (objektkontrollmallen eller ItemContainerStyle). Fördelen med även en liten minskning av antalet element multipliceras med antalet objekt som visas.

Exempel på elementminskning finns i Optimera XAML-inläsning för WinUI och Windows App SDK.

Standardkontrollmallarna för ListViewItem och GridViewItem innehåller ett ListViewItemPresenter-element . Den här presentatören är ett enda optimerat element som visar komplexa visuella objekt för fokus, val och andra visuella tillstånd. Om du redan har anpassade objektkontrollmallar (ItemContainerStyle), eller om du i framtiden redigerar en kopia av en objektkontrollmall, rekommenderar vi att du använder en ListViewItemPresenter eftersom det elementet ger dig en optimal balans mellan prestanda och anpassningsbarhet i de flesta fall. Du anpassar presentatören genom att ange egenskaper för den. Här är till exempel en markering som tar bort bockmarkeringen som visas som standard när ett objekt markeras och ändrar bakgrundsfärgen för det markerade objektet till orange.

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

Det finns cirka 25 egenskaper med självbeskrivande namn som liknar SelectionCheckMarkVisualEnabled och SelectedBackground. Om det visar sig att föredragshållartyperna inte är tillräckligt anpassningsbara för ditt användningsfall, kan du i stället redigera en kopia av kontrollmallen ListViewItemExpanded eller GridViewItemExpanded. I en WinUI-app tittar du på generic.xaml filen som levereras med Windows App SDK-paketet för de aktuella standardmallarna. Tänk på att användning av dessa mallar innebär att vissa prestanda kan bytas ut mot ökad anpassning.

Uppdatera ListView- och GridView-objekt progressivt

Om du använder datavirtualisering kan du hålla Svarstiden för ListView och GridView hög genom att konfigurera kontrollen för att återge tillfälliga gränssnittselement för objekt som fortfarande laddas ned. De tillfälliga elementen ersätts sedan progressivt med det faktiska användargränssnittet när data läses in.

Oavsett var du läser in data från (lokal disk, nätverk eller moln) kan en användare panorera eller rulla en ListView eller GridView så snabbt att det inte går att återge varje objekt med fullständig återgivning samtidigt som smidig panorering och rullning bevaras. För att bevara smidig panorering och rullning kan du välja att återge ett objekt i flera faser utöver att använda platshållare.

Ett exempel på dessa tekniker visas ofta i appar för fotovisning: även om inte alla bilder har lästs in och visats kan användaren fortfarande panorera, rulla och interagera med samlingen. Eller för ett filmobjekt kan du visa titeln i den första fasen, klassificeringen i den andra fasen och en bild av affischen i den tredje fasen. Användaren ser de viktigaste data om varje objekt så tidigt som möjligt, och det innebär att de kan vidta åtgärder samtidigt. Sedan fylls den mindre viktiga informationen i när tiden tillåter. Här är de plattformsfunktioner som du kan använda för att implementera dessa tekniker.

Platshållare

Funktionen för temporära visuella platshållare är aktiverad som standard och styrs med egenskapen ShowsScrollingPlaceholders . Under snabb panorering och rullning ger den här funktionen användaren ett visuellt tips om att det finns fler objekt som ännu inte har visats helt samtidigt som jämnheten bevaras. Om du använder någon av metoderna nedan kan du ställa in ShowsScrollingPlaceholders till false om du föredrar att systemet inte återger platshållarna.

Uppdateringar av progressiva datamallar med x:Phase

Attributet x:Phase fortsätter att fungera i WinUI och är fortfarande ett bra sätt att progressivt återge objektinnehåll.

Så här använder du x:Phase-attributet med {x:Bind} -bindningar för att implementera uppdateringar av progressiva datamallar.

  1. Så här ser bindningskällan ut (det här är den datakälla som vi ska binda till).

    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. Här är markupen som DeferMainPage.xaml innehåller. Rutnätsvyn innehåller en objektmall med element som är bundna till egenskaperna Rubrik, Underrubrik och Beskrivning för klassen MyItem . Observera att x:Phase är standardvärdet 0. Här återges objekt från början med bara rubriken synlig. Sedan är undertextelementet databundet och synligt för alla objekt och så vidare tills alla faser har bearbetats.

    <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. Om du kör appen nu och panorerar eller rullar snabbt genom rutnätsvyn ser du att när varje nytt objekt visas på skärmen återges det först som en mörkgrå rektangel (tack vare egenskapen ShowsScrollingPlaceholders som standard till true) visas rubriken följt av underrubrik följt av beskrivning.

Progressiva datamalluppdateringar med ContainerContentChanging

Den allmänna strategin för containerContentChanging-händelsen är att använda Opacitet för att dölja element som inte behöver vara omedelbart synliga. När elementen återanvänds behåller de sina gamla värden, så vi vill dölja dessa element tills vi har uppdaterat dessa värden från det nya dataobjektet. Vi använder egenskapen Fas på händelseargumenten för att avgöra vilka element som ska uppdateras och visas. Om ytterligare faser behövs registrerar vi ett återanrop.

  1. Vi använder samma bindningskälla som för x:Phase.

  2. Det här är märkupen som MainPage.xaml innehåller. Rutnätsvyn deklarerar en hanterare för händelsen ContainerContentChanging och innehåller en objektmall med element som används för att visa egenskaperna Rubrik, Underrubrik och Beskrivning för klassen MyItem . För att få maximala prestandafördelar med att använda ContainerContentChanging använder vi inte bindningar i markering och tilldelar i stället värden programmatiskt. Undantaget här är elementet som visar rubriken, som vi anser vara i fas 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. Slutligen, här är implementeringen av ContainerContentChanging-händelsehanteraren . Den här koden visar också hur vi lägger till en egenskap av typen ExampleItemViewModel till MainPage för att exponera bindningskällans klass från klassen som representerar vår sida med markering. Så länge du inte har några {Binding} -bindningar i datamallen markerar du händelseargumentobjektet som det hanterades i den första fasen av hanteraren för att antyda för objektet att det inte behöver ange någon datakontext.

    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. Om du kör appen nu och panorerar eller bläddrar snabbt genom rutnätsvyn ser du samma beteende som för x:Phase.

Containeråtervinning med heterogena samlingar

I vissa program måste du ha olika användargränssnitt för olika typer av objekt i en samling. Detta kan skapa en situation där det är omöjligt för virtualiseringspaneler att återanvända eller återanvända de visuella element som används för att visa objekten. Om du återskapar de visuella elementen för ett objekt under panoreringen ångras många av de prestandavinster som tillhandahålls av virtualisering. Lite planering kan dock göra det möjligt för virtualiseringspaneler att återanvända elementen. Utvecklare har ett par alternativ beroende på deras scenario: händelsen ChoosingItemContainer eller en objektmallsväljare. Metoden ChoosingItemContainer har bättre prestanda.

Händelsen ChoosingItemContainer

Att väljaItemContainer är en händelse som gör att du kan ange ett objekt (ListViewItem eller GridViewItem) till ListView eller GridView när ett nytt objekt behövs vid start eller återvinning. Du kan skapa en container baserat på vilken typ av dataobjekt containern ska visa, enligt exemplet nedan. Att väljaItemContainer är det bästa sättet att använda olika datamallar för olika objekt. Cachelagring av containrar är något som kan uppnås med ChoosingItemContainer. Om du till exempel har fem olika mallar, där en mall inträffar i storleksordning oftare än de andra, kan du med ChoosingItemContainer inte bara skapa objekt i de förhållanden som behövs, utan även för att hålla ett lämpligt antal element cachelagrade och tillgängliga för återvinning. ChoosingGroupHeaderContainer har samma funktioner för grupprubriker.

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

Väljare för objektmall

Med en objektmallsväljare (DataTemplateSelector) kan en app returnera en annan objektmall vid körning baserat på vilken typ av dataobjekt som ska visas. Detta gör utvecklingen mer produktiv, men det gör virtualisering av användargränssnittet svårare eftersom inte alla objektmallar kan återanvändas för varje dataobjekt.

När du återanvänder ett objekt (ListViewItem eller GridViewItem) måste ramverket avgöra om de objekt som är tillgängliga för användning i papperskorgskön har en objektmall som matchar den som önskas av det aktuella dataobjektet. Om det inte finns några objekt i papperskorgskön med lämplig objektmall skapas ett nytt objekt och lämplig objektmall instansieras för det. Om papperskorgskön å andra sidan innehåller ett objekt med rätt objektmall tas objektet bort från papperskorgskön och används för det aktuella dataobjektet. En objektmallväljare fungerar i situationer där endast ett litet antal objektmallar används och det finns en platt fördelning i hela samlingen med objekt som använder olika objektmallar.   När det finns en ojämn fördelning av objekt som använder olika objektmallar, kan nya objektmallar behöva skapas under panoreringen, vilket negerar många av de fördelar som virtualisering erbjuder. Dessutom tar en objektmallsväljare bara hänsyn till fem möjliga kandidater när du utvärderar om en viss container kan återanvändas för det aktuella dataobjektet. Därför bör du noga överväga om dina data är lämpliga för användning med en objektmallsväljare innan du använder en i WinUI-appen. Om samlingen till största delen är homogen returnerar väljaren samma typ mest eller hela tiden. Tänk bara på det pris du betalar för de sällsynta undantagen till den homogeniteten och fundera på om det är bättre att använda ChoosingItemContainer eller två objektkontroller.