Připojená rozložení

Kontejner (například Panel), který deleguje logiku rozložení na jiný objekt, závisí na připojeném objektu rozložení a poskytuje chování rozložení pro podřízené prvky. Připojený model rozložení poskytuje aplikaci flexibilitu při změně rozložení položek za běhu nebo snadnější sdílení aspektů rozložení mezi různými částmi uživatelského rozhraní (například položky v řádcích tabulky, které se zobrazují jako zarovnané ve sloupci).

V tomto tématu se podíváme na to, co obnáší vytvoření připojeného rozložení (virtualizujícího a nevirtualizujícího), na koncepty a třídy, které budete potřebovat pochopit, a na kompromisy, které budete muset zvážit při volbě mezi nimi.

Windows App SDK
Třídy rozložení popsané zde jsou součástí WinUI 3, které jsou součástí Windows App SDK. Další informace najdete v Windows App SDK přehledu.

Důležitá rozhraní API:

Klíčové koncepty

Provádění rozložení vyžaduje, aby byly pro každý prvek zodpovězeny dvě otázky:

  1. Jaká velikost bude tento prvek?

  2. Jaká bude pozice tohoto prvku?

Systém rozložení XAML, který na tyto otázky odpovídá, se stručně zabývá diskuzí o vlastních panelech.

Kontejnery a kontext

Panel XAML koncepčně vyplní dvě důležité role v rámci:

  1. Může obsahovat dětské prvky a zavádí větvení ve stromové struktuře.
  2. Použije pro tyto podřízené položky konkrétní strategii rozložení.

Z tohoto důvodu je panel v XAML často synonymem rozložení, ale technicky vzato dělá více než jen rozložení.

ItemsRepeater se také chová jako Panel, ale na rozdíl od Panelu nezpřístupňuje vlastnost Children, která by umožňovala programově přidávat nebo odebírat děti UIElement. Místo toho je životní cyklus jeho potomků automaticky spravován frameworkem tak, aby se shodoval s kolekcí datových položek. I když není odvozený od třídy Panel, chová se a je považován rámcem jako Panel.

Poznámka:

LayoutPanel je kontejner odvozený z panelu, který deleguje jeho logiku na připojený objekt Layout. LayoutPanel je ve verzi Preview a v současné době je k dispozici pouze v předběžných verzích balíčku WinUI 3.

Containers

Koncepčně je Panel kontejnerem prvků, který má také schopnost vykreslit pixely pro pozadí. Panely poskytují způsob zapouzdření běžné logiky rozložení v snadno použitelném balíčku.

Koncept připojeného rozložení rozlišuje dvě role kontejneru a rozložení jasněji. Pokud kontejner deleguje logiku rozložení na jiný objekt, zavoláme tento objekt jako připojené rozložení, jak je vidět v následujícím fragmentu kódu. Kontejnery, které dědí z FrameworkElement, například LayoutPanel, automaticky zveřejňují společné vlastnosti, které poskytují vstup procesu rozložení XAML (například Height a Width).

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

Během procesu rozložení se kontejner spoléhá na připojenou uniformGridLayout k měření a uspořádání podřízených položek.

Stav pro jednotlivé kontejnery

S připojeným rozložením může být jedna instance objektu rozložení přidružena k mnoha kontejnerům, jako je v následujícím fragmentu kódu; proto nesmí záviset na kontejneru hostitele ani přímo na něj odkazovat. Například:

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

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

Pro tuto situaci ExampleLayout musí pečlivě zvážit stav, který používá při výpočtu rozložení a kde je tento stav uložen, aby nedošlo k ovlivnění rozložení prvků na jednom panelu s druhým. Je to podobné vlastnímu panelu, jehož logika MeasureOverride a ArrangeOverride závisí na hodnotách jeho statických vlastností.

LayoutContext

Účelem layoutContext je řešit tyto výzvy. Poskytuje připojenému rozložení schopnost interakce s kontejnerem hostitele, například načítání podřízených prvků, bez zavedení přímé závislosti mezi těmito dvěma. Kontext také umožňuje rozložení uložit libovolný stav, který vyžaduje, aby mohlo souviset s podřízenými elementy kontejneru.

Jednoduché, ne virtualizační rozložení často nemusí udržovat žádný stav, takže se nejedná o problém. Složitější rozložení, například Grid, se však může rozhodnout zachovat stav mezi voláním měření a uspořádáním, aby se zabránilo opakovanému výpočtu hodnoty.

Virtualizace rozložení často musí udržovat určitý stav mezi měřením a uspořádáním i mezi průchody iterativního rozložení.

Inicializace a od-inicializace stavu každého kontejneru

Když je rozložení připojeno ke kontejneru, jeho InitializeForContextCore metoda je volána a poskytuje příležitost inicializovat objekt pro uložení stavu.

Podobně, když je rozložení odebráno z kontejneru, metoda UninitializeForContextCore bude volána. Tím se rozložení umožní vyčistit jakýkoli stav přidružený k danému kontejneru.

Objekt stavu rozložení lze uložit a načíst z kontejneru s vlastností LayoutState v kontextu.

Virtualizace uživatelského rozhraní

Virtualizace uživatelského rozhraní znamená zpoždění vytváření objektu uživatelského rozhraní, dokud nebude potřeba. Jedná se o optimalizaci výkonu. V případě nesouvacích scénářů můžete určit, kdy je to potřeba, na základě řady faktorů, které jsou specifické pro danou aplikaci. V takových případech by aplikace měly zvážit použití x:Load. Nevyžaduje žádné zvláštní úpravy v rámci rozvržení.

Ve scénářích založených na posouvání, jako je seznam, se často určuje, kdy je potřeba, na základě toho, zda "bude viditelný pro uživatele", což výrazně závisí na tom, kde byl umístěn během procesu rozložení, a vyžaduje zvláštní zvážení. Tento dokument se soustředí na tento scénář.

Poznámka:

I když se tento dokument nezabývá, stejné funkce, které umožňují virtualizaci uživatelského rozhraní ve scénářích posouvání, se dají použít ve scénářích bez posouvání. Například ovládací prvek ToolBar řízený daty, který spravuje životní cyklus příkazů, které prezentuje, a reaguje na změny v dostupném prostoru recyklováním a přesunem prvků mezi viditelnou oblastí a nabídkou přetečení.

Začínáme

Nejprve se rozhodněte, jestli rozložení, které potřebujete vytvořit, by mělo podporovat virtualizaci uživatelského rozhraní.

Pár věcí, které je potřeba mít na paměti...

  1. Nevirtuální rozložení se snadněji vytvářejí. Pokud bude počet položek vždy malý, doporučujeme vytvořit ne virtualizační rozložení.
  2. Platforma poskytuje sadu připojených rozložení, která pracují s ItemsRepeater a LayoutPanel pro pokrytí běžných potřeb. Seznamte se s nimi, než se rozhodnete definovat vlastní rozložení.
  3. Virtualizace rozložení má ve srovnání s nevirtualizačním rozložením vždy nějaké dodatečné náklady na procesor a paměť, složitost a režii. Obecně platí, že pokud podřízené položky, které musí rozložení spravovat, se pravděpodobně vejdou do oblasti, která je 3x velikostí oblasti zobrazení, pak z virtualizačního rozložení nemusí být moc výhoda. Velikost 3x je podrobněji popsána dále v tomto dokumentu, ale je způsobená asynchronní povahou posouvání na Windows a jeho dopadu na virtualizaci.

Návod

Výchozí nastavení pro ListView (a ItemsRepeater) jako referenční bod je, že recyklace nezačne, dokud počet položek nestačí k vyplnění trojnásobku velikosti aktuálního zobrazení.

Výběr základního typu

připojená hierarchie rozložení

Základní typ rozložení má dva odvozené typy, které slouží jako výchozí bod pro vytvoření připojeného rozložení:

  1. NonVirtualizingLayout
  2. VirtualizingLayout

Rozložení bez virtualizace

Přístup k vytvoření nevirtualizujícího rozložení by měl být známý všem, kdo vytvořili vlastní panel. Platí stejné koncepty. Primární rozdíl spočívá v tom, že pro přístup k kolekci Children se používá nonVirtualizingLayoutContext a rozložení se může rozhodnout pro uložení stavu.

  1. Odvoďte ze základního typu NonVirtualizingLayout (místo Panelu).
  2. (Volitelné) Definujte vlastnosti závislosti, které při změně zruší platnost rozložení.
  3. (Nové/volitelné) Inicializace libovolného objektu stavu vyžadovaného rozložením jako součást InitializeForContextCore. Uložte ho s hostitelským kontejnerem pomocí LayoutState poskytnutého s kontextem.
  4. Přepište MeasureOverride a zavolejte metodu Measure na všechny děti.
  5. Přepište ArrangeOverride a zavolejte metodu Arrange pro všechny podřízené prvky.
  6. (Nové/volitelné) Vyčistí všechny uložené stavy jako součást UninitializeForContextCore.

Příklad: Jednoduché rozložení stacku (položky různé velikosti)

MyStackLayout

Tady je velmi základní nevirtualizující uspořádání zásobníku různě velkých položek. Nemá žádné vlastnosti pro úpravu chování rozložení. Následující implementace ukazuje, jak rozložení závisí na objektu kontextu poskytovaném kontejnerem:

  1. Získejte počet podřízených prvků a
  2. Přistupte k jednotlivým podřízeným prvkům pomocí indexu.
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>

Virtualizace rozložení

Podobně jako u ne virtualizačního rozložení jsou kroky vysoké úrovně pro virtualizační rozložení stejné. Složitost je z velké části při určování prvků, které spadají do oblasti zobrazení, a měly by být realizovány.

  1. Odvozujte od základního typu VirtualizingLayout.
  2. (Volitelné) Definujte vlastnosti závislosti, které při změně zruší platnost rozložení.
  3. Iniciujte jakýkoli objekt stavu, který bude vyžadován rozložením jako součást InitializeForContextCore. Uložte ho s hostitelským kontejnerem pomocí LayoutState poskytnutého s kontextem.
  4. Přepište MeasureOverride a zavolejte metodu Measure pro každého potomka, který by měl být realizován.
    1. GetOrCreateElementAt metoda se používá k načtení UIElement, který byl připraven rozhraním (například datové vazby použité).
  5. Přepište ArrangeOverride a zavolejte metodu Arrange pro každé realizované podřízené.
  6. (Volitelné) Vyčistí všechny uložené stavy jako součást UninitializeForContextCore.

Návod

Hodnota vrácená MeasureOverride se používá jako velikost virtualizovaného obsahu.

Při vytváření virtualizačního rozložení je potřeba zvážit dva obecné přístupy. Zda zvolit jeden nebo druhý z velké části závisí na tom, "jak určíte velikost prvku". Pokud stačí znát index položky v sadě dat nebo samotná data určuje jeho konečnou velikost, považujeme ji za závislá na datech. To je jednodušší vytvořit. Pokud je ale jediným způsobem, jak určit velikost položky, je vytvořit a změřit uživatelské rozhraní, pak bychom řekli, že je závislý na obsahu. Jedná se o složitější věci.

Proces rozložení

Bez ohledu na to, jestli vytváříte data nebo rozložení závislé na obsahu, je důležité pochopit proces rozložení a vliv asynchronního posouvání Windows.

Zjednodušené zobrazení kroků provedených architekturou od spuštění až po zobrazení uživatelského rozhraní na obrazovce:

  1. Analyzuje značkovací jazyk.

  2. Vygeneruje strom prvků.

  3. Provede průchod rozložení.

  4. Provede průchod vykreslení.

Při virtualizaci uživatelského rozhraní se vytváření prvků, která by se normálně prováděla ve druhém kroku, zpozdí nebo ukončí dříve, jakmile je zjištěno, že byl vytvořen dostatečný obsah k vyplnění zobrazovacího okna. Virtualizační kontejner (například ItemsRepeater) spoléhá na připojené rozložení k řízení tohoto procesu. Poskytuje přiložené rozložení s VirtualizingLayoutContext, které zobrazuje další informace potřebné pro virtualizaci rozložení.

RealizationRect (tj. Viewport)

Posouvání na Windows probíhá asynchronně s vláknem uživatelského rozhraní. Není řízeno rozložením rámce. Interakce a pohyb se spíše vyskytují ve kompozitoru systému. Výhodou tohoto přístupu je, že lze vždy zajistit, aby se obsah posouval při 60 fps. Výzvou však je, že "zobrazovací plocha", jak ji vidí rozložení, může být mírně zastaralá ve srovnání s tím, co je skutečně viditelné na obrazovce. Pokud se uživatel rychle posune, může předběhnout rychlost vlákna uživatelského rozhraní, což způsobí, že obrazovka se zobrazí černě. Z tohoto důvodu je často nutné, aby virtualizační rozložení vygenerovalo dodatečnou vyrovnávací paměť připravených prvků, která stačí k vyplnění oblasti větší než oblast zobrazení. Při větším zatížení při posouvání se uživateli stále zobrazuje obsah.

Realizace rect

Vzhledem k tomu, že vytváření elementů je nákladné, virtualizace kontejnerů (například ItemsRepeater) zpočátku poskytne připojené rozložení s RealizationRect, který se shoduje s viewportem. V době nečinnosti může kontejner zvětšit vyrovnávací paměť připraveného obsahu opakovaným vyvoláním rozvržení pomocí stále většího obdélníku realizace. Toto chování je optimalizace výkonu, která má za cíl dosáhnout rovnováhy mezi rychlou spouštěcí dobou a dobrým zážitkem z posouvání. Maximální velikost vyrovnávací paměti, kterou generuje ItemsRepeater, je řízena svými vlastnostmi VerticalCacheLength a HorizontalCacheLength.

Opětovné použití elementů (recyklace)

Očekává se, že rozložení nastaví velikost a umístění prvků tak, aby při každém spuštění vyplnily RealizationRect. VirtualizingLayout ve výchozím nastavení recykluje všechny nepoužité prvky na konci každého průchodu rozložení.

VirtualizingLayoutContext, který se předává do rozložení jako součást MeasureOverride a ArrangeOverride, poskytuje další informace, které virtualizující rozložení potřebuje. Mezi nejčastěji používané možnosti patří:

  1. Zadejte dotaz na počet položek v datech (ItemCount).
  2. Načtěte konkrétní položku pomocí metody GetItemAt .
  3. Načtěte RealizationRect, které představuje oblast zobrazení a vyrovnávací paměť, jež by mělo být vyplněno realizovanými prvky rozložením.
  4. Vyžádejte si prvek UIElement pro konkrétní položku pomocí metody GetOrCreateElementAt.

Vyžádání elementu pro daný index způsobí, že se tento prvek označí jako "v použití" pro daný průchod rozložení. Pokud prvek ještě neexistuje, bude realizován a automaticky připraven k použití (například nafouknutí stromu uživatelského rozhraní definovaného v DataTemplate, zpracování jakékoli datové vazby atd.). Jinak se načte z fondu existujících instancí.

Na konci každé míry projde každý existující, realizovaný element, který nebyl označen jako "v použití", je automaticky považován za dostupný pro opětovné použití, pokud nebyla použita možnost SuppressAutoRecycle při načtení elementu prostřednictvím metody GetOrCreateElementAt. Rámec ho automaticky přesune do recyklačního fondu a zpřístupní ho. Může být následně načten k použití jiným kontejnerem. Framework se snaží tomu zabránit, pokud je to možné, protože jsou spojeny určité náklady se znovu nastavením rodiče prvku.

Pokud virtualizující rozložení ví na začátku každého měření, které prvky již nebudou v rámci realizačního obdélníku, pak může optimalizovat své opětovné použití. Místo toho, abyste se spoléhali na výchozí chování architektury. Rozložení může prvky předběžně přesunout do fondu recyklace pomocí metody RecycleElement. Volání této metody před vyžádáním nových prvků způsobí, že tyto existující prvky budou k dispozici, když později rozložení vydá požadavek GetOrCreateElementAt pro index, který ještě není přidružen k elementu.

VirtualizingLayoutContext poskytuje dvě další vlastnosti navržené pro autory rozložení vytvářející rozložení závislé na obsahu. Podrobněji se probírají později.

  1. A RecommendedAnchorIndex, která poskytuje volitelný vstup pro rozložení.
  2. A LayoutOrigin, který může být volitelným výstupem rozložení.

Virtualizace rozložení závislá na datech

Virtualizace rozložení je jednodušší, pokud víte, jakou velikost každé položky by měla být, aniž byste museli měřit obsah, který se má zobrazit. V tomto dokumentu budeme jednoduše odkazovat na tuto kategorii rozložení jako na rozložení dat, protože obvykle zahrnují inspekci dat. Na základě dat může aplikace vybrat vizuální reprezentaci se známou velikostí, například protože je součástí těchto dat nebo byla určena návrhem.

Obecný přístup k rozložení je následující:

  1. Vypočítejte velikost a umístění každé položky.
  2. Jako součást MeasureOverride:
    1. Pomocí RealizationRect určete, které položky se mají zobrazit ve výřezu.
    2. Načtěte UIElement, která by měla představovat položku pomocí GetOrCreateElementAt metoda.
    3. Změřte uiElement s předem vypočítanou velikostí.
  3. Jako součást ArrangeOverride, uspořádejte každý realizovaný UIElement s předem vypočítanou pozicí.

Poznámka:

Přístup k rozložení dat je často nekompatibilní s virtualizací dat. Konkrétně tam, kde jediná data načtená do paměti jsou data potřebná k vyplnění toho, co je viditelné pro uživatele. Virtualizace dat neodkazuje na opožděné ani přírůstkové načítání dat při posouvání dolů uživatelem, kde tato data zůstávají rezidentní. Spíše se jedná o to, kdy se položky uvolní z paměti, když se posunou mimo zobrazení. Rozložení dat, které kontroluje každou datovou položku jako součást svého strukturování, by zabránilo očekávanému fungování virtualizace dat. Výjimkou je rozložení, jako je UniformGridLayout, které předpokládá, že všechno má stejnou velikost.

Návod

Pokud vytváříte vlastní ovládací prvek pro knihovnu ovládacích prvků, kterou budou používat jiní uživatelé v nejrůznějších situacích, nemusí být rozložení dat pro vás možností.

Příklad: Xbox rozložení informačního kanálu o aktivitách

Uživatelské rozhraní informačního kanálu o aktivitách Xbox používá opakující se vzor, ve kterém má každý řádek širokou dlaždici, následovanou dvěma úzkými dlaždicemi, které jsou invertovány na následujícím řádku. V tomto rozložení je velikost každé položky funkcí pozice v sadě dat a známé velikosti dlaždic (široké vs. úzké).

informační kanál o aktivitách Xbox

Následující kód vás provede vlastním virtualizačním uživatelským rozhraním informačního kanálu o aktivitách, který ilustruje obecný přístup, který můžete použít pro rozložení dat.

Návod

Pokud máte nainstalovanou aplikaci Galerie WinUI 3, kliknutím sem aplikaci otevřete a uvidíte ItemsRepeater v akci. Získejte aplikaci z Microsoft Store nebo získejte zdrojový kód na 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;
}

(Volitelné) Správa mapování položek na UIElement

VirtualizingLayoutContext ve výchozím nastavení udržuje mapování mezi realizované prvky a indexem ve zdroji dat, který představují. Rozložení se může rozhodnout pro správu samotného mapování tak, že při načítání elementu prostřednictvím metody GetOrCreateElementAt vždy požaduje možnost SuppressAutoRecycle, která brání výchozímu chování automatické recyklace. Rozložení se může rozhodnout učinit toto například, pokud bude použito pouze tehdy, když je posouvání omezeno na jeden směr a položky, které zohledňuje, budou vždy souvislé (tj. znalost indexu prvního a posledního prvku postačuje k určení všech prvků, které mají být realizovány).

Příklad: míra informačního kanálu o aktivitách Xbox

Následující fragment kódu ukazuje další logiku, kterou je možné přidat do MeasureOverride v předchozí ukázce pro správu mapování.

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

Virtualizace rozložení závislá na obsahu

Pokud musíte nejprve změřit obsah uživatelského rozhraní pro položku, aby se zjistila jeho přesná velikost, jedná se o rozložení závislé na obsahu. Můžete si ho také představit jako rozložení, ve kterém každá položka musí mít velikost sama, a ne jako rozložení s informací o jeho velikosti. Virtualizace rozložení, která spadají do této kategorie, jsou složitější.

Poznámka:

Rozložení závislá na obsahu nemají (nemělo by) narušit virtualizaci dat.

Odhady

Rozložení závislá na obsahu závisí na odhadu, aby odhadla velikost nerealizovaného obsahu i pozici realizovaného obsahu. Vzhledem k tomu, že se tyto odhady mění, to způsobí, že realizovaný obsah pravidelně mění své pozice v posuvném prostoru. To může vést k velmi frustrujícímu a narušujícímu uživatelskému prostředí, pokud se tomu nezabrání. Zde jsou popsány potenciální problémy a zmírnění rizik.

Poznámka:

Rozložení dat, která berou v úvahu každou položku a znají přesnou velikost všech položek, realizované nebo ne, a jejich pozice se můžou těmto problémům zcela vyhnout.

Ukotvení při posouvání

XAML poskytuje mechanismus pro zmírnění náhlého posunu oblasti zobrazení tím, že posouvání ovládacích prvků podporuje ukotvení posouvání implementací rozhraní IScrollAnchorProvider . Když uživatel manipuluje s obsahem, ovládací prvek posouvání průběžně vybírá prvek ze sady kandidátů, které byly přihlášeny ke sledování. Pokud se pozice prvku ukotvení během rozložení posune, ovládací prvek posouvání automaticky posune jeho oblast zobrazení, aby se zachovala oblast zobrazení.

Hodnota RecommendedAnchorIndex poskytnutá rozložení může odrážet aktuálně vybraný prvek ukotvení vybraný ovládacím prvkem posouvání. Alternativně, pokud vývojář explicitně požaduje, aby prvek byl realizován pro index s metodou GetOrCreateElement na ItemsRepeater, pak tento index je nastaven jako RecommendedAnchorIndex při dalším průchodu rozložením. To umožňuje, aby rozložení bylo připravené pro pravděpodobný scénář, že vývojář si uvědomí prvek a následně požádá o zobrazení prostřednictvím metody StartBringIntoView .

RecommendedAnchorIndex je index položky ve zdroji dat, kterou by rozložení závislé na obsahu mělo umístit jako první při odhadu umístění svých položek. Měl by sloužit jako výchozí bod pro umístění ostatních realizovaných položek.

Dopad na posuvníky

I při ukotvení rolování, pokud se odhady rozložení hodně liší, například kvůli významným změnám ve velikosti obsahu, může se zdát, že pozice posuvníkového ukazatele skáče sem a tam. To může pro uživatele působit rušivě, pokud se zdá, že palec nesleduje pozici ukazatele myši při přetahování.

Čím přesnější mohou být odhady rozložení, tím méně pravděpodobné je, že uživatel uvidí posun palce posuvníku.

Opravy rozložení

Rozložení založené na obsahu by mělo být připraveno, aby byl odhad porovnán s realitou. Když se například uživatel posune na začátek obsahu a rozložení si uvědomí úplně první prvek, může zjistit, že očekávaná pozice elementu vzhledem k prvku, ze kterého začala, způsobí, že se zobrazí někde jinde než zdroj (x:0, y:0). Pokud k tomu dojde, rozložení může použít LayoutOrigin vlastnost nastavit pozici vypočítanou jako nový původ rozložení. Čistý výsledek je podobný ukotvení posouvání, kde je zorné pole řídicího prvku pro posouvání automaticky upraveno tak, aby odpovídalo pozici obsahu podle rozvržení.

Oprava rozloženíOrigin

Odpojené oblasti zobrazení

Velikost vrácená metodou MeasureOverride rozložení představuje nejlepší odhad velikosti obsahu, která se může změnit s každým následným rozložením. Když uživatel posune rozložení, bude se průběžně znovu vyhodnocovat s aktualizovaným objektem RealRealIzaceRect.

Pokud uživatel přetáhne palec velmi rychle, může se zdát, že oblast zobrazení v kontextu rozložení provádí velké skoky, při kterých se předchozí pozice nepřekrývá s aktuální pozicí. Důvodem je asynchronní povaha posouvání. Je také možné, aby aplikace, která využívá rozložení, požadovala, aby prvek byl přenesen do zobrazení pro položku, která není aktuálně realizována, a odhaduje se, že leží mimo aktuální rozsah sledovaný rozložením.

Když rozložení zjistí, že jeho odhad není správný nebo se zobrazí neočekávaný posun oblasti zobrazení, musí změnit orientaci počáteční pozice. Virtualizační rozložení, která se dodávají jako součást ovládacích prvků XAML, se vyvíjejí jako rozložení závislá na obsahu, protože umisťují méně omezení povahy obsahu, který se zobrazí.

Příklad: Jednoduchá virtualizace stohového rozložení pro položky s proměnlivou velikostí

Následující ukázka ukazuje jednoduché rozložení zásobníku pro položky s proměnlivou velikostí, které:

  • podporuje virtualizaci uživatelského rozhraní,
  • používá odhady k odhadu velikosti nespravovaných položek,
  • je si vědom potenciálních přerušení posunů zobrazení a
  • uplatňuje opravy rozložení pro vyrovnání těchto změn.

Použití: Označení

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

Kód v zákulisí: 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;

Kód: 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);
    }
}