Оптимизация пользовательского интерфейса ListView и GridView

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

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

Два ключевых фактора производительности коллекции

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

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

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

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

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

Если вы предоставляете шаблон панели настраиваемых элементов (см. ItemsPanel), убедитесь, что вы используете панель виртуализации, например ItemsWrapGrid или ItemsStackPanel. Если вы используете VariableSizedWrapGrid, WrapGrid или StackPanel, вы не получите виртуализацию. Кроме того, следующие события ListView вызываются только при использовании ItemsWrapGrid или ItemsStackPanel: ВыборGroupHeaderContainer, ВыборItemContainer и ContainerContentChanging.

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

Сокращение элементов на элемент

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

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

Примеры сокращения элементов см. в статье "Оптимизация разметки XAML".

Шаблоны элементов управления по умолчанию для 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 шаблона элемента управления. Их можно найти в \Program Files (x86)\Windows Kits\10\DesignTime\CommonConfiguration\Neutral\UAP\<version>\Generic\generic.xaml. Помните, что использование этих шаблонов означает торговую производительность для повышения настройки.

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

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

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

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

Заполнители

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

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

Вот как использовать атрибут 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, субтитров и описания класса 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 . В этом коде также показано, как добавить свойство типа RecordingViewModel в 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 или селектор шаблона элемента. Подход ВыборItemContainer обеспечивает более высокую производительность.

Событие ChoosingItemContainer

ChoosingItemContainer — это событие, которое позволяет предоставить элемент (ListViewItem/GridViewItem) для ListView/GridView при необходимости создания элемента в ходе запуска или повторного использования. Контейнер можно создать на основе типа элемента данных, отображаемого контейнером (показан в приведенном ниже примере). ВыборItemContainer — это более высокий способ использования различных шаблонов данных для различных элементов. Кэширование контейнеров — это то, что можно достичь с помощью выборкиItemContainer. Например, если у вас есть пять разных шаблонов, с одним шаблоном чаще, чем другие, то Выбор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, 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) платформа должна определить, имеют ли элементы, доступные для использования в "очереди повторного запуска" (очередь повторного запуска — это кэш элементов, которые в настоящее время не используются для отображения данных), шаблон элемента, который соответствует нужному для текущего элемента данных. Если в очереди повторного запуска нет элементов с подходящим шаблоном элемента, создается новый элемент, и для него создается экземпляр соответствующего шаблона элемента. Если, с другой стороны, очередь перезапуска содержит элемент с соответствующим шаблоном элемента, этот элемент удаляется из очереди перезапуска и используется для текущего элемента данных. Селектор шаблонов элементов работает в ситуациях, когда используется только небольшое количество шаблонов элементов, и в коллекции элементов, использующих разные шаблоны элементов, существует неструктурированное распределение.

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