附属布局

将布局逻辑委托给另一个对象的容器(例如面板),它依赖于附加的布局对象来为其子元素提供布局行为。 附加布局模型为应用程序提供了在运行时更改项目布局的灵活性,或者更轻松地在 UI 的不同部分之间共享布局的各个方面(例如,表中似乎在列内对齐的行中的项)。

在本主题中,我们将讨论创建附加布局(虚拟化和非虚拟化)所涉及的内容、需要理解的概念和类,以及在决定选择它们之间的权衡时需要考虑的问题。

获取 WinUI
此控件包含在 WinUI 中,它是一个 NuGet 包,其中包含Windows应用的新控件和 UI 功能。 有关详细信息(包括安装说明),请参阅 WinUI 概述

重要 API

关键概念

执行布局需要为每个元素回答两个问题:

  1. 元素的大小是什么

  2. 此元素 的位置 是什么?

XAML 的布局系统在讨论 自定义面板时简要介绍了这些问题。

容器和上下文

从概念上讲,XAML 的 面板 在框架中填充了两个重要角色:

  1. 它可以包含子元素,并在元素树中引入分支。
  2. 它将特定布局策略应用于这些子级。

因此,XAML 中的面板通常与布局同义,但从技术上讲讲,不仅仅是布局。

ItemsRepeater 的行为也类似于 Panel,但不同于 Panel 的是,它没有公开允许以编程方式添加或删除 UIElement 子元素的 Children 属性。 相反,其子对象的生命周期由框架自动管理,使其与数据项集合对应。 虽然它不是从 Panel 面板派生的,但其行为和框架对它的处理如同面板一样。

注释

LayoutPanel 是派生自 Panel 的容器,它将其逻辑委托给附加的 Layout 对象。 LayoutPanel 处于 预览状态 ,目前仅在 WinUI 3 包的 预发行版 中可用。

容器

从概念上讲, Panel 是元素的容器,还可以呈现 背景的像素。 面板提供了一种在易于使用的包中封装常见布局逻辑的方法。

附加布局的概念使容器和布局的两个角色之间的区别更加清晰。 如果容器将其布局逻辑委托给另一个对象,我们将调用附加布局的对象,如以下代码片段所示。 从 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 的目的是应对这些挑战。 它使附加布局能够与主机容器进行交互,例如检索子元素,而无需在两者之间引入直接依赖项。 上下文还允许布局存储它所需的任何状态,这些状态可能与容器的子元素相关。

简单、非虚拟化的布局通常不需要维护任何状态,因此无需担心。 但是,更复杂的布局(如网格)可以选择在度量值和排列调用之间保持状态,以避免重新计算值。

虚拟化布局 通常需要 在测量和排列之间以及迭代布局过程之间保持一些状态。

初始化和反初始化每个容器的状态

当布局附加到容器时,将调用其 InitializeForContextCore 方法,并提供初始化对象以存储状态的机会。

同样,从容器中删除布局时,将调用 UninitializeForContextCore 方法。 这样,布局就有机会清理与该容器关联的任何状态。

布局的状态对象可以使用上下文上的 LayoutState 属性在容器中存储和检索。

UI 虚拟化

UI 虚拟化意味着在需要 UI 对象 之前延迟创建 UI 对象。 这是性能优化。 对于非滚动场景,确定何时需要可能取决于应用程序特定的各种事项。 在这些情况下,应用应考虑使用 x:Load。 它不需要在布局中进行任何特殊处理。

在基于滚动的方案(如列表)中,确定 何时需要 通常基于“用户是否可见”,这在很大程度上取决于它在布局过程中的位置,并且需要特殊注意事项。 此方案是本文档的重点。

注释

尽管本文档未介绍,但在滚动方案中启用 UI 虚拟化的相同功能可以在非滚动方案中应用。 例如,一个数据驱动的 ToolBar 控件,它通过回收/移动可见区域和溢出菜单之间的元素来管理它呈现的命令的生存期,并响应可用空间的变化。

入门

首先,确定是否需要创建的布局应支持 UI 虚拟化。

要记住的一些事项...

  1. 非虚拟化布局更易于设计。 如果项数始终较小,建议设计非虚拟化的布局。
  2. 该平台提供了一组附加布局,这些布局适用于 ItemsRepeaterLayoutPanel ,以满足常见需求。 在决定需要定义自定义布局之前熟悉这些布局。
  3. 与非虚拟化布局相比,虚拟化布局始终具有一些额外的 CPU 和内存成本/复杂性/开销。 作为一般经验法则,如果布局需要管理的子级可能适合视区大小为 3 倍的区域,那么虚拟化布局可能没有太大的收益。 本文后面将更详细地讨论 3x 大小,但这与 Windows 的滚动异步性质及其对虚拟化的影响有关。

小窍门

作为参考点, ListView (和 ItemsRepeater)的默认设置是,在项数足以填满当前视区大小的 3 倍之前,回收才会开始。

选择基类型

附加的布局层次结构

基本 布局 类型有两个派生类型,用作创作附加布局的起点:

  1. NonVirtualizingLayout
  2. VirtualizingLayout

非虚拟化布局

创建非虚拟化布局的方法应该对已创建 自定义面板的任何人感到熟悉。 相同的概念适用。 主要区别在于使用 NonVirtualizingLayoutContext 来访问 Children 集合,布局可以选择存储状态。

  1. 派生自基类型 NonVirtualizingLayout(而非 Panel)。
  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 返回的值用作虚拟化内容的大小。

创作虚拟化布局时,需要考虑两种常规方法。 是否选择一个或另一个在很大程度上取决于“如何确定元素的大小”。 如果它足以知道数据集中某个项的索引或数据本身决定了其最终大小,那么我们会将其视为 数据依赖型。 这些更容易创建。 但是,如果确定项的大小的唯一方法是创建和测量 UI,那么我们会说它依赖于 内容。 这些更为复杂。

布局过程

无论是创建数据还是依赖于内容的布局,请务必了解布局过程以及Windows异步滚动的影响。

框架从启动到在屏幕上显示 UI 的步骤的简化视图是:

  1. 它分析标记。

  2. 生成元素树。

  3. 执行布局传递。

  4. 执行渲染过程。

使用 UI 虚拟化,创建通常在步骤 2 中完成的元素会在确定创建足够的内容以填充视区后延迟或提前结束。 虚拟化容器(例如 ItemsRepeater)依赖其附加布局来驱动此过程。 它为附加的布局提供了 VirtualizingLayoutContext,这种布局展示了虚拟化布局所需的额外信息。

RealizationRect (即视区)

在 Windows 系统中,滚动操作是相对于 UI 线程异步进行的。 它不受框架的布局控制。 相反,交互和移动发生在系统的合成器中。 此方法的优点是,平移内容始终可以在 60fps 上完成。 然而,挑战在于,布局中看到的 viewport 可能与屏幕上实际可见的内容略有过时。 如果用户快速滚动,可能会超过 UI 线程生成新内容的速度,导致屏幕显示为黑色。 因此,虚拟化布局通常需要生成足以填充大于视口的区域的已准备元素的附加缓冲区。 在滚动期间负载较重时,用户仍然可以看到内容。

实现 rect

由于元素创建成本高昂,因此虚拟化容器(例如 ItemsRepeater)最初将为附加布局提供与视区匹配的 RealizationRect 。 在空闲时间,容器可以通过对布局进行重复调用,并使用越来越大的实现矩形,从而增大已准备内容缓冲区的大小。 此行为是一种性能优化,它尝试在快速启动时间和良好的平移体验之间取得平衡。 ItemsRepeater 将生成的最大缓冲区大小由 其 VerticalCacheLengthHorizontalCacheLength 属性控制。

重新使用元素 (回收)

布局应调整和定位元素,以在每次运行时填充 RealizationRect 。 默认情况下, VirtualizingLayout 将在每个布局传递结束时回收任何未使用的元素。

作为 MeasureOverrideArrangeOverride 的一部分传递给布局的 VirtualizingLayoutContext 提供了虚拟化布局所需的附加信息。 它提供的一些最常用功能包括:

  1. 查询数据中的项数(ItemCount)。
  2. 使用 GetItemAt 方法检索特定项。
  3. 检索一个 RealizationRect,表示布局应填充已实现元素的视口和缓冲区。
  4. 使用 GetOrCreateElement 方法请求特定项的 UIElement。

请求某个给定索引的元素将导致该元素在该次布局迭代中被标记为“正在使用”。 如果该元素尚不存在,则会被实例化并自动准备好使用(例如,扩展在 DataTemplate 中定义的 UI 树,处理任何数据绑定等)。 否则,将从现有实例池中检索它。

在每个度量值传递结束时,任何未标记为“正在使用”的现有已实现元素都将自动被视为可供重复使用,除非通过 GetOrCreateElementAt 方法检索元素时使用了 SuppressAutoRecycle 选项。 框架会自动将其移动到回收池并使其可用。 随后可能会被提取供其他容器使用。 框架会尽量避免这种情况,因为为元素重设父级会带来一些成本。

如果虚拟化布局在每次测量开始时知道哪些元素将不再落入实现矩形内,则可以优化其重用。 而不是依赖于框架的默认行为。 布局可以使用 RecycleElement 方法抢先地将元素移动到回收池。 在请求新元素之前调用此方法会导致当布局稍后针对尚未与元素关联的索引发出 GetOrCreateElementAt 请求时,这些现有元素将可用。

VirtualizingLayoutContext 提供了两个专为创建内容相关布局的布局作者设计的附加属性。 稍后将更详细地讨论它们。

  1. 一个 RecommendedAnchorIndex,它为布局提供了一个可选的输入
  2. 一个LayoutOrigin是布局的可选输出

依赖于数据的虚拟化布局

如果知道每个项的大小应该是什么,而无需测量要显示的内容,虚拟化布局会更容易。 在本文档中,我们只需将此类别的虚拟化 布局称为数据布局 ,因为它们通常涉及检查数据。 根据数据,应用可能会选取具有已知大小的视觉表示形式,可能是因为它属于数据部分,或者以前由设计决定。

一般方法是让布局:

  1. 计算每个项的大小和位置。
  2. 作为 MeasureOverride 的一部分:
    1. 使用 RealizationRect 确定应出现在视区中的项。
    2. 使用GetOrCreateElementAt方法检索对应项的UIElement。
    3. 度量 UIElement 使用预先计算的大小。
  3. 作为 ArrangeOverride 的一部分,排列每个已实际化的 UIElement,使其符合预先计算的位置。

注释

数据布局方法通常与 数据虚拟化不兼容。 具体而言,加载到内存中的唯一数据是填充用户可见内容所需的数据。 数据虚拟化不是指用户滚动到该数据保持驻留的位置时延迟或增量加载数据。 相反,它指的是当项目随着滚动而离开视图时从内存中释放。 作为数据布局的一部分检查每个数据项的数据布局会阻止数据虚拟化按预期工作。 一种例外是 UniformGridLayout 布局,它认为所有内容的大小相同。

小窍门

如果要为控件库创建自定义控件,该控件库将在各种情况下供其他人使用,则数据布局可能不是一个选项。

示例:Xbox活动源布局

Xbox 活动源的 UI 使用了一种重复图案,每行包含一个宽磁贴,后面跟着两个窄磁贴,而在接下来的一行中,这个顺序被反转。 在此布局中,每个项的大小都是项在数据集中的位置和磁贴的已知大小(宽与窄)的函数。

Xbox 活动源

下面的代码演示了活动源的自定义虚拟化 UI 是什么,以说明对 数据布局可能采用的一般方法。

小窍门

如果已安装 WinUI 3 示例库应用,请单击此处打开该应用并查看 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 在实现的元素和它们所表示的数据源中的索引之间保持映射。 布局可以选择通过 GetOrCreateElementAt 方法检索元素时始终请求 SuppressAutoRecycle 选项来管理此映射本身,从而阻止默认自动回收行为。 例如,布局可能会选择在滚动仅限于一个方向时执行这一设置,并且它所考虑的项目始终是连续的(也就是说,只需知道第一个和最后一个元素的索引,就能够确定所有应该实现的元素)。

示例: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; }
    // ...
}

依赖于内容的虚拟化布局

如果首先必须测量项目的 UI 内容以计算其确切尺寸,则它是 依赖于内容的布局。 可以将其视为一种布局,其中每个元素必须自己调整大小,而不是布局告诉元素其大小。 虚拟化属于此类别的布局更为复杂。

注释

依赖于内容的布局不会(不应)破坏数据虚拟化。

估计

依赖内容的布局依赖于估计来猜测未实现的内容的大小和实现的内容的位置。 随着这些估计的变化,它将导致已实现的内容定期在可滚动区域中移动位置。 如果不缓解,这可能会导致非常令人沮丧和不和谐的用户体验。 此处讨论了潜在问题和缓解措施。

注释

考虑每个项并知道所有项的确切大小、已实现或未实现,以及它们的位置的数据布局可以完全避免这些问题。

滚动定位

XAML 提供一种机制,通过实现 IScrollAnchorPovider 接口,让滚动控件支持滚动定位,从而缓解突然视区转移。 当用户操作内容时,滚动控件会持续从已选择进行追踪的候选项集之中选择一个元素。 如果定位点元素的位置在布局期间移动,则滚动控件会自动移动其视区以维护视区。

提供给布局的 RecommendedAnchorIndex 的值可能反映当前由滚动控件选择的定位点元素。 或者,如果开发人员显式请求在 ItemsRepeater 上使用 GetOrCreateElement 方法实现索引的元素,则会在下一个布局传递中将该索引指定为 RecommendedAnchorIndex。 这使布局可以针对开发人员实现元素的可能场景做好准备,并随后请求通过 StartBringIntoView 方法将其引入视图。

RecommendedAnchorIndex 是数据源中项的索引,依赖内容的布局在估计项的位置时应首先定位该索引。 它应作为定位其他已实现项的起点。

对 ScrollBar 的影响

即使使用滚动定位,如果布局的估计变化很大,可能是由于内容大小的显著变化,那么 ScrollBar 的拇指位置似乎会跳来跳去。 如果拇指在拖动鼠标指针时没有跟踪鼠标指针的位置,则用户可能会很不和谐。

布局的估计越准确,那么用户看到 ScrollBar 的拇指跳跃的可能性就越小。

布局更正

应准备好依赖内容的布局,以便根据现实来合理化其估计。 例如,当用户滚动到内容顶部且布局识别到第一个元素时,它可能会发现该元素相对于起始元素的预期位置会导致它出现在 (x:0, y:0) 之外的某个位置。 发生这种情况时,布局可以使用 LayoutOrigin 属性来设置其计算出的作为新布局原点的位置。 最终结果类似于滚动锚定,在这种情况下,滚动控件的视口会自动调整,以考虑布局报告的内容位置。

更正 LayoutOrigin

断开连接的视口

从布局的 MeasureOverride 方法返回的大小表示该内容尺寸的最佳估计,该尺寸可能会随着每次连续布局而变化。 当用户滚动时,将使用更新的 RealizationRect 持续重新评估布局。

如果用户非常快速地拖动滑块,从布局的角度来看,视图窗口可能会突然跳跃到与之前位置不重叠的新位置。 这是因为滚动的异步性质。 应用还可以请求通过布局将某个元素滚动到视图,以实现该元素的可视化,尽管该元素当前未实现并且预计位于布局所跟踪的当前范围之外。

当布局发现其猜测不正确并且/或看到意外视区移位时,它需要重新定位其起始位置。 作为 XAML 控件一部分交付的虚拟化布局是作为依赖内容的布局开发的,因为它们对要显示的内容的性质施加了较少的限制。

示例:适用于可变大小项的简单虚拟化堆栈布局

下面的示例演示了一个简单的可变大小项的堆栈布局:

  • 支持 UI 虚拟化,
  • 使用估计来猜测未实现的项目的大小,
  • 了解潜在的不连续视区转移,并且
  • 应用布局调整以适应这些变动。

用法:标记

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