Присоединенные макеты

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

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

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

Важные API:

Основные понятия

Для выполнения макета требуется ответить на два вопроса для каждого элемента:

  1. Какой размер будет иметь этот элемент?

  2. Какая позиция этого элемента будет?

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

Контейнеры и контекст

Концептуально панель XAML выполняет две важные роли в фреймворке:

  1. Он может содержать дочерние элементы и вводит ветвление в дереве элементов.
  2. Она применяет определенную стратегию макета к этим дочерним элементам.

По этой причине панель в XAML часто является синонимом макета, но технически говоря, делает больше, чем просто макет.

Элемент ItemsRepeater также ведет себя как Панель, но, в отличие от Панели, он не предоставляет свойство Children, которое позволит программно добавлять или удалять дочерние элементы UIElement. Вместо этого срок существования дочерних объектов автоматически управляется фреймворком, чтобы соответствовать набору данных. Хотя он не является производным от панели, он ведет себя и обрабатывается платформой, как панель.

Замечание

LayoutPanel — это контейнер, производный от панели, который делегирует логику присоединенному объекту Layout. LayoutPanel находится в предварительной версии и в настоящее время доступен только в предварительных сборках пакета WinUI 3.

Containers

Концептуально панель — это контейнер элементов, который может отрисовывать пиксели заднего фона. Панели предоставляют способ инкапсулировать общую логику макета в простом пакете.

Концепция присоединенного макета делает различие между двумя ролями контейнера и макета более четким. Если контейнер делегирует логику макета другому объекту, мы назовем этот объект подключенным макетом, как показано в фрагменте ниже. Контейнеры, наследуемые от FrameworkElement, такие как LayoutPanel, автоматически предоставляют общие свойства, которые предоставляют входные данные процессу макета XAML (например, высота и ширина).

<LayoutPanel>
    <LayoutPanel.Layout>
        <UniformGridLayout/>
    </LayoutPanel.Layout>
    <Button Content="1"/>
    <Button Content="2"/>
    <Button Content="3"/>
</LayoutPanel>

Во время процесса макета контейнер зависит от прикрепленного объекта UniformGridLayout для оценки размеров и упорядочивания дочерних элементов.

Состояние на контейнер

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

<!-- ... --->
<Page.Resources>
    <ExampleLayout x:Name="exampleLayout"/>
<Page.Resources>

<LayoutPanel x:Name="example1" Layout="{StaticResource exampleLayout}"/>
<LayoutPanel x:Name="example2" Layout="{StaticResource exampleLayout}"/>
<!-- ... --->

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

LayoutContext

Цель LayoutContext заключается в решении этих проблем. Он предоставляет присоединенному макету возможность взаимодействия с контейнером-хозяином, например, получения дочерних элементов, без введения прямой зависимости между ними. Контекст также позволяет макету хранить любое состояние, которое требуется, что может быть связано с дочерними элементами контейнера.

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

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

Инициализация и деинициализация состояния каждого контейнера

При присоединении макета к контейнеру вызывается его метод InitializeForContextCore и предоставляет возможность инициализировать объект для хранения состояния.

Аналогичным образом, когда макет удаляется из контейнера, вызывается метод UninitializeForContextCore . Это дает макету возможность очистить любое состояние, связанное с этим контейнером.

Объект состояния макета можно хранить и извлекать из контейнера с помощью свойства LayoutState в контексте.

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

Виртуализация пользовательского интерфейса означает задержку создания объекта пользовательского интерфейса до тех пор, пока не потребуется. Это оптимизация производительности. Для сценариев без прокрутки определение времени необходимости может зависеть от множества факторов, специфичных для приложения. В таких случаях приложения должны рассмотреть возможность использования x:Load. Для этого не требуется специальная обработка в макете.

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

Замечание

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

Начало работы

Во-первых, определите, должен ли создаваемый макет поддерживать виртуализацию пользовательского интерфейса.

Несколько вещей, которые следует учитывать...

  1. Макеты без виртуализации легче создавать. Если количество элементов всегда будет небольшим, рекомендуется создать макет, отличный от виртуализации.
  2. Платформа предоставляет набор присоединенных макетов, которые работают с ItemsRepeater и LayoutPanel для удовлетворения распространенных потребностей. Ознакомьтесь с ними, прежде чем принимать решение о необходимости определить пользовательский макет.
  3. Макеты виртуализации всегда имеют некоторые дополнительные затраты на ЦП и память/ сложность/нагрузку по сравнению с макетом, не виртуализирующим. Как общее правило, если дочерние элементы макета, которыми нужно управлять, как правило, разместятся в области, равной тройному размеру окна просмотра, то может не быть значительных преимуществ от макета виртуализации. Размер 3x рассматривается более подробно далее в этом документе, но обусловлен асинхронным характером прокрутки Windows и его воздействием на виртуализацию.

Подсказка

В качестве точки отсчёта, параметры по умолчанию для ListViewItemsRepeater) таковы, что переработка не начинается, пока количество элементов не будет достаточно, чтобы заполнить в три раза больше текущего размера окна просмотра.

Выбор базового типа

присоединенная иерархия макета

Базовый тип макета имеет два производных типа, которые служат начальной точкой для создания присоединенного макета:

  1. NonVirtualizingLayout
  2. VirtualizingLayout

Макет, отличный от виртуализации

Подход к созданию не использующего виртуализацию макета должен быть знакомым любому, кто создал пользовательскую панель. Применяются те же понятия. Основное различие заключается в том, что для доступа к коллекции Children используется NonVirtualizingLayoutContext, и макет может выбрать хранение состояния.

  1. Производный от базового типа NonVirtualizingLayout (вместо Панели).
  2. (Необязательно) Определите свойства зависимости, которые при изменении будут приводить к недействительности макета.
  3. (Новый/Необязательный) Инициализировать любой необходимый для макета объект состояния в рамках InitializeForContextCore. Сохраните его в контейнере хоста, используя LayoutState, который предоставляется в контексте.
  4. Переопределите MeasureOverride и вызовите метод Measure для всех дочерних элементов.
  5. Переопределите ArrangeOverride и вызовите метод Arrange для всех дочерних элементов.
  6. (Новое/необязательно) Очистка любого сохраненного состояния в составе UninitializeForContextCore.

Пример: простой макет стека (элементы разного размера)

MyStackLayout

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

  1. Получите количество дочерних элементов и
  2. Доступ к каждому дочернему элементу по индексу.
public class MyStackLayout : NonVirtualizingLayout
{
    protected override Size MeasureOverride(NonVirtualizingLayoutContext context, Size availableSize)
    {
        double extentHeight = 0.0;
        foreach (var element in context.Children)
        {
            element.Measure(availableSize);
            extentHeight += element.DesiredSize.Height;
        }

        return new Size(availableSize.Width, extentHeight);
    }

    protected override Size ArrangeOverride(NonVirtualizingLayoutContext context, Size finalSize)
    {
        double offset = 0.0;
        foreach (var element in context.Children)
        {
            element.Arrange(
                new Rect(0, offset, finalSize.Width, element.DesiredSize.Height));
            offset += element.DesiredSize.Height;
        }

        return finalSize;
    }
}
 <LayoutPanel MaxWidth="196">
    <LayoutPanel.Layout>
        <local:MyStackLayout/>
    </LayoutPanel.Layout>

    <Button HorizontalAlignment="Stretch">1</Button>
    <Button HorizontalAlignment="Right">2</Button>
    <Button HorizontalAlignment="Center">3</Button>
    <Button>4</Button>

</LayoutPanel>

Виртуализация макетов

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

  1. Производный от базового типа VirtualizingLayout.
  2. (Необязательно) Определите свойства зависимостей, которые при изменении будут недействительными макета.
  3. Инициализировать любой объект состояния, который потребуется макету в рамках InitializeForContextCore. Сохраните его в контейнере хоста, используя LayoutState, который предоставляется в контексте.
  4. Переопределите MeasureOverride и вызовите метод Measure для каждого дочернего элемента, который должен быть реализован.
    1. Метод GetOrCreateElementAt используется для извлечения UIElement, подготовленного фреймворком (например, примененных привязок данных).
  5. Переопределите ArrangeOverride и вызовите метод Arrange для каждого реализованного дочернего элемента.
  6. (Необязательно) Очистите любое сохраненное состояние в составе UninitializeForContextCore.

Подсказка

Значение, возвращаемое measureOverride , используется в качестве размера виртуализированного содержимого.

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

Процесс вёрстки

Независимо от того, создаете ли вы макет, зависящий от данных или от содержимого, важно понимать процесс создания макета и влияние асинхронного скроллинга в Windows.

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

  1. Он анализирует разметку.

  2. Создает дерево элементов.

  3. Выполняет передачу макета.

  4. Выполняет передачу отрисовки.

При виртуализации пользовательского интерфейса создание элементов, которые обычно выполняются на шаге 2, задерживается или заканчивается рано после определения того, что для заполнения окна просмотра было создано достаточное содержимое. Контейнер виртуализации (например, ItemsRepeater) передаёт управление подключённому макету для ведения этого процесса. Он предоставляет присоединенный макет с VirtualizingLayoutContext, который содержит дополнительную информацию, необходимую для виртуализированного макета.

The RealizationRect (т. е. Viewport)

Прокрутка в Windows происходит асинхронно по отношению к потоку пользовательского интерфейса. Он не контролируется макетом платформы. Скорее, взаимодействие и движение происходит в компосторе системы. Преимуществом этого подхода является то, что панорамирование содержимого всегда возможно с частотой 60 кадров в секунду. Однако проблема заключается в том, что "viewport", как показано в макете, может быть немного устаревшим относительно того, что на самом деле видно на экране. Если пользователь быстро прокручивает, скорость UI-потока может отставать в генерации нового контента, и он может увидеть черный экран. По этой причине для виртуализации макета часто требуется создать дополнительный буфер подготовленных элементов, достаточный для заполнения области, превышающей окно просмотра. При более тяжелой нагрузке во время прокрутки пользователя по-прежнему отображается содержимое.

Реализация прямоугольника

Так как создание элемента является дорогостоящим, виртуализация контейнеров (например, ItemsRepeater) изначально предоставит присоединенный макет с RealizationRect, который соответствует области просмотра. Во время простоя контейнер может увеличить буфер подготовленного содержимого, выполняя повторяющиеся вызовы макета, используя все более крупный прямоугольник реализации. Это оптимизация производительности, которая стремится достичь баланса между быстрым временем запуска и качеством восприятия/пользовательского опыта при сдвиге. Максимальный размер буфера, создаваемый ItemsRepeater, управляется своими свойствами VerticalCacheLength и HorizontalCacheLength .

Повторное использование элементов (рециркуляция)

Ожидается, что макет будет изменять размер и располагать элементы так, чтобы они заполняли RealizationRect при каждом запуске. По умолчанию VirtualizingLayout будет перерабатывать любые неиспользуемые элементы в конце каждого прохода макета.

VirtualizingLayoutContext, передаваемый в макет в рамках MeasureOverride и ArrangeOverride, предоставляет дополнительные сведения о потребностях макета виртуализации. Некоторые из наиболее часто используемых функций, которые она предоставляет, — это возможность:

  1. Запросите количество элементов в данных (ItemCount).
  2. Извлеките определенный элемент с помощью метода GetItemAt .
  3. Извлеките объект RealizeRect , представляющий окно просмотра и буфер, которые макет должен заполнить реализованными элементами.
  4. Запросите UIElement для определенного элемента с помощью метода GetOrCreateElement .

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

В конце каждого шага измерений любой существующий, реализованный элемент, который не был помечен как "используем", автоматически считается доступным для повторного использования, если параметр SuppressAutoRecycle не использовался при извлечении элемента с помощью метода GetOrCreateElementAt. Фреймворк автоматически перемещает его в рециркуляционный пул и делает доступным. Затем его можно извлечь для использования другим контейнером. Платформа пытается избежать этого, если это возможно, так как существует некоторая стоимость, связанная с повторной родительской настройкой элемента.

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

VirtualizingLayoutContext предоставляет два дополнительных свойства, предназначенных для авторов макета, создающих макет, зависящий от содержимого. Они подробно рассматриваются позже.

  1. Параметр RecommendedAnchorIndex , предоставляющий необязательные входные данные для макета.
  2. LayoutOrigin, который является необязательным output макета.

Макеты виртуализации, зависящие от данных

Макет виртуализации проще, если вы знаете, какой размер каждого элемента должен быть без необходимости измерять содержимое. В этом документе мы просто будем ссылаться на эту категорию макетов виртуализации как макеты данных , так как они обычно включают проверку данных. На основе данных приложение может выбрать визуальное представление с известным размером, возможно, являющееся частью данных или предварительно определённым в процессе проектирования.

Общий подход заключается в том, что макет должен:

  1. Вычислите размер и положение каждого элемента.
  2. В рамках MeasureOverride:
    1. Чтобы определить, какие элементы должны отображаться в режиме просмотра, используйте RealizationRect.
    2. Получите UIElement, который должен представлять элемент с помощью метода GetOrCreateElementAt .
    3. Измерьте UIElement с предварительно рассчитанным размером.
  3. В рамках метода ArrangeOverride, упорядочьте каждый реализованный UIElement с предварительно вычисленной позицией.

Замечание

Подход к макету данных часто несовместим с виртуализацией данных. В частности, когда единственные данные, загруженные в память, — это данные, необходимые для заполнения видимых пользователем данных. Виртуализация данных не относится к отложенной или поэтапной загрузке данных при прокрутке пользователем страницы вниз, где данные продолжают находиться в памяти. Скорее, это относится к тому, когда элементы освобождаются из памяти, когда они исчезают из видимости при прокрутке. Наличие макета данных, который проверяет каждый элемент данных в составе макета данных, не позволит виртуализации данных работать должным образом. Исключением является макет, такой как UniformGridLayout, который предполагает, что все имеет одинаковый размер.

Подсказка

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

Пример: макет ленты активности Xbox

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

Лента активности Xbox

Приведенный ниже код описывает кастомный виртуализированный UI для ленты активности, чтобы проиллюстрировать общий подход, который может быть использован для макета данных.

Подсказка

Если у вас установлено приложение WinUI 3 Gallery, нажмите здесь, чтобы открыть приложение и увидеть ItemsRepeater в действии. Получите приложение из Microsoft Store или получите исходный код в GitHub.

Implementation

/// <summary>
///  This is a custom layout that displays elements in two different sizes
///  wide (w) and narrow (n). There are two types of rows 
///  odd rows - narrow narrow wide
///  even rows - wide narrow narrow
///  This pattern repeats.
/// </summary>

public class ActivityFeedLayout : VirtualizingLayout // STEP #1 Inherit from base attached layout
{
    // STEP #2 - Parameterize the layout
    #region Layout parameters

    // We'll cache copies of the dependency properties to avoid calling GetValue during layout since that
    // can be quite expensive due to the number of times we'd end up calling these.
    private double _rowSpacing;
    private double _colSpacing;
    private Size _minItemSize = Size.Empty;

    /// <summary>
    /// Gets or sets the size of the whitespace gutter to include between rows
    /// </summary>
    public double RowSpacing
    {
        get { return _rowSpacing; }
        set { SetValue(RowSpacingProperty, value); }
    }

    /// <summary>
    /// Gets or sets the size of the whitespace gutter to include between items on the same row
    /// </summary>
    public double ColumnSpacing
    {
        get { return _colSpacing; }
        set { SetValue(ColumnSpacingProperty, value); }
    }

    public Size MinItemSize
    {
        get { return _minItemSize; }
        set { SetValue(MinItemSizeProperty, value); }
    }

    public static readonly DependencyProperty RowSpacingProperty =
        DependencyProperty.Register(
            nameof(RowSpacing),
            typeof(double),
            typeof(ActivityFeedLayout),
            new PropertyMetadata(0, OnPropertyChanged));

    public static readonly DependencyProperty ColumnSpacingProperty =
        DependencyProperty.Register(
            nameof(ColumnSpacing),
            typeof(double),
            typeof(ActivityFeedLayout),
            new PropertyMetadata(0, OnPropertyChanged));

    public static readonly DependencyProperty MinItemSizeProperty =
        DependencyProperty.Register(
            nameof(MinItemSize),
            typeof(Size),
            typeof(ActivityFeedLayout),
            new PropertyMetadata(Size.Empty, OnPropertyChanged));

    private static void OnPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
        var layout = obj as ActivityFeedLayout;
        if (args.Property == RowSpacingProperty)
        {
            layout._rowSpacing = (double)args.NewValue;
        }
        else if (args.Property == ColumnSpacingProperty)
        {
            layout._colSpacing = (double)args.NewValue;
        }
        else if (args.Property == MinItemSizeProperty)
        {
            layout._minItemSize = (Size)args.NewValue;
        }
        else
        {
            throw new InvalidOperationException("Don't know what you are talking about!");
        }

        layout.InvalidateMeasure();
    }

    #endregion

    #region Setup / teardown // STEP #3: Initialize state

    protected override void InitializeForContextCore(VirtualizingLayoutContext context)
    {
        base.InitializeForContextCore(context);

        var state = context.LayoutState as ActivityFeedLayoutState;
        if (state == null)
        {
            // Store any state we might need since (in theory) the layout could be in use by multiple
            // elements simultaneously
            // In reality for the Xbox Activity Feed there's probably only a single instance.
            context.LayoutState = new ActivityFeedLayoutState();
        }
    }

    protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
    {
        base.UninitializeForContextCore(context);

        // clear any state
        context.LayoutState = null;
    }

    #endregion

    #region Layout // STEP #4,5 - Measure and Arrange

    protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
    {
        if (this.MinItemSize == Size.Empty)
        {
            var firstElement = context.GetOrCreateElementAt(0);
            firstElement.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));

            // setting the member value directly to skip invalidating layout
            this._minItemSize = firstElement.DesiredSize;
        }

        // Determine which rows need to be realized.  We know every row will have the same height and
        // only contain 3 items.  Use that to determine the index for the first and last item that
        // will be within that realization rect.
        var firstRowIndex = Math.Max(
            (int)(context.RealizationRect.Y / (this.MinItemSize.Height + this.RowSpacing)) - 1,
            0);
        var lastRowIndex = Math.Min(
            (int)(context.RealizationRect.Bottom / (this.MinItemSize.Height + this.RowSpacing)) + 1,
            (int)(context.ItemCount / 3));

        // Determine which items will appear on those rows and what the rect will be for each item
        var state = context.LayoutState as ActivityFeedLayoutState;
        state.LayoutRects.Clear();

        // Save the index of the first realized item.  We'll use it as a starting point during arrange.
        state.FirstRealizedIndex = firstRowIndex * 3;

        // ideal item width that will expand/shrink to fill available space
        double desiredItemWidth = Math.Max(this.MinItemSize.Width, (availableSize.Width - this.ColumnSpacing * 3) / 4);

        // Foreach item between the first and last index,
        //     Call GetElementOrCreateElementAt which causes an element to either be realized or retrieved
        //       from a recycle pool
        //     Measure the element using an appropriate size
        //
        // Any element that was previously realized which we don't retrieve in this pass (via a call to
        // GetElementOrCreateAt) will be automatically cleared and set aside for later re-use.
        // Note: While this work fine, it does mean that more elements than are required may be
        // created because it isn't until after our MeasureOverride completes that the unused elements
        // will be recycled and available to use.  We could avoid this by choosing to track the first/last
        // index from the previous layout pass.  The diff between the previous range and current range
        // would represent the elements that we can pre-emptively make available for re-use by calling
        // context.RecycleElement(element).
        for (int rowIndex = firstRowIndex; rowIndex < lastRowIndex; rowIndex++)
        {
            int firstItemIndex = rowIndex * 3;
            var boundsForCurrentRow = CalculateLayoutBoundsForRow(rowIndex, desiredItemWidth);

            for (int columnIndex = 0; columnIndex < 3; columnIndex++)
            {
                var index = firstItemIndex + columnIndex;
                var rect = boundsForCurrentRow[index % 3];
                var container = context.GetOrCreateElementAt(index);

                container.Measure(
                    new Size(boundsForCurrentRow[columnIndex].Width, boundsForCurrentRow[columnIndex].Height));

                state.LayoutRects.Add(boundsForCurrentRow[columnIndex]);
            }
        }

        // Calculate and return the size of all the content (realized or not) by figuring out
        // what the bottom/right position of the last item would be.
        var extentHeight = ((int)(context.ItemCount / 3) - 1) * (this.MinItemSize.Height + this.RowSpacing) + this.MinItemSize.Height;

        // Report this as the desired size for the layout
        return new Size(desiredItemWidth * 4 + this.ColumnSpacing * 2, extentHeight);
    }

    protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
    {
        // walk through the cache of containers and arrange
        var state = context.LayoutState as ActivityFeedLayoutState;
        var virtualContext = context as VirtualizingLayoutContext;
        int currentIndex = state.FirstRealizedIndex;

        foreach (var arrangeRect in state.LayoutRects)
        {
            var container = virtualContext.GetOrCreateElementAt(currentIndex);
            container.Arrange(arrangeRect);
            currentIndex++;
        }

        return finalSize;
    }

    #endregion
    #region Helper methods

    private Rect[] CalculateLayoutBoundsForRow(int rowIndex, double desiredItemWidth)
    {
        var boundsForRow = new Rect[3];

        var yoffset = rowIndex * (this.MinItemSize.Height + this.RowSpacing);
        boundsForRow[0].Y = boundsForRow[1].Y = boundsForRow[2].Y = yoffset;
        boundsForRow[0].Height = boundsForRow[1].Height = boundsForRow[2].Height = this.MinItemSize.Height;

        if (rowIndex % 2 == 0)
        {
            // Left tile (narrow)
            boundsForRow[0].X = 0;
            boundsForRow[0].Width = desiredItemWidth;
            // Middle tile (narrow)
            boundsForRow[1].X = boundsForRow[0].Right + this.ColumnSpacing;
            boundsForRow[1].Width = desiredItemWidth;
            // Right tile (wide)
            boundsForRow[2].X = boundsForRow[1].Right + this.ColumnSpacing;
            boundsForRow[2].Width = desiredItemWidth * 2 + this.ColumnSpacing;
        }
        else
        {
            // Left tile (wide)
            boundsForRow[0].X = 0;
            boundsForRow[0].Width = (desiredItemWidth * 2 + this.ColumnSpacing);
            // Middle tile (narrow)
            boundsForRow[1].X = boundsForRow[0].Right + this.ColumnSpacing;
            boundsForRow[1].Width = desiredItemWidth;
            // Right tile (narrow)
            boundsForRow[2].X = boundsForRow[1].Right + this.ColumnSpacing;
            boundsForRow[2].Width = desiredItemWidth;
        }

        return boundsForRow;
    }

    #endregion
}

internal class ActivityFeedLayoutState
{
    public int FirstRealizedIndex { get; set; }

    /// <summary>
    /// List of layout bounds for items starting with the
    /// FirstRealizedIndex.
    /// </summary>
    public List<Rect> LayoutRects
    {
        get
        {
            if (_layoutRects == null)
            {
                _layoutRects = new List<Rect>();
            }

            return _layoutRects;
        }
    }

    private List<Rect> _layoutRects;
}

(Необязательно) Управление сопоставлением элемента с UIElement

По умолчанию VirtualizingLayoutContext поддерживает сопоставление между реализованными элементами и индексом в источнике данных, который они представляют. Макет может управлять данным сопоставлением самостоятельно, всегда запрашивая параметр SuppressAutoRecycle при получении элемента с помощью метода GetOrCreateElementAt, предотвращая тем самым поведение автоматической перезагрузки по умолчанию. Макет может выбрать это, например, если он будет использоваться только при прокрутке в одном направлении и элементы, которые он рассматривает, предполагается, будут расположены последовательно (т. е. если известны индексы первого и последнего элементов, этого достаточно, чтобы знать все элементы, которые должны быть реализованы).

Пример: лента активности Xbox

В приведенном ниже фрагменте показана дополнительная логика, которую можно добавить в MeasureOverride в предыдущем примере для управления сопоставлением.

    protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
    {
        //...

        // Determine which items will appear on those rows and what the rect will be for each item
        var state = context.LayoutState as ActivityFeedLayoutState;
        state.LayoutRects.Clear();

         // Recycle previously realized elements that we know we won't need so that they can be used to
        // fill in gaps without requiring us to realize additional elements.
        var newFirstRealizedIndex = firstRowIndex * 3;
        var newLastRealizedIndex = lastRowIndex * 3 + 3;
        for (int i = state.FirstRealizedIndex; i < newFirstRealizedIndex; i++)
        {
            context.RecycleElement(state.IndexToElementMap.Get(i));
            state.IndexToElementMap.Clear(i);
        }

        for (int i = state.LastRealizedIndex; i < newLastRealizedIndex; i++)
        {
            context.RecycleElement(context.IndexElementMap.Get(i));
            state.IndexToElementMap.Clear(i);
        }

        // ...

        // Foreach item between the first and last index,
        //     Call GetElementOrCreateElementAt which causes an element to either be realized or retrieved
        //       from a recycle pool
        //     Measure the element using an appropriate size
        //
        for (int rowIndex = firstRowIndex; rowIndex < lastRowIndex; rowIndex++)
        {
            int firstItemIndex = rowIndex * 3;
            var boundsForCurrentRow = CalculateLayoutBoundsForRow(rowIndex, desiredItemWidth);

            for (int columnIndex = 0; columnIndex < 3; columnIndex++)
            {
                var index = firstItemIndex + columnIndex;
                var rect = boundsForCurrentRow[index % 3];
                UIElement container = null;
                if (state.IndexToElementMap.Contains(index))
                {
                    container = state.IndexToElementMap.Get(index);
                }
                else
                {
                    container = context = context.GetOrCreateElementAt(index, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
                    state.IndexToElementMap.Add(index, container);
                }

                container.Measure(
                    new Size(boundsForCurrentRow[columnIndex].Width, boundsForCurrentRow[columnIndex].Height));

                state.LayoutRects.Add(boundsForCurrentRow[columnIndex]);
            }
        }

        // ...
   }

internal class ActivityFeedLayoutState
{
    // ...
    Dictionary<int, UIElement> IndexToElementMap { get; set; }
    // ...
}

Макеты виртуализации, зависящие от содержимого

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

Замечание

Макеты, зависящие от содержания, не нарушают (или не должны нарушать) виртуализацию данных.

Оценки

Макеты, зависящие от содержимого, полагаются на оценку, чтобы угадать размер нереализованного содержимого и положение реализованного содержимого. По мере изменения этих оценок это приведет к тому, что контент будет регулярно смещаться в области прокрутки. Это может привести к очень разочаровывающей и удручающей пользовательской среде, если она не устранена. Здесь рассматриваются потенциальные проблемы и способы устранения рисков.

Замечание

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

Привязка прокрутки

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

Значение RecommendedAnchorIndex, предоставленного макету, может отражать элемент привязки, выбранный в данный момент элементом управления прокрутки. Кроме того, если разработчик явно запрашивает реализацию элемента для заданного индекса с помощью метода GetOrCreateElement в ItemsRepeater, то этот индекс назначается как RecommendedAnchorIndex при следующем проходе макета. Это позволяет подготовить макет к вероятному сценарию, когда разработчик добавляет элемент и затем вызывает метод StartBringIntoView, чтобы отобразить его на экране.

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

Влияние на полосы прокрутки

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

Чем точнее макет может быть в его оценках, тем меньше вероятность того, что пользователь увидит, что палец ScrollBar прыгает.

Исправления макета

Макет, зависящий от содержимого, должен быть подготовлен, чтобы его оценка соответствовала реальности. Например, когда пользователь прокручивает содержимое вверх и макет отображает самый первый элемент, он может обнаружить, что ожидаемое положение элемента относительно начальной точки приведет к тому, что он будет отображаться где-то, кроме исходной позиции (x:0, y:0). При этом макет может использовать свойство LayoutOrigin для установки вычисленной позиции в качестве новой точки отсчета макета. Результат аналогичен якорю прокрутки, в котором окно просмотра элемента управления прокруткой автоматически настраивается для учета положения содержимого, как уточняется в макете.

Исправление LayoutOrigin

Отсоединённые окна просмотра

Размер, возвращаемый методом MeasureOverride макета, представляет лучшее предположение по размеру содержимого, которое может измениться с каждым последовательным макетом. По мере прокрутки макета, макет будет постоянно повторно оцениваться с обновленным RealizationRect.

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

Когда макет обнаруживает, что его предположение неверно и /или видит непредвиденный сдвиг окна просмотра, он должен перенаправить начальную позицию. Макеты виртуализации, которые отправляются в составе элементов управления XAML, разрабатываются как макеты, зависящие от содержимого, так как они помещают меньше ограничений на характер отображаемого содержимого.

Пример: простой макет виртуализованного стека для элементов переменного размера

В приведенном ниже примере показан простой макет стека для элементов с переменным размером, которые:

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

Использование: разметка

<ScrollViewer>

  <ItemsRepeater x:Name="repeater" >
    <ItemsRepeater.Layout>

      <local:VirtualizingStackLayout />

    </ItemsRepeater.Layout>
    <ItemsRepeater.ItemTemplate>
      <DataTemplate x:Key="item">
        <UserControl IsTabStop="True" UseSystemFocusVisuals="True" Margin="5">
          <StackPanel BorderThickness="1" Background="LightGray" Margin="5">
            <Image x:Name="recipeImage" Source="{Binding ImageUri}"  Width="100" Height="100"/>
              <TextBlock x:Name="recipeDescription"
                         Text="{Binding Description}"
                         TextWrapping="Wrap"
                         Margin="10" />
          </StackPanel>
        </UserControl>
      </DataTemplate>
    </ItemsRepeater.ItemTemplate>
  </ItemsRepeater>

</ScrollViewer>

Codebehind: Main.cs

string _lorem = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam laoreet erat vel massa rutrum, eget mollis massa vulputate. Vivamus semper augue leo, eget faucibus nulla mattis nec. Donec scelerisque lacus at dui ultricies, eget auctor ipsum placerat. Integer aliquet libero sed nisi eleifend, nec rutrum arcu lacinia. Sed a sem et ante gravida congue sit amet ut augue. Donec quis pellentesque urna, non finibus metus. Proin sed ornare tellus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam laoreet erat vel massa rutrum, eget mollis massa vulputate. Vivamus semper augue leo, eget faucibus nulla mattis nec. Donec scelerisque lacus at dui ultricies, eget auctor ipsum placerat. Integer aliquet libero sed nisi eleifend, nec rutrum arcu lacinia. Sed a sem et ante gravida congue sit amet ut augue. Donec quis pellentesque urna, non finibus metus. Proin sed ornare tellus.";

var rnd = new Random();
var data = new ObservableCollection<Recipe>(Enumerable.Range(0, 300).Select(k =>
               new Recipe
               {
                   ImageUri = new Uri(string.Format("ms-appx:///Images/recipe{0}.png", k % 8 + 1)),
                   Description = k + " - " + _lorem.Substring(0, rnd.Next(50, 350))
               }));

repeater.ItemsSource = data;

Код: VirtualizingStackLayout.cs

// This is a sample layout that stacks elements one after
// the other where each item can be of variable height. This is
// also a virtualizing layout - we measure and arrange only elements
// that are in the viewport. Not measuring/arranging all elements means
// that we do not have the complete picture and need to estimate sometimes.
// For example the size of the layout (extent) is an estimation based on the
// average heights we have seen so far. Also, if you drag the mouse thumb
// and yank it quickly, then we estimate what goes in the new viewport.

// The layout caches the bounds of everything that are in the current viewport.
// During measure, we might get a suggested anchor (or start index), we use that
// index to start and layout the rest of the items in the viewport relative to that
// index. Note that since we are estimating, we can end up with negative origin when
// the viewport is somewhere in the middle of the extent. This is achieved by setting the
// LayoutOrigin property on the context. Once this is set, future viewport will account
// for the origin.
public class VirtualizingStackLayout : VirtualizingLayout
{
    // Estimation state
    List<double> m_estimationBuffer = Enumerable.Repeat(0d, 100).ToList();
    int m_numItemsUsedForEstimation = 0;
    double m_totalHeightForEstimation = 0;

    // State to keep track of realized bounds
    int m_firstRealizedDataIndex = 0;
    List<Rect> m_realizedElementBounds = new List<Rect>();

    Rect m_lastExtent = new Rect();

    protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
    {
        var viewport = context.RealizationRect;
        DebugTrace("MeasureOverride: Viewport " + viewport);

        // Remove bounds for elements that are now outside the viewport.
        // Proactive recycling elements means we can reuse it during this measure pass again.
        RemoveCachedBoundsOutsideViewport(viewport);

        // Find the index of the element to start laying out from - the anchor
        int startIndex = GetStartIndex(context, availableSize);

        // Measure and layout elements starting from the start index, forward and backward.
        Generate(context, availableSize, startIndex, forward:true);
        Generate(context, availableSize, startIndex, forward:false);

        // Estimate the extent size. Note that this can have a non 0 origin.
        m_lastExtent = EstimateExtent(context, availableSize);
        context.LayoutOrigin = new Point(m_lastExtent.X, m_lastExtent.Y);
        return new Size(m_lastExtent.Width, m_lastExtent.Height);
    }

    protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
    {
        DebugTrace("ArrangeOverride: Viewport" + context.RealizationRect);
        for (int realizationIndex = 0; realizationIndex < m_realizedElementBounds.Count; realizationIndex++)
        {
            int currentDataIndex = m_firstRealizedDataIndex + realizationIndex;
            DebugTrace("Arranging " + currentDataIndex);

            // Arrange the child. If any alignment needs to be done, it
            // can be done here.
            var child = context.GetOrCreateElementAt(currentDataIndex);
            var arrangeBounds = m_realizedElementBounds[realizationIndex];
            arrangeBounds.X -= m_lastExtent.X;
            arrangeBounds.Y -= m_lastExtent.Y;
            child.Arrange(arrangeBounds);
        }

        return finalSize;
    }

    // The data collection has changed, since we are maintaining the bounds of elements
    // in the viewport, we will update the list to account for the collection change.
    protected override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args)
    {
        InvalidateMeasure();
        if (m_realizedElementBounds.Count > 0)
        {
            switch (args.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
                    break;
                case NotifyCollectionChangedAction.Replace:
                    OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count);
                    OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
                    break;
                case NotifyCollectionChangedAction.Remove:
                    OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count);
                    break;
                case NotifyCollectionChangedAction.Reset:
                    m_realizedElementBounds.Clear();
                    m_firstRealizedDataIndex = 0;
                    break;
                default:
                    throw new NotImplementedException();
            }
        }
    }

    // Figure out which index to use as the anchor and start laying out around it.
    private int GetStartIndex(VirtualizingLayoutContext context, Size availableSize)
    {
        int startDataIndex = -1;
        var recommendedAnchorIndex = context.RecommendedAnchorIndex;
        bool isSuggestedAnchorValid = recommendedAnchorIndex != -1;

        if (isSuggestedAnchorValid)
        {
            if (IsRealized(recommendedAnchorIndex))
            {
                startDataIndex = recommendedAnchorIndex;
            }
            else
            {
                ClearRealizedRange();
                startDataIndex = recommendedAnchorIndex;
            }
        }
        else
        {
            // Find the first realized element that is visible in the viewport.
            startDataIndex = GetFirstRealizedDataIndexInViewport(context.RealizationRect);
            if (startDataIndex < 0)
            {
                startDataIndex = EstimateIndexForViewport(context.RealizationRect, context.ItemCount);
                ClearRealizedRange();
            }
        }

        // We have an anchorIndex, realize and measure it and
        // figure out its bounds.
        if (startDataIndex != -1 & context.ItemCount > 0)
        {
            if (m_realizedElementBounds.Count == 0)
            {
                m_firstRealizedDataIndex = startDataIndex;
            }

            var newAnchor = EnsureRealized(startDataIndex);
            DebugTrace("Measuring start index " + startDataIndex);
            var desiredSize = MeasureElement(context, startDataIndex, availableSize);

            var bounds = new Rect(
                0,
                newAnchor ?
                    (m_totalHeightForEstimation / m_numItemsUsedForEstimation) * startDataIndex : GetCachedBoundsForDataIndex(startDataIndex).Y,
                availableSize.Width,
                desiredSize.Height);
            SetCachedBoundsForDataIndex(startDataIndex, bounds);
        }

        return startDataIndex;
    }


    private void Generate(VirtualizingLayoutContext context, Size availableSize, int anchorDataIndex, bool forward)
    {
        // Generate forward or backward from anchorIndex until we hit the end of the viewport
        int step = forward ? 1 : -1;
        int previousDataIndex = anchorDataIndex;
        int currentDataIndex = previousDataIndex + step;
        var viewport = context.RealizationRect;
        while (IsDataIndexValid(currentDataIndex, context.ItemCount) &&
            ShouldContinueFillingUpSpace(previousDataIndex, forward, viewport))
        {
            EnsureRealized(currentDataIndex);
            DebugTrace("Measuring " + currentDataIndex);
            var desiredSize = MeasureElement(context, currentDataIndex, availableSize);
            var previousBounds = GetCachedBoundsForDataIndex(previousDataIndex);
            Rect currentBounds = new Rect(0,
                                          forward ? previousBounds.Y + previousBounds.Height : previousBounds.Y - desiredSize.Height,
                                          availableSize.Width,
                                          desiredSize.Height);
            SetCachedBoundsForDataIndex(currentDataIndex, currentBounds);
            previousDataIndex = currentDataIndex;
            currentDataIndex += step;
        }
    }

    // Remove bounds that are outside the viewport, leaving one extra since our
    // generate stops after generating one extra to know that we are outside the
    // viewport.
    private void RemoveCachedBoundsOutsideViewport(Rect viewport)
    {
        int firstRealizedIndexInViewport = 0;
        while (firstRealizedIndexInViewport < m_realizedElementBounds.Count &&
               !Intersects(m_realizedElementBounds[firstRealizedIndexInViewport], viewport))
        {
            firstRealizedIndexInViewport++;
        }

        int lastRealizedIndexInViewport = m_realizedElementBounds.Count - 1;
        while (lastRealizedIndexInViewport >= 0 &&
            !Intersects(m_realizedElementBounds[lastRealizedIndexInViewport], viewport))
        {
            lastRealizedIndexInViewport--;
        }

        if (firstRealizedIndexInViewport > 0)
        {
            m_firstRealizedDataIndex += firstRealizedIndexInViewport;
            m_realizedElementBounds.RemoveRange(0, firstRealizedIndexInViewport);
        }

        if (lastRealizedIndexInViewport >= 0 && lastRealizedIndexInViewport < m_realizedElementBounds.Count - 2)
        {
            m_realizedElementBounds.RemoveRange(lastRealizedIndexInViewport + 2, m_realizedElementBounds.Count - lastRealizedIndexInViewport - 3);
        }
    }

    private bool Intersects(Rect bounds, Rect viewport)
    {
        return !(bounds.Bottom < viewport.Top ||
            bounds.Top > viewport.Bottom);
    }

    private bool ShouldContinueFillingUpSpace(int dataIndex, bool forward, Rect viewport)
    {
        var bounds = GetCachedBoundsForDataIndex(dataIndex);
        return forward ?
            bounds.Y < viewport.Bottom :
            bounds.Y > viewport.Top;
    }

    private bool IsDataIndexValid(int currentDataIndex, int itemCount)
    {
        return currentDataIndex >= 0 && currentDataIndex < itemCount;
    }

    private int EstimateIndexForViewport(Rect viewport, int dataCount)
    {
        double averageHeight = m_totalHeightForEstimation / m_numItemsUsedForEstimation;
        int estimatedIndex = (int)(viewport.Top / averageHeight);
        // clamp to an index within the collection
        estimatedIndex = Math.Max(0, Math.Min(estimatedIndex, dataCount));
        return estimatedIndex;
    }

    private int GetFirstRealizedDataIndexInViewport(Rect viewport)
    {
        int index = -1;
        if (m_realizedElementBounds.Count > 0)
        {
            for (int i = 0; i < m_realizedElementBounds.Count; i++)
            {
                if (m_realizedElementBounds[i].Y < viewport.Bottom &&
                   m_realizedElementBounds[i].Bottom > viewport.Top)
                {
                    index = m_firstRealizedDataIndex + i;
                    break;
                }
            }
        }

        return index;
    }

    private Size MeasureElement(VirtualizingLayoutContext context, int index, Size availableSize)
    {
        var child = context.GetOrCreateElementAt(index);
        child.Measure(availableSize);

        int estimationBufferIndex = index % m_estimationBuffer.Count;
        bool alreadyMeasured = m_estimationBuffer[estimationBufferIndex] != 0;
        if (!alreadyMeasured)
        {
            m_numItemsUsedForEstimation++;
        }

        m_totalHeightForEstimation -= m_estimationBuffer[estimationBufferIndex];
        m_totalHeightForEstimation += child.DesiredSize.Height;
        m_estimationBuffer[estimationBufferIndex] = child.DesiredSize.Height;

        return child.DesiredSize;
    }

    private bool EnsureRealized(int dataIndex)
    {
        if (!IsRealized(dataIndex))
        {
            int realizationIndex = RealizationIndex(dataIndex);
            Debug.Assert(dataIndex == m_firstRealizedDataIndex - 1 ||
                dataIndex == m_firstRealizedDataIndex + m_realizedElementBounds.Count ||
                m_realizedElementBounds.Count == 0);

            if (realizationIndex == -1)
            {
                m_realizedElementBounds.Insert(0, new Rect());
            }
            else
            {
                m_realizedElementBounds.Add(new Rect());
            }

            if (m_firstRealizedDataIndex > dataIndex)
            {
                m_firstRealizedDataIndex = dataIndex;
            }

            return true;
        }

        return false;
    }

    // Figure out the extent of the layout by getting the number of items remaining
    // above and below the realized elements and getting an estimation based on
    // average item heights seen so far.
    private Rect EstimateExtent(VirtualizingLayoutContext context, Size availableSize)
    {
        double averageHeight = m_totalHeightForEstimation / m_numItemsUsedForEstimation;

        Rect extent = new Rect(0, 0, availableSize.Width, context.ItemCount * averageHeight);

        if (context.ItemCount > 0 && m_realizedElementBounds.Count > 0)
        {
            extent.Y = m_firstRealizedDataIndex == 0 ?
                            m_realizedElementBounds[0].Y :
                            m_realizedElementBounds[0].Y - (m_firstRealizedDataIndex - 1) * averageHeight;

            int lastRealizedIndex = m_firstRealizedDataIndex + m_realizedElementBounds.Count;
            if (lastRealizedIndex == context.ItemCount - 1)
            {
                var lastBounds = m_realizedElementBounds[m_realizedElementBounds.Count - 1];
                extent.Y = lastBounds.Bottom;
            }
            else
            {
                var lastBounds = m_realizedElementBounds[m_realizedElementBounds.Count - 1];
                int lastRealizedDataIndex = m_firstRealizedDataIndex + m_realizedElementBounds.Count;
                int numItemsAfterLastRealizedIndex = context.ItemCount - lastRealizedDataIndex;
                extent.Height = lastBounds.Bottom + numItemsAfterLastRealizedIndex * averageHeight - extent.Y;
            }
        }

        DebugTrace("Extent " + extent + " with average height " + averageHeight);
        return extent;
    }

    private bool IsRealized(int dataIndex)
    {
        int realizationIndex = dataIndex - m_firstRealizedDataIndex;
        return realizationIndex >= 0 && realizationIndex < m_realizedElementBounds.Count;
    }

    // Index in the m_realizedElementBounds collection
    private int RealizationIndex(int dataIndex)
    {
        return dataIndex - m_firstRealizedDataIndex;
    }

    private void OnItemsAdded(int index, int count)
    {
        // Using the old indexes here (before it was updated by the collection change)
        // if the insert data index is between the first and last realized data index, we need
        // to insert items.
        int lastRealizedDataIndex = m_firstRealizedDataIndex + m_realizedElementBounds.Count - 1;
        int newStartingIndex = index;
        if (newStartingIndex > m_firstRealizedDataIndex &&
            newStartingIndex <= lastRealizedDataIndex)
        {
            // Inserted within the realized range
            int insertRangeStartIndex = newStartingIndex - m_firstRealizedDataIndex;
            for (int i = 0; i < count; i++)
            {
                // Insert null (sentinel) here instead of an element, that way we do not
                // end up creating a lot of elements only to be thrown out in the next layout.
                int insertRangeIndex = insertRangeStartIndex + i;
                int dataIndex = newStartingIndex + i;
                // This is to keep the contiguousness of the mapping
                m_realizedElementBounds.Insert(insertRangeIndex, new Rect());
            }
        }
        else if (index <= m_firstRealizedDataIndex)
        {
            // Items were inserted before the realized range.
            // We need to update m_firstRealizedDataIndex;
            m_firstRealizedDataIndex += count;
        }
    }

    private void OnItemsRemoved(int index, int count)
    {
        int lastRealizedDataIndex = m_firstRealizedDataIndex + m_realizedElementBounds.Count - 1;
        int startIndex = Math.Max(m_firstRealizedDataIndex, index);
        int endIndex = Math.Min(lastRealizedDataIndex, index + count - 1);
        bool removeAffectsFirstRealizedDataIndex = (index <= m_firstRealizedDataIndex);

        if (endIndex >= startIndex)
        {
            ClearRealizedRange(RealizationIndex(startIndex), endIndex - startIndex + 1);
        }

        if (removeAffectsFirstRealizedDataIndex &&
            m_firstRealizedDataIndex != -1)
        {
            m_firstRealizedDataIndex -= count;
        }
    }

    private void ClearRealizedRange(int startRealizedIndex, int count)
    {
        m_realizedElementBounds.RemoveRange(startRealizedIndex, count);
        if (startRealizedIndex == 0)
        {
            m_firstRealizedDataIndex = m_realizedElementBounds.Count == 0 ? 0 : m_firstRealizedDataIndex + count;
        }
    }

    private void ClearRealizedRange()
    {
        m_realizedElementBounds.Clear();
        m_firstRealizedDataIndex = 0;
    }

    private Rect GetCachedBoundsForDataIndex(int dataIndex)
    {
        return m_realizedElementBounds[RealizationIndex(dataIndex)];
    }

    private void SetCachedBoundsForDataIndex(int dataIndex, Rect bounds)
    {
        m_realizedElementBounds[RealizationIndex(dataIndex)] = bounds;
    }

    private Rect GetCachedBoundsForRealizationIndex(int relativeIndex)
    {
        return m_realizedElementBounds[relativeIndex];
    }

    void DebugTrace(string message, params object[] args)
    {
        Debug.WriteLine(message, args);
    }
}