Оптимизация производительности ListView и GridView

Примечание Дополнительные сведения см. в разделе //build/session Резкое увеличение производительности при взаимодействии пользователей с большими объемами данных в GridView и ListView.

Повышение производительности ListView и GridView через виртуализацию пользовательского интерфейса, уменьшение элементов и постепенное обновление элементов. Сведения о методах виртуализации данных см. в разделе "Виртуализация данных ListView" и "GridView" для WinUI.

Два ключевых фактора эффективности работы с коллекциями

Управление коллекциями — это распространенный сценарий. Средство просмотра фотографий имеет коллекции фотографий, читатель имеет коллекции статей, книг или историй, а приложение для покупок имеет коллекции продуктов. В этом разделе показано, как сделать приложение WinUI эффективным при управлении коллекциями.

Существует два ключевых фактора производительности, когда речь идет о коллекциях: одно время, затраченное потоком пользовательского интерфейса на создание элементов; Другой — это память, используемая как необработанным набором данных, так и элементами пользовательского интерфейса, используемыми для отрисовки этих данных.

Для плавного сдвига и прокрутки важно, чтобы поток пользовательского интерфейса выполнял задачу эффективного и умного создания экземпляров, привязки данных и размещения элементов.

Виртуализация пользовательского интерфейса

Виртуализация пользовательского интерфейса является самым важным улучшением, который можно сделать. Это означает, что элементы пользовательского интерфейса, представляющие элементы, создаются по запросу. Для элемента управления, привязанного к коллекции из 1000 элементов, было бы расточительно создавать пользовательский интерфейс для всех элементов одновременно, так как они не могут отображаться одновременно. ListView и GridView (и другие стандартные элементы управления, производные от ItemsControl), выполняют виртуализацию пользовательского интерфейса. Когда элементы близки к прокрутке в представление (несколько страниц), платформа создает пользовательский интерфейс для элементов и кэширует их. Когда маловероятно, что элементы будут отображаться снова, платформа освобождает память.

Если вы предоставляете шаблон панели настраиваемых элементов (см. ItemsPanel), убедитесь, что вы используете панель виртуализации, например ItemsWrapGrid или ItemsStackPanel. Если вы используете VariableSizedWrapGrid, WrapGrid или StackPanel, вы не получите виртуализацию. Кроме того, следующие события ListView вызываются только при использовании ItemsWrapGrid или ItemsStackPanel: ChoosingGroupHeaderContainer, ChoosingItemContainer, и ContainerContentChanging. Для пользовательских макетов в пакете SDK для приложений Windows современный эквивалент — это реализация на основе VirtualizingLayout, когда встроенные панели элементов не соответствуют вашим потребностям.

Концепция окна просмотра важна для виртуализации пользовательского интерфейса, так как платформа должна создавать элементы, которые, скорее всего, будут отображаться. Как правило, область просмотра ItemsControl — это объём логического элемента управления. Например, область просмотра ListView — это ширина и высота элемента ListView. Некоторые панели позволяют дочерним элементам неограниченное пространство, например ScrollViewer и Grid с автоматическими строками или столбцами. Когда виртуализированный ItemsControl помещается на панель, он занимает столько места, сколько необходимо для отображения всех элементов, что сводит на нет преимущества виртуализации. Восстановите виртуализацию, задав ширину и высоту в ItemsControl.

Уменьшение количества элементов на элемент

Сводите количество элементов пользовательского интерфейса, используемых для отображения ваших элементов, к разумному минимуму.

При первом отображении элемента управления элементами создаются все элементы, необходимые для отрисовки окна просмотра. Кроме того, при подходе к порту просмотра платформа обновляет элементы пользовательского интерфейса в кэшированных шаблонах элементов с привязанными объектами данных. Минимизация сложности разметки внутри шаблонов окупается в памяти и времени, затраченного на поток пользовательского интерфейса, повышая скорость реагирования, особенно во время сдвига и прокрутки. Шаблоны, которые имеются в вопросе, являются шаблоном элемента (см. ItemTemplate) и шаблоном элемента управления ListViewItem или GridViewItem (шаблон элемента управления или ItemContainerStyle). Преимущество даже небольшого уменьшения количества элементов умножается на число отображаемых элементов.

Примеры сокращения элементов см. в статье "Оптимизация загрузки XAML" для WinUI и пакета SDK для приложений Windows.

Шаблоны элементов управления по умолчанию для ListViewItem и GridViewItem содержат элемент ListViewItemPresenter . Этот компонент является одним оптимизированным элементом, который отображает сложные визуализации для фокусировки, выбора и других визуальных состояний. Если у вас уже есть пользовательские шаблоны элементов управления (ItemContainerStyle) или если в будущем вы редактируете копию шаблона элемента управления элементами, рекомендуется использовать ListViewItemPresenter , так как этот элемент обеспечивает оптимальный баланс между производительностью и настраиваемостью в большинстве случаев. Вы настраиваете презентер, устанавливая его свойства. Например, вот разметка, которая удаляет флажок, который по умолчанию отображается при выборе элемента и изменяет цвет фона выбранного элемента на оранжевый.

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

Существует около 25 свойств с самоописываемыми именами, похожими на SelectionCheckMarkVisualEnabled и SelectedBackground. Если типы представлений оказываются недостаточно настраиваемыми для вашего варианта использования, вы можете изменить копию шаблона ListViewItemExpanded или GridViewItemExpanded элемента управления. В приложении WinUI просмотрите generic.xaml файл, который поставляется с пакетом SDK для приложений Windows для текущих стандартных шаблонов. Помните, что использование этих шаблонов означает обмен некоторой производительностью на увеличение возможности настройки.

Постепенное обновление элементов ListView и GridView

Если вы используете виртуализацию данных, вы можете сохранить скорость реагирования ListView и GridView , настроив элемент управления для отображения временных элементов пользовательского интерфейса для элементов, которые по-прежнему загружаются. Затем временные элементы постепенно заменяются настоящим пользовательским интерфейсом по мере загрузки данных.

Кроме того, независимо от того, где вы загружаете данные из (локального диска, сети или облака), пользователь может сдвигать или прокрутку ListView или GridView так быстро, что невозможно отобразить каждый элемент с полной точностью при сохранении плавного сдвига и прокрутки. Чтобы сохранить плавность сдвига и прокрутки, вы можете отрисовать элемент на нескольких этапах, помимо использования заполнителей.

Пример этих методов часто отображается в приложениях для просмотра фотографий: несмотря на то, что не все изображения были загружены и отображены, пользователь по-прежнему может сдвигать, прокручивать и взаимодействовать с коллекцией. Или, для элемента фильма, вы можете показать название на первом этапе, рейтинг во втором этапе и изображение плаката на третьем этапе. Пользователь видит самые важные данные о каждом элементе как можно раньше, и это означает, что они могут одновременно принимать меры. Затем менее важные сведения заполняются по мере того, как позволяет время. Ниже приведены функции платформы, которые можно использовать для реализации этих методов.

Заполнители

Функция временных заполнителей включена по умолчанию и управляется свойством ShowsScrollingPlaceholders . Во время быстрого сдвига и прокрутки эта функция дает пользователю визуальное указание, что есть больше элементов, которые еще не отображаются, при этом сохраняя плавность. Если вы используете один из приведенных ниже способов, вы можете установить ShowsScrollingPlaceholders в значение False, если предпочитаете, чтобы система не отображала заполнители.

Прогрессивные обновления шаблонов данных с помощью x:Phase

Атрибут x:Phase продолжает работать в WinUI и остается хорошим способом постепенно отображать содержимое элемента.

Вот как использовать атрибут x:Phase с привязками {x:Bind} для реализации прогрессивных обновлений шаблона данных.

  1. Вот как выглядит источник привязки (это источник данных, к которому мы привязываемся).

    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. Вот разметка, которую содержит DeferMainPage.xaml. Представление сетки содержит шаблон элемента с элементами, привязанными к свойствам Title, Title и Description класса MyItem . Обратите внимание, что по умолчанию x:Phase используется значение 0. Здесь элементы изначально отображаются только с видимым заголовком. Затем элемент подзаголовка связан с данными и отображается для всех элементов, и так далее до тех пор, пока все этапы не будут обработаны.

    <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. Если вы запустите приложение сейчас и быстро прокрутите или листайте представление сетки, вы заметите, что по мере появления каждого нового элемента на экране он сначала представляется в виде темно-серого прямоугольника (благодаря свойству ShowsScrollingPlaceholders, у которого по умолчанию установлено значение true), затем появляется заголовок, за ним подзаголовок, а затем описание.

Прогрессивные обновления шаблонов данных с помощью ContainerContentChanging

Общая стратегия события ContainerContentChanging — использовать непрозрачность для скрытия элементов, которые не должны быть немедленно видимы. При перезапуске элементов они сохраняют старые значения, поэтому мы хотим скрыть эти элементы до тех пор, пока эти значения не будут обновлены из нового элемента данных. Свойство Phase используется для аргументов события, чтобы определить, какие элементы необходимо обновить и показать. Если требуются дополнительные этапы, мы регистрируем обратный вызов.

  1. Мы будем использовать тот же источник привязки, что и для x:Phase.

  2. Вот разметка, которую содержит MainPage.xaml. Представление в виде сетки объявляет обработчик для события ContainerContentChanging и содержит шаблон элемента с элементами, используемыми для отображения свойств Title, Subtitle и Description класса MyItem. Чтобы получить максимальные преимущества производительности при использовании ContainerContentChanging, мы не используем привязки в разметке и вместо этого присваиваем значения программным способом. Исключением здесь является элемент, отображающий заголовок, который мы считаем этапом 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. Наконец, вот реализация обработчика событий ContainerContentChanging . В этом коде также показано, как добавить свойство типа ExampleItemViewModel в MainPage , чтобы предоставить исходный класс привязки из класса, представляющего страницу разметки. Если в шаблоне данных нет привязок {Binding}, пометьте объект аргументов событий как обработанный на первом этапе обработчика, чтобы указать элементу, что ему не нужно задавать контекст данных.

    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. Если вы сейчас запускаете приложение и панорамируете или быстро прокрутите представление сетки, вы увидите такое же поведение, как у x:Phase.

Переработка контейнеров с разнородными коллекциями

В некоторых приложениях необходимо иметь другой пользовательский интерфейс для различных типов элементов в коллекции. Это может создать ситуацию, когда невозможно повторно использовать панели виртуализации или перезапускать визуальные элементы, используемые для отображения элементов. Повторное создание визуальных элементов для элемента во время прокрутки отменяет многие преимущества в производительности, предоставляемые виртуализацией. Однако небольшое планирование может позволить виртуализации панелей повторно использовать элементы. Разработчики имеют несколько вариантов в зависимости от их сценария: событие SelectingItemContainer или селектор шаблона элемента. Подход ВыборКонтейнераЭлементов обеспечивает более высокую производительность.

Событие выбора контейнера элемента

ChoosingItemContainer — это событие, позволяющее предоставить элемент (ListViewItem или GridViewItem) ListView или GridView всякий раз, когда новый элемент необходим во время запуска или переработки. Контейнер можно создать на основе типа элемента данных, отображаемого контейнером, как показано в примере ниже. ChoosingItemContainer — это более производительный метод использования различных шаблонов данных для разных элементов. Кэширование контейнеров можно осуществить с помощью ChoosingItemContainer. Например, если у вас есть пять разных шаблонов, один из которых встречается на порядок чаще остальных, то ВыборItemContainer позволяет не только создавать элементы в необходимых соотношениях, но и сохранять соответствующее количество элементов, кэшированных и доступных для повторного использования. ChoosingGroupHeaderContainer предоставляет те же функции для заголовков групп.

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

Селектор шаблона элемента

Селектор шаблона элемента (DataTemplateSelector) позволяет приложению возвращать другой шаблон элемента во время выполнения на основе типа отображаемого элемента данных. Это делает разработку более продуктивной, но это делает виртуализацию пользовательского интерфейса более сложной, так как не каждый шаблон элемента можно повторно использовать для каждого элемента данных.

При повторной переработке элемента (ListViewItem или GridViewItem) платформа должна определить, доступны ли элементы, доступные для использования в очереди перезапуска, имеют шаблон элемента, соответствующий нужному текущему элементу данных. Если в очереди утилизации нет элементов с соответствующим шаблоном элемента, создается новый элемент и для него инициализируется соответствующий шаблон элемента. Если, с другой стороны, очередь перезапуска содержит элемент с соответствующим шаблоном элемента, то этот элемент удаляется из очереди перезапуска и используется для текущего элемента данных. Селектор шаблонов элементов работает в ситуациях, когда используется только небольшое количество шаблонов элементов, и в коллекции элементов, использующих разные шаблоны элементов, существует равномерное распределение.   Если существует неравномерное распределение элементов, использующих различные шаблоны элементов, то, скорее всего, новые шаблоны элементов придется создавать при прокрутке, что сводит на нет многие преимущества, предоставляемые виртуализацией. Кроме того, селектор шаблона элемента рассматривает только пять возможных кандидатов при оценке возможности повторного использования определенного контейнера для текущего элемента данных. Поэтому следует тщательно рассмотреть, подходит ли ваши данные для использования с селектором шаблона элементов, прежде чем использовать его в приложении WinUI. Если коллекция в основном однородна, то селектор возвращает один и тот же тип больше или все время. Просто помните о цене, которую вы платите за редкие исключения этой однородности, и рассмотрите, предпочтительнее ли использовать ChoosingItemContainer или два элемента управления.