Dołączone układy

Kontener (na przykład Panel), który deleguje logikę układu do innego obiektu, opiera się na dołączonym obiekcie układu w celu zapewnienia zachowania układu dla jego elementów podrzędnych. Dołączany model układu rozmieszczenia umożliwia aplikacji łatwe zmienianie układu elementów w czasie pracy lub łatwiejsze dzielenie się aspektami układu między różnymi częściami interfejsu użytkownika (na przykład elementami w wierszach tabeli, które wydają się być wyrównane w kolumnie).

W tym temacie omówimy zagadnienia związane z tworzeniem dołączonego układu (wirtualizacja i niewirtualizowanie), pojęciami i klasami, które należy zrozumieć, oraz kompromisami, które należy wziąć pod uwagę podczas podejmowania decyzji między nimi.

Zestaw SDK do aplikacji systemu Windows
Opisane tutaj klasy układu są częścią interfejsu WinUI 3 uwzględnionego w Zestaw SDK do aplikacji systemu Windows. Aby uzyskać więcej informacji, zobacz Zestaw SDK do aplikacji systemu Windows omówienie.

Ważne interfejsy API:

Kluczowe pojęcia

Wykonanie układu wymaga odpowiedzi na dwa pytania dla każdego elementu:

  1. Jaki rozmiar będzie ten element?

  2. Jaka będzie pozycja tego elementu?

System układu XAML, który odpowiada na te pytania, jest krótko omówiony w ramach dyskusji na temat paneli niestandardowych.

Kontenery i kontekst

Koncepcyjnie panel XAML wypełnia dwie ważne role w strukturze:

  1. Może zawierać elementy podrzędne i wprowadza rozgałęzianie w drzewie elementów XML.
  2. Stosuje konkretną strategię układu do tych elementów podrzędnych.

Z tego powodu panel w języku XAML często był synonimem układu, ale technicznie mówiąc, robi coś więcej niż tylko układ.

Element ItemsRepeater zachowuje się również tak jak Panel, ale w przeciwieństwie do Panelu, nie uwidacznia właściwości Children, która umożliwia programowe dodawanie lub usuwanie elementów podrzędnych UIElement. Zamiast tego okres istnienia jej elementów podrzędnych jest automatycznie zarządzany przez platformę w celu odpowiadania kolekcji elementów danych. Chociaż nie pochodzi on z Panelu, zachowuje się i jest traktowany przez framework jak Panel.

Uwaga / Notatka

LayoutPanel to kontener pochodzący z panelu, który deleguje logikę do dołączonego obiektu Layout. LayoutPanel jest w wersji zapoznawczej i jest obecnie dostępny tylko w wersjach wstępnych pakietu WinUI 3.

Containers

Koncepcyjnie Panel jest kontenerem elementów, które mają również możliwość renderowania pikseli dla tła. Panele umożliwiają hermetyzowanie typowej logiki układu w łatwym do użycia pakiecie.

Koncepcja dołączonego układu sprawia, że rozróżnienie między dwiema rolami kontenera i układem jest bardziej jasne. Jeśli kontener deleguje logikę układu graficznego do innego obiektu, nazwiemy ten obiekt dołączonym układem, jak pokazano w poniższym fragmencie kodu. Kontenery dziedziczone z elementu FrameworkElement, takie jak LayoutPanel, automatycznie uwidaczniają typowe właściwości, które zapewniają dane wejściowe do procesu układu XAML (na przykład Wysokość i Szerokość).

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

Podczas procesu układu kontener korzysta z dołączonego elementu UniformGridLayout do mierzenia i rozmieszczania jego elementów podrzędnych.

Stan dla każdego kontenera

W przypadku dołączonego układu pojedyncze wystąpienie obiektu układu może być skojarzone z wieloma kontenerami, takimi jak w poniższym fragmencie kodu; w związku z tym nie może zależeć od kontenera hostów ani odwoływać się bezpośrednio do niego. Przykład:

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

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

W takiej sytuacji PrzykładLayout musi dokładnie rozważyć stan używany w obliczeniach układu i miejsce przechowywania tego stanu, aby uniknąć wpływu na układ elementów w jednym panelu z drugim. Byłoby to analogiczne do niestandardowego panelu, którego logika MeasureOverride i ArrangeOverride zależy od wartości jego właściwości statycznych .

LayoutContext

Celem obiektu LayoutContext jest radzenie sobie z tymi wyzwaniami. Zapewnia on dołączony układ możliwość interakcji z kontenerem hosta, takim jak pobieranie elementów podrzędnych, bez wprowadzania bezpośredniej zależności między tymi dwoma elementami. Kontekst umożliwia również układowi przechowywanie dowolnego stanu, który może być powiązany z elementami podrzędnymi kontenera.

Proste, niezwirtualizujące układy często nie muszą utrzymywać żadnego stanu, co nie stanowi problemu. Bardziej złożony układ, taki jak siatka, może jednak zdecydować się zachować stan pomiędzy operacjami mierzenia i rozmieszczania, aby uniknąć ponownego obliczania wartości.

Układy wirtualizacji często wymagają zachowania pewnego stanu zarówno pomiędzy fazami miary i rozmieszczenia, jak i pomiędzy kolejnych przebiegach układu iteracyjnego.

Inicjalizowanie i deinicjalizowanie stanu dla każdego kontenera

Gdy układ jest przypisany do kontenera, wywoływana jest jego metoda InitializeForContextCore, co stwarza możliwość zainicjowania obiektu w celu przechowywania stanu.

Podobnie po usunięciu układu z kontenera zostanie wywołana metoda UninitializeForContextCore . Daje to układowi możliwość wyczyszczenia dowolnego stanu skojarzonego z tym kontenerem.

Obiekt stanu układu może być przechowywany w kontenerze i pobierany z kontenera za pomocą właściwości LayoutState w kontekście.

Wirtualizacja interfejsu użytkownika

Wirtualizacja interfejsu użytkownika oznacza opóźnienie tworzenia obiektu interfejsu użytkownika do momentu, gdy będzie potrzebny. Jest to optymalizacja wydajności. W przypadku scenariuszy bez przewijania określenie , kiedy jest to konieczne, może opierać się na dowolnej liczbie elementów specyficznych dla aplikacji. W takich przypadkach aplikacje powinny rozważyć użycie elementu x:Load. Nie wymaga żadnej specjalnej obsługi w twoim układzie.

W scenariuszach z przewijaniem, takich jak lista, określenie potrzeby często bazuje na tym, czy będzie widoczne dla użytkownika, co w dużym stopniu zależy od tego, gdzie zostało umieszczone podczas procesu układu i wymaga specjalnych uwag. Ten dokument koncentruje się na tym scenariuszu.

Uwaga / Notatka

Chociaż nie jest to omówione w tym dokumencie, te same możliwości, które umożliwiają wirtualizację interfejsu użytkownika w scenariuszach przewijania, mogą być stosowane w scenariuszach nieprzewijania. Na przykład sterowana danymi kontrolka ToolBar, która zarządza okresem istnienia prezentowanych poleceń i reaguje na zmiany dostępnego miejsca przez odtwarzanie /przenoszenie elementów między widocznym obszarem a menu przepełnienia.

Wprowadzenie

Najpierw zdecyduj, czy układ, który chcesz utworzyć, powinien obsługiwać wirtualizację interfejsu użytkownika.

Kilka rzeczy, o których należy pamiętać...

  1. Układy niezwirtualizujące są łatwiejsze do utworzenia. Jeśli liczba elementów będzie zawsze mała, zaleca się tworzenie układu bez wirtualizacji.
  2. Platforma udostępnia zestaw dołączonych układów, które współpracują z elementami ItemsRepeater i LayoutPanel w celu zaspokojenia typowych potrzeb. Przed podjęciem decyzji o konieczności zdefiniowania układu niestandardowego zapoznaj się z tymi elementami.
  3. Układy wirtualizacji zawsze mają dodatkowy koszt procesora CPU i pamięci/złożoność/obciążenie w porównaniu z układem niewirtualizującym. Ogólnie rzecz biorąc, jeśli elementy podrzędne, którymi układ będzie musiał zarządzać, będą prawdopodobnie mieścić się w obszarze trzy razy większym od widoku, wówczas może nie być zbyt wiele korzyści z układu wirtualizującego. Rozmiar 3x jest omówiony bardziej szczegółowo w dalszej części tego dokumentu, ale wynika z asynchronicznego charakteru przewijania w Windows oraz jego wpływu na wirtualizację.

Wskazówka

Jako punkt odniesienia, domyślne ustawienia ListView (i ItemsRepeater) polegają na tym, że recykling nie rozpoczyna się, dopóki liczba elementów nie będzie wystarczająca do wypełnienia 3 razy rozmiaru bieżącego widoku.

Wybierz typ podstawowy

dołączona hierarchia układu

Typ układu podstawowego ma dwa typy pochodne, które służą jako punkt początkowy tworzenia dołączonego układu:

  1. Układ Bez Wirtualizacji
  2. UkładWirtualizacyjny

Układ niezwirtualizujący

Podejście do tworzenia układu niewirtualizującego powinno być dobrze znane każdemu, kto miał do czynienia z tworzeniem paneli niestandardowych. Te same pojęcia mają zastosowanie. Podstawową różnicą jest to, że obiekt NonVirtualizingLayoutContext jest używany do uzyskiwania dostępu do kolekcji Elementy i układ może zdecydować się na przechowywanie stanu.

  1. Pochodzi z typu podstawowego NonVirtualizingLayout (zamiast Panelu).
  2. (Opcjonalnie) Zdefiniuj właściwości zależności, które po zmianie spowodują unieważnienie układu.
  3. (Nowy/Opcjonalny) Zainicjuj dowolny obiekt stanu wymagany przez układ w ramach elementu InitializeForContextCore. Ukryj to w kontenerze głównym, używając LayoutState, dostarczonego z kontekstem.
  4. Zastąpij metodę MeasureOverride i wywołaj metodę Measure dla wszystkich elementów podrzędnych.
  5. Zastąpij metodę ArrangeOverride i wywołaj metodę Arrange dla wszystkich elementów podrzędnych.
  6. (Nowy/Opcjonalny) Wyczyść wszystkie zapisane stany w ramach elementu UninitializeForContextCore.

Przykład: Prosty układ stosu (elementy o różnych rozmiarach)

MyStackLayout

Oto bardzo podstawowy, niewirtualizujący układ stosu o różnych rozmiarach elementów. Brak jakichkolwiek właściwości, które pozwalałyby dostosować zachowanie układu. W poniższej implementacji ilustruje się, w jaki sposób układ korzysta z obiektu kontekstu dostarczanego przez kontener do:

  1. Pobierz liczbę dzieci i
  2. Uzyskaj dostęp do każdego elementu podrzędnego według indeksu.
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>

Układy wirtualizacji

Podobnie jak w przypadku układu niewirtualizowania, ogólne kroki dotyczące układu wirtualizacji są takie same. Złożoność polega w dużej mierze na określaniu, jakie elementy będą mieścić się w obszarze wyświetlania i powinny zostać renderowane.

  1. Pochodzi z podstawowego typu VirtualizingLayout.
  2. (Opcjonalnie) Zdefiniuj właściwości zależności, które po zmianie spowodują unieważnienie układu.
  3. Zainicjuj dowolny obiekt stanu, który będzie wymagany przez układ w ramach elementu InitializeForContextCore. Ukryj to w kontenerze głównym, używając LayoutState, dostarczonego z kontekstem.
  4. Zastąp metodę MeasureOverride i wywołaj metodę Measure dla każdego elementu podrzędnego, który należy zrealizować.
    1. Metoda GetOrCreateElementAt służy do pobierania elementu UIElement, który został przygotowany przez strukturę (na przykład zastosowane powiązania danych).
  5. Zastąpij metodę ArrangeOverride i wywołaj metodę Rozmieść dla każdego zrealizowanego elementu podrzędnego.
  6. (Opcjonalnie) Wyczyść jakikolwiek zapisany stan w ramach elementu UninitializeForContextCore.

Wskazówka

Wartość zwracana przez metodę MeasureOverride jest używana jako rozmiar zwirtualizowanej zawartości.

Istnieją dwa ogólne podejścia do rozważenia podczas tworzenia układu wirtualizacji. Wybór jednego lub drugiego zależy w dużej mierze od "sposobu określenia rozmiaru elementu". Jeśli wystarczy znać indeks elementu w zestawie danych lub same dane dyktują jego ostateczną wielkość, rozważmy, że jest zależna od danych. Są one bardziej proste do utworzenia. Jeśli jednak jedynym sposobem określenia rozmiaru elementu jest utworzenie i zmierzenie interfejsu użytkownika, oznacza to, że jest to zależne od zawartości. Są one bardziej złożone.

Proces układu

Niezależnie od tego, czy tworzysz układ danych, czy zależny od zawartości, ważne jest, aby zrozumieć proces układu i wpływ asynchronicznego przewijania Windows.

Uproszczony widok kroków wykonywanych przez framework od uruchamiania do wyświetlania interfejsu użytkownika na ekranie to:

  1. Analizuje znaczniki.

  2. Generuje drzewo elementów.

  3. Wykonuje przebieg układu.

  4. Przeprowadza przepust renderowania.

W przypadku wirtualizacji interfejsu użytkownika tworzenie elementów, które normalnie byłyby wykonywane w kroku 2, jest opóźnione lub kończy się wcześnie po ustaleniu, że utworzono wystarczającą zawartość do wypełnienia widoku. Kontener wirtualizacji (na przykład ItemsRepeater) przekazuje kontrolę do przypisanego układu, aby sterować tym procesem. Udostępnia on dołączony układ z elementem VirtualizingLayoutContext , który przedstawia dodatkowe informacje wymagane przez układ wirtualizacji.

The RealizationRect (tj. Viewport)

Przewijanie w systemie Windows odbywa się asynchronicznie względem wątku interfejsu użytkownika. Nie jest on kontrolowany przez układ struktury. Zamiast tego interakcja i ruch ma miejsce w kompozytorze systemu. Zaletą tego podejścia jest to, że przesuwanie zawartości można zawsze wykonywać przy 60fps. Wyzwaniem jest jednak to, że "viewport", jak widać w układzie, może być nieco nieaktualny w stosunku do tego, co jest rzeczywiście widoczne na ekranie. Jeśli użytkownik szybko przewija się, może prześcignąć szybkość wątku interfejsu użytkownika w celu wygenerowania nowej zawartości i "przesuwania na czarno". Z tego powodu często konieczne jest, aby układ wirtualizujący generował dodatkowy bufor przygotowanych elementów, wystarczający do wypełnienia obszaru większego niż obszar widoczny. Gdy podczas przewijania system jest bardziej obciążony, użytkownik nadal widzi treść.

Prostokąt realizacji

Ponieważ tworzenie elementów jest kosztowne, wirtualizacja kontenerów (na przykład ItemsRepeater) początkowo zapewni dołączonemu układowi RealizationRect zgodną z portem widoku. W czasie bezczynności kontener może zwiększyć bufor przygotowanej zawartości, wykonując wielokrotne wywołania rozmieszczenia przy pomocy coraz większego prostokąta realizacji. To zachowanie jest optymalizacją wydajności, która próbuje osiągnąć równowagę między szybkim czasem uruchamiania a dobrym wrażeniem przewijania. Maksymalny rozmiar buforu generowany przez element ItemsRepeater jest kontrolowany przez jego właściwości VerticalCacheLength i HorizontalCacheLength .

Ponowne używanie elementów (odtwarzanie)

Oczekuje się, że układ dopasuje rozmiar i rozmieści elementy, aby wypełnić RealizationRect za każdym razem, gdy jest uruchamiany. Domyślnie VirtualizingLayout będzie przetwarzać ponownie nieużywane elementy na końcu każdego przejścia układu.

VirtualizingLayoutContext przekazywany do układu w ramach MeasureOverride i ArrangeOverride dostarcza dodatkowych informacji potrzebnych do wirtualizacji układu. Niektóre z najczęściej używanych rzeczy, które zapewnia, to możliwość:

  1. Wykonaj zapytanie dotyczące liczby elementów w danych (ItemCount).
  2. Pobierz określony element przy użyciu metody GetItemAt .
  3. Pobierz wartość RealizationRect reprezentującą widokport i bufor, który układ powinien wypełniać zrealizowanymi elementami.
  4. Pobierz obiekt UIElement dla określonego elementu za pomocą metody GetOrCreateElementAt.

Żądanie elementu dla danego indeksu spowoduje, że ten element zostanie oznaczony jako "w użyciu" dla tej iteracji schematu układu. Jeśli element jeszcze nie istnieje, zostanie zrealizowany i automatycznie przygotowany do użycia (na przykład zawyżanie drzewa interfejsu użytkownika zdefiniowanego w elemecie DataTemplate, przetwarzanie dowolnego powiązania danych itp.). W przeciwnym razie zostanie pobrany z puli istniejących wystąpień.

Na zakończenie każdego przebiegu procesu mierzenia, istniejący, zrealizowany element, który nie został oznaczony jako "w użyciu", jest automatycznie uznawany za dostępny do ponownego użycia, chyba że opcja SuppressAutoRecycle była używana przy pobieraniu elementu za pomocą metody GetOrCreateElementAt. Platforma automatycznie przenosi ją do puli recyklingu i udostępnia ją. Może zostać następnie pobrany do użycia przez inny kontener. Struktura próbuje tego uniknąć, gdy jest to możliwe, ponieważ istnieje jakiś koszt związany z ponownym rodzicielsowaniem elementu.

Jeśli układ wirtualizacji wie na początku każdego zmierzania, które elementy nie będą już mieścić się w prostokącie realizacji, może zoptymalizować ponowne wykorzystanie. Zamiast polegać na domyślnym zachowaniu platformy. Układ może przy użyciu metody RecycleElement wstępnie przenosić elementy do puli recyklingu. Wywołanie tej metody przed żądaniem nowych elementów powoduje, że istniejące elementy będą dostępne, gdy układ później wystawia żądanie GetOrCreateElementAt dla indeksu, który nie jest jeszcze skojarzony z elementem.

Element VirtualizingLayoutContext udostępnia dwie dodatkowe właściwości przeznaczone dla autorów układów tworzących układ zależny od zawartości. Zostaną one omówione bardziej szczegółowo później.

  1. Zalecany Indeks Kotwicy, który udostępnia opcjonalne dane wejściowe do układu.
  2. Element LayoutOrigin, który jest opcjonalnym wyjściem układu.

Układy wirtualizacji zależne od danych

Układ wirtualizacji jest łatwiejszy, jeśli wiesz, jaki rozmiar każdego elementu powinien być bez konieczności mierzenia zawartości do pokazania. W tym spisie treści po prostu odwołujemy się do tej kategorii układów wirtualizacji jako układów danych , ponieważ zwykle obejmują one inspekcję danych. Na podstawie tych danych aplikacja może wybrać wizualną reprezentację o znanym rozmiarze — być może dlatego, że jest częścią danych lub została wcześniej określona przez projekt.

Ogólne podejście przewiduje, że układ powinien:

  1. Oblicz rozmiar i położenie każdego elementu.
  2. W ramach elementu MeasureOverride:
    1. Użyj obiektu RealizationRect , aby określić, które elementy powinny być wyświetlane w obszarze widoku.
    2. Pobierz UIElement, który powinien reprezentować element, za pomocą metody GetOrCreateElementAt.
    3. Mierzenie elementu UIElement przy użyciu wstępnie obliczonego rozmiaru.
  3. W ramach ArrangeOverride, rozmieść każdy zrealizowany element UI we wstępnie obliczonym położeniu.

Uwaga / Notatka

Podejście do układu danych jest często niezgodne z wirtualizacją danych. W szczególności w przypadku, gdy jedyne dane załadowane do pamięci to dane wymagane do wypełnienia danych widocznych dla użytkownika. Wirtualizacja danych nie odnosi się do opóźnionego ani przyrostowego ładowania danych, gdy użytkownik przewija dane w dół, w którym pozostają przechowywane dane. Chodzi o to, że elementy są zwalniane z pamięci, gdy są przewijane poza widok. Układ danych, który sprawdza każdy element danych w ramach układu danych, uniemożliwiłby działanie wirtualizacji danych zgodnie z oczekiwaniami. Wyjątkiem jest układ, taki jak UniformGridLayout, który zakłada, że wszystko ma ten sam rozmiar.

Wskazówka

Jeśli tworzysz kontrolkę niestandardową dla biblioteki kontrolek, która będzie używana przez inne osoby w wielu różnych sytuacjach, układ danych może nie być opcją dla Ciebie.

Przykład: układ kanału aktywności Xbox

Interfejs użytkownika kanału aktywności Xbox używa powtarzającego się wzorca, w którym każdy wiersz ma szeroki kafelek, a następnie dwa wąskie kafelki odwrócone w kolejnym wierszu. W tym układzie rozmiar każdego elementu jest funkcją pozycji elementu w zestawie danych i znanym rozmiarem kafelków (szeroki i wąski).

Xbox kanał aktywności

Poniższy kod zawiera opis niestandardowego interfejsu użytkownika wirtualizującego dla kanału aktywności, aby zilustrować ogólne podejście, jakie można podjąć w przypadku układu danych.

Wskazówka

Jeśli masz zainstalowaną aplikację Galeria WinUI 3 , kliknij tutaj, aby otworzyć aplikację i zobaczyć akcję ItemsRepeater. Pobierz aplikację z Microsoft Store lub pobierz kod źródłowy 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;
}

(Opcjonalnie) Zarządzanie mapowaniem elementów na UIElementy

Domyślnie element VirtualizingLayoutContext utrzymuje mapowanie między zrealizowanymi elementami a indeksem w źródle danych, które reprezentują. Układ może wybrać samodzielne zarządzanie tym mapowaniem, zawsze żądając opcji SuppressAutoRecycle podczas pobierania elementu za pomocą metody GetOrCreateElementAt, która uniemożliwia domyślne zachowanie automatycznego recyklingu. Układ może to zrobić na przykład, jeśli będzie używany tylko wtedy, gdy przewijanie jest ograniczone do jednego kierunku, a rozważane elementy będą zawsze ciągłe (tj. znajomość indeksu pierwszego i ostatniego elementu wystarcza, aby znać wszystkie elementy, które powinny zostać zrealizowane).

Przykład: pomiar kanału aktywności Xbox

Poniższy fragment kodu przedstawia dodatkową logikę, którą można dodać do elementu MeasureOverride w poprzednim przykładzie w celu zarządzania mapowaniem.

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

Układy wirtualizacji zależne od zawartości

Jeśli musisz najpierw zmierzyć zawartość interfejsu użytkownika dla elementu, aby ustalić jego dokładny rozmiar, jest to układ zależny od zawartości. Można również traktować go jako układ, w którym każdy element musi mieć sam rozmiar, a nie układ informujący o jego rozmiarze. Wirtualizacja układów, które należą do tej kategorii, jest bardziej zaangażowana.

Uwaga / Notatka

Układy zależne od zawartości nie (nie powinny) przerywać wirtualizacji danych.

Szacunki

Układy zależne od zawartości opierają się na szacowaniu, aby odgadnąć zarówno rozmiar niezrealizowanej zawartości, jak i położenie zrealizowanej zawartości. W miarę jak te szacunki się zmieniają, spowoduje to, że zawartość będzie regularnie zmieniać swoje pozycje w obszarze przewijanym. Może to prowadzić do bardzo frustrującego i nieprzyjemnego doświadczenia użytkownika, jeśli nie zostanie złagodzone. Potencjalne problemy i środki zaradcze zostały omówione tutaj.

Uwaga / Notatka

Układy danych, które uwzględniają każdy element i znają dokładny rozmiar wszystkich elementów, niezależnie od tego, czy są zrealizowane, oraz ich pozycje, mogą całkowicie unikać problemów.

Zakotwiczenie przewijania

Język XAML udostępnia mechanizm ograniczający nagłe przesunięcia obszaru widoku, polegający na tym, że kontrolki przewijania obsługują kotwiczenie przewijania dzięki implementacji interfejsu IScrollAnchorProvider. Gdy użytkownik manipuluje zawartością, kontrolka przewijania stale wybiera element z zestawu kandydatów, które zostały wybrane do śledzenia. Jeśli pozycja elementu kotwicy przesuwa się podczas rozmieszczenia, kontrolka przewijania automatycznie przesuwa swoje okno widoku, aby zachować widok.

Wartość parametru RecommendedAnchorIndex podanego w układzie może odzwierciedlać aktualnie wybrany element kotwicy wybrany przez kontrolkę przewijania. Alternatywnie, jeśli deweloper jawnie zażąda, aby element został zrealizowany dla indeksu za pomocą metody GetOrCreateElement na ItemsRepeater, ten indeks jest podawany jako RecommendedAnchorIndex w następnym przekazaniu układu. Dzięki temu układ może być przygotowany do prawdopodobnego scenariusza, w ramach którego deweloper realizuje element, a następnie żąda, aby był on w widoku za pomocą metody StartBringIntoView .

RecommendedAnchorIndex to indeks elementu w źródle danych, który układ zależny od zawartości powinien najpierw umieścić podczas szacowania pozycji elementów. Powinien on służyć jako punkt wyjścia do pozycjonowania innych zrealizowanych elementów.

Wpływ na paski przewijania

Nawet w przypadku zakotwiczenia przewijania, jeśli oszacowania układu różnią się znacznie, być może ze względu na znaczące różnice w rozmiarze zawartości, pozycja suwaka paska przewijania może wydawać się skakać. Może to być dezorientujące dla użytkownika, jeśli kciuk nie wydaje się śladować położenia wskaźnika myszy podczas jego przeciągania.

Im dokładniej układ potrafi oszacować parametry, tym mniej prawdopodobne, że użytkownik zobaczy, jak kciuk paska przewijania przeskakuje.

Poprawki układu

Układ graficzny zależny od zawartości powinien zostać przygotowany tak, aby oszacowanie było zgodne z rzeczywistością. Na przykład, gdy użytkownik przewija na górę zawartości, a układ identyfikuje pierwszy element, może się okazać, że oczekiwana pozycja względem elementu startowego spowoduje, że pojawi się w innym miejscu niż początek (x:0, y:0). W takim przypadku układ może użyć właściwości LayoutOrigin , aby ustawić położenie obliczone jako nowe źródło układu. Ostateczny wynik jest podobny do zakotwiczenia przewijania, w którym przeglądarka kontrolki jest automatycznie dostosowywana, aby uwzględniać pozycję zawartości zgodnie z układem.

Korygowanie LayoutOrigin

Rozłączone porty widoków

Rozmiar zwrócony z metody MeasureOverride układu reprezentuje najlepsze oszacowanie rozmiaru zawartości, która może ulec zmianie z każdym kolejnym rozmieszczeniem. Gdy użytkownik przewija układ, będzie on stale ponownie oceniany z zaktualizowanym RealizationRect.

Jeśli użytkownik przeciąga kciuk bardzo szybko, to jest możliwe, że widok, z perspektywy układu, wydaje się wykonywać duże skoki, gdzie poprzednia pozycja nie pokrywa się z bieżącą. Jest to spowodowane asynchronicznym charakterem przewijania. Istnieje również możliwość, że aplikacja korzystająca z układu żąda, aby element został przeniesiony do widoku dla elementu, który nie jest obecnie zrealizowany i szacuje się, że znajduje się poza bieżącym zakresem śledzonym przez układ.

Gdy układ stwierdzi, że jego przewidywanie jest niepoprawne i/lub widzi nieoczekiwane przesunięcie widoku, musi ponownie ustawić się w pozycji początkowej. Układy wirtualizacji dostarczane w ramach kontrolek XAML są opracowywane jako układy zależne od zawartości, ponieważ nakładają mniej ograniczeń na charakter wyświetlanej zawartości.

Przykład: Prosty układ stacku do wirtualizacji dla elementów o zmiennym rozmiarze

W poniższym przykładzie przedstawiono prosty układ stosu dla elementów o zmiennym rozmiarze, które:

  • obsługuje wirtualizację interfejsu użytkownika,
  • używa oszacowań, aby odgadnąć rozmiar niezrealizowanych elementów,
  • jest świadomy potencjalnych nieciągłych przesunięć widoku i
  • stosuje poprawki układu do uwzględnienia tych zmian.

Użycie: znaczniki

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

Kodbehind: 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;

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