Partager via


Optimiser les performances listView et GridView

Note Pour plus d’informations, consultez la session //build/ considérablement augmenter les performances lorsque les utilisateurs interagissent avec de grandes quantités de données dans GridView et ListView.

Améliorez les performances de ListView et GridView et le temps de démarrage par le biais de la virtualisation de l’interface utilisateur, de la réduction des éléments et de la mise à jour progressive des éléments. Pour connaître les techniques de virtualisation des données, consultez La virtualisation des données ListView et GridView pour WinUI.

Deux facteurs clés dans les performances de collecte

La manipulation de collections est un scénario courant. Une visionneuse de photos a des collections de photos, un lecteur a des collections d’articles, de livres ou d’histoires, et une application shopping a des collections de produits. Cette rubrique montre ce que vous pouvez faire pour rendre votre application WinUI efficace lors de la manipulation de collections.

Il existe deux facteurs clés en matière de performances lorsqu’il s’agit de regroupements : l’un est le temps passé par le thread d’interface utilisateur créant des éléments ; l’autre est la mémoire utilisée par le jeu de données brut et les éléments d’interface utilisateur utilisés pour afficher ces données.

Pour un déplacement fluide et un défilement lisse, il est essentiel que le fil d'interface utilisateur effectue un travail efficace et intelligent pour instancier, lier les données et disposer les éléments.

Virtualisation de l’interface utilisateur

La virtualisation de l’interface utilisateur est l’amélioration la plus importante que vous pouvez apporter. Cela signifie que les éléments d’interface utilisateur représentant les éléments sont créés à la demande. Pour un contrôle d’éléments lié à une collection de 1 000 éléments, il s’agirait d’un gaspillage de ressources pour créer l’interface utilisateur pour tous les éléments en même temps, car ils ne peuvent pas tous être affichés en même temps. ListView et GridView (et d’autres contrôles dérivés d’ItemsControl standard) effectuent la virtualisation de l’interface utilisateur pour vous. Lorsque les éléments sont proches d’être affichés (à quelques pages), l’infrastructure génère l’interface utilisateur des éléments et les met en cache. Lorsqu’il est peu probable que les éléments soient affichés à nouveau, l’infrastructure récupère la mémoire.

Si vous fournissez un modèle de panneau d’éléments personnalisés (voir ItemsPanel), veillez à utiliser un panneau de virtualisation tel que ItemsWrapGrid ou ItemsStackPanel. Si vous utilisez VariableSizedWrapGrid, WrapGrid ou StackPanel, vous n’obtiendrez pas de virtualisation. En outre, les événements ListView suivants sont déclenchés uniquement lors de l’utilisation d’un ItemsWrapGrid ou d’un ItemsStackPanel : ChoosingGroupHeaderContainer, ChoosingItemContainer et ContainerContentChanging. Pour les dispositions personnalisées dans le Kit de développement logiciel (SDK) d’application Windows, l’équivalent moderne est une implémentation basée sur VirtualizingLayout lorsque les panneaux d’éléments intégrés ne répondent pas à vos besoins.

Le concept d’une fenêtre d’affichage est essentiel à la virtualisation de l’interface utilisateur, car l’infrastructure doit créer les éléments susceptibles d’être affichés. En général, la fenêtre d’affichage d’un ItemsControl est l’étendue du contrôle logique. Par exemple, la fenêtre d’affichage d’un ListView est la largeur et la hauteur de l’élément ListView . Certains panneaux permettent aux éléments enfants un espace illimité, tel que ScrollViewer et une grille avec des lignes ou des colonnes de taille automatique. Lorsqu’un ItemsControl virtualisé est placé dans un panneau comme celui-ci, il faut suffisamment de place pour afficher tous ses éléments, ce qui élimine la virtualisation. Restaurez la virtualisation en définissant une largeur et une hauteur sur ItemsControl.

Réduction des éléments par élément

Conservez le nombre d’éléments d’interface utilisateur utilisés pour afficher vos éléments au minimum.

Lorsqu’un contrôle d’éléments est affiché pour la première fois, tous les éléments nécessaires pour afficher une fenêtre d’affichage complète des éléments sont créés. En outre, à mesure que les éléments approchent de la fenêtre d’affichage, l’infrastructure met à jour les éléments d’interface utilisateur dans les modèles d’éléments mis en cache avec les objets de données liés. La réduction de la complexité du balisage à l’intérieur des modèles paie en mémoire et dans le temps passé sur le thread d’interface utilisateur, ce qui améliore la réactivité, en particulier lors du panoramique et du défilement. Les modèles en question sont le modèle d’élément (voir ItemTemplate) et le modèle de contrôle d’un ListViewItem ou d’un GridViewItem (le modèle de contrôle d’élément ou ItemContainerStyle). L’avantage d’une petite réduction du nombre d’éléments est multiplié par le nombre d’éléments affichés.

Pour obtenir des exemples de réduction d’éléments, consultez Optimiser le chargement XAML pour WinUI et le Kit de développement logiciel (SDK) d’application Windows.

Les modèles de contrôle par défaut pour ListViewItem et GridViewItem contiennent un élément ListViewItemPresenter . Ce présentateur est un élément optimisé unique qui affiche des visuels complexes pour la mise au point, la sélection et d'autres états visuels. Si vous avez déjà des modèles de contrôle d’élément personnalisés (ItemContainerStyle) ou si, à l’avenir, vous modifiez une copie d’un modèle de contrôle d’élément, nous vous recommandons d’utiliser un ListViewItemPresenter , car cet élément vous offre un équilibre optimal entre les performances et la personnalisation dans la majorité des cas. Vous personnalisez le présentateur en définissant des propriétés sur celui-ci. Par exemple, voici le balisage qui supprime la coche qui s’affiche par défaut lorsqu’un élément est sélectionné et modifie la couleur d’arrière-plan de l’élément sélectionné en 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>
<!-- ... -->

Il existe environ 25 propriétés avec des noms auto-décrivant similaires à SelectionCheckMarkVisualEnabled et SelectedBackground. Si les types de présentateurs ne sont pas suffisamment personnalisables pour votre cas d'usage, vous pouvez modifier une copie du modèle de contrôle ListViewItemExpanded ou GridViewItemExpanded à la place. Dans une application WinUI, examinez le generic.xaml fichier fourni avec le package sdk d’application Windows pour les modèles par défaut actuels. N’oubliez pas que l’utilisation de ces modèles implique un compromis sur les performances pour une personnalisation accrue.

Mettre à jour les éléments ListView et GridView progressivement

Si vous utilisez la virtualisation des données, vous pouvez conserver la réactivité listView et GridView élevée en configurant le contrôle pour afficher des éléments d’interface utilisateur temporaires pour les éléments en cours de téléchargement. Les éléments temporaires sont ensuite remplacés progressivement par l’interface utilisateur réelle au fur et à mesure que les données se chargent.

En outre, quel que soit l’endroit où vous chargez des données à partir de (disque local, réseau ou cloud), un utilisateur peut parcourir ou faire défiler un ListView ou GridView de façon rapide, afin qu’il ne soit pas possible d’afficher chaque élément avec une fidélité totale tout en préservant le panoramique lisse et le défilement. Pour conserver un panoramique fluide et un défilement régulier, vous pouvez choisir de rendre un élément en plusieurs phases, en plus d’utiliser des espaces réservés.

Un exemple de ces techniques est souvent vu dans les applications d’affichage photo : même si toutes les images n’ont pas été chargées et affichées, l’utilisateur peut toujours parcourir, faire défiler et interagir avec la collection. Ou, pour un élément de film, vous pouvez afficher le titre dans la première phase, l’évaluation dans la deuxième phase et une image de l’affiche dans la troisième phase. L’utilisateur voit les données les plus importantes sur chaque élément dès que possible, ce qui signifie qu’il est en mesure de prendre des mesures à la fois. Ensuite, les informations moins importantes sont renseignées au fur et à mesure que le temps le permet. Voici les fonctionnalités de plateforme que vous pouvez utiliser pour implémenter ces techniques.

Espaces réservés

La fonctionnalité de visuels d’espace réservé temporaire est activée par défaut et est contrôlée par la propriété ShowsScrollingPlaceholders. Pendant le mouvement panoramique rapide et le défilement, cette fonctionnalité donne à l’utilisateur une indication visuelle signalant qu’il y a plus d’éléments encore à afficher complètement tout en préservant la douceur. Si vous utilisez l'une des techniques ci-dessous, vous pouvez définir ShowsScrollingPlaceholders sur false si vous préférez que le système ne rende pas les placeholders.

Mises à jour progressives du modèle de données à l’aide de x :Phase

L’attribut x:Phase continue de fonctionner dans WinUI et reste un bon moyen de restituer progressivement le contenu de l’élément.

Voici comment utiliser l’attribut x :Phase avec des liaisons {x :Bind} pour implémenter des mises à jour progressives du modèle de données.

  1. Voici à quoi ressemble la source de liaison (il s’agit de la source de données à laquelle nous allons nous lier).

    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. Voici le balisage qui DeferMainPage.xaml contient. L’affichage grille contient un modèle d’élément avec des éléments liés aux propriétés Title, Subtitle et Description de la classe MyItem . Notez que x :Phase a la valeur 0 par défaut. Ici, les éléments sont initialement affichés avec uniquement le titre visible. Ensuite, l’élément de sous-titre est lié aux données et rendu visible pour tous les éléments, et ainsi de suite jusqu’à ce que toutes les phases aient été traitées.

    <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. Si vous exécutez l’application maintenant et faites défiler l’application rapidement dans l’affichage grille, vous remarquerez que, à mesure que chaque nouvel élément apparaît à l’écran, il est d’abord affiché sous la forme d’un rectangle gris foncé (grâce à la propriété ShowsScrollingPlaceholders par défaut la valeur true), puis le titre apparaît, suivi du sous-titre, suivi de la description.

Mises à jour progressives du modèle de données à l’aide de ContainerContentChanging

La stratégie générale de l’événement ContainerContentChanging consiste à utiliser Opacity pour masquer les éléments qui n’ont pas besoin d’être immédiatement visibles. Lorsque les éléments sont recyclés, ils conservent leurs anciennes valeurs. Nous voulons donc masquer ces éléments tant que nous n’avons pas mis à jour ces valeurs à partir du nouvel élément de données. Nous utilisons la propriété Phase sur les arguments d’événement pour déterminer quels éléments mettre à jour et afficher. Si des phases supplémentaires sont nécessaires, nous enregistrons un rappel.

  1. Nous allons utiliser la même source de liaison que pour x :Phase.

  2. Voici le balisage qui MainPage.xaml contient. La vue grille déclare un gestionnaire pour son événement ContainerContentChanging et contient un modèle d’élément avec des éléments utilisés pour afficher les propriétés Title, Title, Subtitle et Description de la classe MyItem . Pour bénéficier des performances maximales de l’utilisation de ContainerContentChanging, nous n’utilisons pas de liaisons dans le balisage et affectons des valeurs par programmation. L’exception ici est l’élément affichant le titre, que nous considérons comme étant dans la phase 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. Enfin, voici l’implémentation du gestionnaire d’événements ContainerContentChanging . Ce code montre également comment nous ajoutons une propriété de type ExampleItemViewModel à MainPage pour exposer la classe source de liaison à partir de la classe qui représente notre page de balisage. Tant que vous n’avez pas de liaisons {Binding} dans votre modèle de données, marquez l’objet d’arguments d’événement comme géré dans la première phase du gestionnaire pour indiquer à l’élément qu’il n’a pas besoin de définir un contexte de données.

    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. Si vous exécutez l'application maintenant et que vous panoramiquez ou défilez rapidement la vue en grille, vous verrez le même comportement que pour x:Phase.

Recyclage de conteneurs avec des collections hétérogènes

Dans certaines applications, vous devez disposer d’une interface utilisateur différente pour différents types d’éléments au sein d’une collection. Cela peut créer une situation où il est impossible de virtualiser des panneaux pour réutiliser ou recycler les éléments visuels utilisés pour afficher les éléments. La recréation des éléments visuels d’un objet pendant le panoramique annule de nombreux gains de performance fournis par la virtualisation. Toutefois, une petite planification peut permettre la virtualisation des panneaux pour réutiliser les éléments. Les développeurs ont quelques options en fonction de leur scénario : l’événement ChoosingItemContainer ou un sélecteur de modèle d’élément. L’approche ChoosingItemContainer offre de meilleures performances.

Événement ChoosingItemContainer

ChoosingItemContainer est un événement qui vous permet de fournir un élément (ListViewItem ou GridViewItem) à ListView ou GridView chaque fois qu’un nouvel élément est nécessaire pendant le démarrage ou le recyclage. Vous pouvez créer un conteneur en fonction du type d’élément de données affiché par le conteneur, comme illustré dans l’exemple ci-dessous. ChoosingItemContainer est le moyen le plus performant d’utiliser différents modèles de données pour différents éléments. La mise en cache de conteneur est un élément qui peut être obtenu à l’aide de ChoosingItemContainer. Par exemple, si vous avez cinq modèles différents, avec un modèle qui se produit un ordre de grandeur plus souvent que les autres, alors ChoosingItemContainer vous permet non seulement de créer des éléments aux ratios nécessaires, mais également de conserver un nombre approprié d’éléments mis en cache et disponibles pour le recyclage. ChoosingGroupHeaderContainer fournit les mêmes fonctionnalités pour les en-têtes de groupe.

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

Sélecteur de modèle d’élément

Un sélecteur de modèle d’élément (DataTemplateSelector) permet à une application de retourner un modèle d’élément différent au moment de l’exécution en fonction du type de l’élément de données qui sera affiché. Cela rend le développement plus productif, mais il rend la virtualisation de l’interface utilisateur plus difficile, car aucun modèle d’élément ne peut être réutilisé pour chaque élément de données.

Lors du recyclage d’un élément (ListViewItem ou GridViewItem), l’infrastructure doit décider si les éléments disponibles pour une utilisation dans la file d’attente de recyclage ont un modèle d’élément qui correspond à celui souhaité par l’élément de données actif. S’il n’y a aucun élément dans la file d’attente de recyclage avec le modèle d’élément approprié, un nouvel élément est créé et le modèle d’élément approprié est instancié pour celui-ci. Si, d’autre part, la file d’attente de recyclage contient un élément avec le modèle d’élément approprié, cet élément est supprimé de la file d’attente de recyclage et est utilisé pour l’élément de données actuel. Un sélecteur de modèle d’élément fonctionne dans les situations où seul un petit nombre de modèles d’élément sont utilisés et qu’il existe une distribution plate dans la collection d’éléments qui utilisent différents modèles d’éléments.   Lorsqu’il existe une distribution inégale d’éléments qui utilisent différents modèles d’éléments, de nouveaux modèles d’éléments devront probablement être créés pendant le défilement, ce qui compromet de nombreux gains fournis par la virtualisation. En outre, un sélecteur de modèle d’élément considère uniquement cinq candidats possibles lors de l’évaluation de la réutilisation d’un conteneur particulier pour l’élément de données actuel. Vous devez donc déterminer soigneusement si vos données sont appropriées pour une utilisation avec un sélecteur de modèle d’élément avant d’en utiliser une dans votre application WinUI. Si votre collection est principalement homogène, le sélecteur retourne le même type le plus ou tout le temps. N’oubliez pas le prix que vous payez pour les rares exceptions à cette homogénéité, et déterminez si l’utilisation de ChoosingItemContainer ou deux contrôles d’éléments est préférable.