Ekli Düzenler

Düzen mantığını başka bir nesneye devreden bir kapsayıcı (örneğin, Panel), alt öğeleri için düzen davranışını sağlamak için ekli düzen nesnesine dayanır. Ekli düzen modeli, uygulama için çalışma zamanında öğelerin düzenini değiştirme veya düzenin yönlerini kullanıcı arabiriminin farklı bölümleri arasında daha kolay paylaşma esnekliği sağlar (örneğin, tablonun satırlarındaki öğeler sütun içinde hizalanmış gibi görünür).

Bu konu başlığında, ekli düzen oluşturma (sanallaştırma ve sanallaştırmama), anlamanız gereken kavramlar ve sınıflar ve aralarında karar verirken göz önünde bulundurmanız gereken dengeler ele alınmaktadır.

Windows Uygulama SDK'sı
Burada açıklanan düzen sınıfları, Windows Uygulama SDK'sı dahil winUI 3'ün bir parçasıdır. Daha fazla bilgi için bkz. Windows Uygulama SDK'sı genel bakış.

Önemli API'ler:

Önemli Kavramlar

Düzen gerçekleştirmek için her öğe için iki sorunun yanıtlanması gerekir:

  1. Bu öğe hangi boyutta olacak?

  2. Bu öğenin konumu ne olacak?

XAML'nin bu soruları yanıtlayan düzen sistemi, Özel paneller tartışmasının bir parçası olarak kısaca ele alınmıştır.

Kapsayıcılar ve Bağlam

Kavramsal olarak, XAML'in Paneli çerçevede iki önemli rolü doldurur:

  1. Alt öğeleri içerebilir ve öğe ağacında dallanmayı tanıtır.
  2. Bu çocuklara belirli bir düzen stratejisi uygular.

Bu nedenle, XAML'deki bir Panel genellikle düzen ile eşanlamlı olmuştur, ancak teknik olarak konuşursak, yalnızca düzenden fazlasını yapar.

ItemsRepeater da Panel gibi davranır, ancak Panel'in aksine, UIElement alt öğelerini program aracılığıyla eklemeye veya kaldırmaya izin veren bir Children özelliğini kullanıma sunmaz. Bunun yerine, alt öğelerinin yaşam süresi, bir veri öğeleri koleksiyonuna karşılık gelen çerçeve tarafından otomatik olarak yönetilir. Panel'den türetilmemiş olsa da, davranış sergiler ve çerçeve tarafından bir Panel gibi ele alınır.

Uyarı

LayoutPanel, Panel'den türetilen ve mantığını ekli Düzen nesnesine devreden bir kapsayıcıdır. LayoutPanel Önizleme aşamasındadır ve şu anda yalnızca WinUI 3 paketinin Ön sürüm sürümlerinde mevcuttur.

Containers

Kavramsal olarak Panel , bir Arka Plan için pikselleri işleme özelliğine de sahip öğelerden oluşan bir kapsayıcıdır. Paneller, ortak düzen mantığını kullanımı kolay bir pakette kapsüllemek için bir yol sağlar.

Ekli düzen kavramı, kapsayıcı ve düzenin iki rolü arasındaki ayrımı daha net hale getirir. Kapsayıcı, düzen mantığını başka bir nesneye devrediyorsa, bu nesneye aşağıdaki kod parçacığında görüldüğü gibi ekli düzeni çağırırız. LayoutPanel gibi FrameworkElement'ten devralan kapsayıcılar, XAML'nin düzen işlemine giriş sağlayan ortak özellikleri otomatik olarak kullanıma sunar (örneğin, Yükseklik ve Genişlik).

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

Düzen işlemi sırasında kapsayıcı, alt öğelerini ölçmek ve düzenlemek için ekli UniformGridLayout'a dayanır.

Konteyner Başı Durumu

Ekli düzende, düzen nesnesinin tek bir örneği aşağıdaki kod parçacığında olduğu gibi birçok kapsayıcıyla ilişkilendirilebilir; bu nedenle konak kapsayıcıya bağımlı olmamalıdır veya doğrudan başvurmamalıdır. Örneğin:

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

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

Bu durumda ExampleLayout , düzen hesaplamasında kullandığı durumu ve bir paneldeki öğelerin düzenini diğer panelde etkilememek için bu durumun depolandığı konumu dikkatle dikkate almalıdır. MeasureOverride ve ArrangeOverride mantığı statik özelliklerinin değerlerine bağlı olan özel bir Panel'e benzer.

LayoutContext

LayoutContext'in amacı bu zorluklarla başa çıkmaktır. Ekli düzene, ikisi arasında doğrudan bağımlılık olmadan alt öğeleri alma gibi konak kapsayıcısıyla etkileşim kurma olanağı sağlar. Bağlam, düzenin ayrıca kapsayıcının alt öğeleriyle ilgili olabilecek her türlü durumu depolamasını sağlar.

Basit, sanallaştırılmayan düzenlerin genellikle herhangi bir durumu koruması gerekmez ve bu da sorunu gidermez. Ancak, Grid gibi daha karmaşık bir düzen, measure ve arrange çağrıları arasında durumu korumayı ve böylelikle bir değeri yeniden hesaplamaktan kaçınmayı tercih edebilir.

Sanallaştırılmış düzenler genellikle hem ölçüm hem de düzenleme ile düzenleme döngüleri arasında bazı durumların korunmasını gerektirir.

Per-Container Durumunu Başlatma ve Başlatmayı Sonlandırma

Kapsayıcıya bir düzen eklendiğinde InitializeForContextCore yöntemi çağrılır ve durumu depolamak için bir nesne başlatma fırsatı sağlar.

Benzer şekilde, düzen kapsayıcıdan kaldırıldığında UninitializeForContextCore yöntemi çağrılır. Bu, düzene söz konusu kapsayıcıyla ilişkilendirdiği tüm durumları temizleme fırsatı verir.

Düzenin durum nesnesi, bağlam üzerinde LayoutState özelliğiyle depolanabilir ve kapsayıcıdan alınabilir.

UI Sanallaştırma

UI sanallaştırma, kullanıcı arabirimi nesnesinin oluşturulmasını gereken zamana kadar geciktirme anlamına gelir. Bu bir performans iyileştirmesi. Kaydırma olmayan senaryolarda , ne zaman gerek duyulduğunu belirlemek, uygulamaya özgü herhangi bir sayıda şeyi temel alabilir. Bu gibi durumlarda, uygulamalar x:Load'ı kullanmayı göz önünde bulundurmalıdır. Düzeninizde özel bir işleme gerektirmez.

Liste gibi kaydırma tabanlı senaryolarda, gerektiğinde belirlemek genellikle düzen işlemi sırasında nereye yerleştirildiğine büyük ölçüde bağlı olan ve özellikle dikkat edilmesi gereken "bir kullanıcı tarafından görülebilecek mi" konusuna dayanır. Bu senaryo, bu belge için bir odak noktasıdır.

Uyarı

Bu belgede ele alınmasa da, kaydırma senaryolarında ui sanallaştırmayı etkinleştiren özellikler, kaydırma olmayan senaryolarda da uygulanabilir. Örneğin, sunduğu komutların ömrünü yöneten ve öğeleri görünür bir alan ile taşma menüsü arasında geri dönüştürerek/taşıyarak kullanılabilir alandaki değişikliklere yanıt veren veri temelli toolbar denetimi.

Başlangıç Yapmak

İlk olarak, oluşturmanız gereken düzenin kullanıcı arabirimi sanallaştırmasını destekleyip desteklemeyeceğine karar verin.

Akılda tutulması gereken birkaç şey...

  1. Sanallaştırma yapılmayan düzenleri oluşturmak daha kolaydır. Öğe sayısı her zaman küçük olacaksa sanallaştırma olmayan bir düzen yazmanız önerilir.
  2. Platform, ortak gereksinimleri karşılamak için ItemsRepeater ve LayoutPanel ile birlikte çalışan bir dizi ekli düzen sağlar. Özel bir düzen tanımlamanız gerektiğinden karar vermeden önce bunları tanıyın.
  3. Sanallaştırma düzenleri, sanallaştırılmayan bir düzene kıyasla her zaman ek CPU ve bellek maliyeti/karmaşıklığı/ek yüküne sahiptir. Genel bir kural olarak, düzenin yönetmesi gereken alt öğeler muhtemelen görünüm penceresinin 3 katı büyüklüğünde bir alana sığacaksa, sanallaştırılmış bir düzen kullanmanın çok fazla kazanç sağlamayacağı düşünülebilir. 3x boyutu bu belgenin ilerleyen bölümlerinde daha ayrıntılı olarak ele alınmıştır, ancak Windows kaydırma işleminin asenkron yapısı ve bu özelliğin sanallaştırma üzerindeki etkisinden kaynaklanır.

Tip

Başvuru noktası olarak, ListView (ve ItemsRepeater) için varsayılan ayarlar, öğe sayısı geçerli görünüm penceresinin boyutunun 3 katını doldurmak için yeterli olana kadar geri dönüştürmenin başlamamasıdır.

Temel türünüzü seçin

ekli düzen hiyerarşisi

Temel Düzen türü, ekli düzen yazmak için başlangıç noktası görevi görecek türetilmiş iki türe sahiptir:

  1. NonVirtualizingLayout
  2. VirtualizingLayout

Sanallaştırılmayan Düzen

Sanallaştırılmayan bir düzen oluşturma yaklaşımı, Özel Panel oluşturmuş olan herkese tanıdık gelmelidir. Aynı kavramlar geçerlidir. Birincil fark, NonVirtualizingLayoutContext kullanılarak Children koleksiyonuna erişilmesi ve düzenin durumu saklamayı tercih edebilmesidir.

  1. Panel yerine, NonVirtualizingLayout temel türünden türetin.
  2. (İsteğe bağlı) Değiştirildiğinde düzeni geçersiz kılacak bağımlılık özelliklerini tanımlayın.
  3. (Yeni/İsteğe Bağlı)InitializeForContextCore'un bir parçası olarak düzenin gerektirdiği herhangi bir durum nesnesini başlatın. Bağlamla birlikte sağlanan LayoutState kullanılarak ana kapsayıcıya yerleştirin.
  4. MeasureOverride'ı geçersiz kılın ve tüm alt öğeler üzerinde Measure metodunu çağırın.
  5. ArrangeOverride'ı geçersiz kılın ve tüm alt öğeler üzerinde Arrange yöntemini çağırın.
  6. (Yeni/İsteğe Bağlı)UninitializeForContextCore'un bir parçası olarak kaydedilmiş tüm durumları temizleyin.

Örnek: Farklı Boyutlardaki Öğeler için Basit Yığın Düzeni

MyStackLayout

Aşağıda, farklı boyutlardaki öğelerin sanallaştırılmayan temel bir yığın düzeni yer alır. Düzenin davranışını ayarlayacak özelliklere sahip değil. Aşağıdaki uygulama, düzenin kapsayıcı tarafından sağlanan bağlam nesnesine nasıl bağımlı olduğunu gösterir:

  1. Çocuk sayısını alın ve
  2. Her alt öğeye dizine göre erişin.
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>

Düzenleri Sanallaştırma

Sanallaştırılmayan bir düzene benzer şekilde, sanallaştırma düzeni için üst düzey adımlar aynıdır. Karmaşıklık büyük ölçüde görünüm penceresi içinde hangi öğelerin yer alacağı ve gerçekleştirilmesi gerektiğini belirlemededir.

  1. VirtualizingLayout temel türünden türetilir.
  2. (İsteğe bağlı) Değiştirildiğinde düzeni geçersiz kılacak bağımlılık özelliklerinizi tanımlayın.
  3. InitializeForContextCore'un bir parçası olarak düzenin gerektirdiği herhangi bir durum nesnesini başlatın. Bağlamla birlikte sağlanan LayoutState kullanılarak ana kapsayıcıya yerleştirin.
  4. MeasureOverride'ı geçersiz kılın ve gerçekleştirilmesi gereken her alt öğe için Measure yöntemini çağırın.
    1. GetOrCreateElementAt yöntemi, çerçeve tarafından hazırlanmış bir UIElement almak için kullanılır (örneğin, veri bağlamaları uygulanır).
  5. ArrangeOverride'ı geçersiz kılın ve gerçekleştirilen her alt öğe için Arrange yöntemini çağırın.
  6. (İsteğe bağlı) UninitializeForContextCore'un bir parçası olarak kaydedilmiş tüm durumları temizleyin.

Tip

MeasureOverride tarafından döndürülen değer, sanallaştırılmış içeriğin boyutu olarak kullanılır.

Sanallaştırma düzeni yazarken dikkate alınması gereken iki genel yaklaşım vardır. Birini mi yoksa diğerini mi seçeceğiniz büyük ölçüde "bir öğenin boyutunu nasıl belirleyeceksiniz"e bağlıdır. Veri kümesindeki bir öğenin dizinini bilmek için yeterliyse veya verilerin kendisi nihai boyutunu belirlerse, bunu verilere bağımlı olarak değerlendiririz. Bunları oluşturmak daha kolaydır. Ancak, bir öğenin boyutunu belirlemenin tek yolu kullanıcı arabirimini oluşturmak ve ölçmekse içeriğe bağımlı olduğunu söyleyebiliriz. Bunlar daha karmaşıktır.

Düzen İşlemi

Veri veya içeriğe bağımlı bir düzen oluşturup oluşturmadığınıza bağlı olarak, düzen sürecini ve Windows’un zaman uyumsuz kaydırmasının etkilerini anlamak önemlidir.

Çerçeve tarafından gerçekleştirilen adımların başlangıçtan kullanıcı arabirimini ekranda görüntülemeye kadar basitleştirilmiş bir görünümü şöyledir:

  1. İşaretlemeyi ayrıştırıyor.

  2. Bir öğe ağacı oluşturur.

  3. Düzen geçişi gerçekleştirir.

  4. Görüntü işleme geçişi gerçekleştirir.

Ui sanallaştırma ile normalde 2. adımda yapılacak öğelerin oluşturulması, görünüm penceresine yetecek kadar içerik oluşturulduğu belirlendikten sonra geciktirilir veya erken sonlandırılır. Bir sanallaştırma kapsayıcısı (örneğin, ItemsRepeater), bu süreci yönlendirmek için bağlı olduğu düzene güvenir. Sanallaştırma düzeninin ihtiyaç duyduğu ek bilgileri ortaya çıkaran bir VirtualizingLayoutContext ile ekli düzeni sağlar.

RealizationRect (ör. Görünüm Penceresi)

Windows'da kaydırma işlemi, kullanıcı arabirimi iş parçacığına zaman uyumsuz gerçekleşir. Düzen, yapının kontrolünde değil. Bunun yerine, etkileşim ve hareket sistemin oluşturucusunda gerçekleşir. Bu yaklaşımın avantajı, kaydırma içeriğinin her zaman 60fps'de yapılabilmesidir. Ancak zorluk, düzende görüldüğü gibi "görünüm penceresi"nin ekranda görünene göre biraz eski olmasıdır. Kullanıcı hızla kaydırırsa, yeni içerik oluşturma hızı kullanıcı arabirimi iş parçacığının hızını aşabilir ve bu da ekranın siyah gösterilmesine neden olabilir. Bu nedenle, genellikle bir sanallaştırma düzeninin görünüm penceresine göre daha büyük bir alanı doldurmak için yeterli hazırlanmış öğelerden oluşan ek bir arabellek oluşturması gerekir. Kaydırma sırasında yük daha ağır olduğunda bile kullanıcıya içerik sunulmaya devam eder.

Gerçekleştirme düzeltmesi

Öğe oluşturma maliyetli olduğundan, kapsayıcıların sanallaştırılması (örneğin, ItemsRepeater), başlangıçta ekli düzeni görünüm penceresiyle eşleşen bir RealizationRect sağlayacaktır. Boşta kalma süresinde kapsayıcı, giderek daha büyük bir gerçekleştirme düzeltmesi kullanarak düzen için yinelenen çağrılar yaparak hazırlanan içeriğin arabelleğinin büyümesine neden olabilir. Bu davranış, hızlı başlangıç süresi ile iyi bir kaydırma deneyimi arasında denge kurmaya çalışan bir performans iyileştirmesidir. ItemsRepeater'ın oluşturacağı en büyük arabellek boyutu VerticalCacheLength ve HorizontalCacheLength özellikleri tarafından denetlenir.

Öğeleri Yeniden Kullanma (Geri Dönüşüm)

Düzenin, her çalıştırıldığında RealizationRect'i dolduracak öğeleri boyutlandırması ve konumlandırması beklenir. Varsayılan olarak VirtualizingLayout , her düzen geçişinin sonunda kullanılmayan öğeleri geri dönüştürecektir.

MeasureOverride ve ArrangeOverride'in bir parçası olarak düzene geçirilen VirtualizingLayoutContext, bir sanallaştırma düzeninin ihtiyaç duyduğu ek bilgileri sağlar. Sağladığı en yaygın kullanılan şeylerden bazıları şunlardır:

  1. Verilerdeki öğe sayısını (ItemCount) sorgulama.
  2. GetItemAt yöntemini kullanarak belirli bir öğeyi alın.
  3. RealizationRect adında, düzenin gerçekleştirilmiş öğelerle doldurması gereken görünüm penceresi ve arabelleği temsil eden bir alan elde edin.
  4. GetOrCreateElementAt yöntemiyle belirli bir öğe için UIElement isteyin.

Belirli bir dizin için bir öğe istemek, bu öğenin düzenin bu geçişi için "kullanımda" olarak işaretlenmesine neden olur. Öğe henüz yoksa, bu öğe oluşturulur ve otomatik olarak kullanıma hazırlanır (örneğin, DataTemplate içinde tanımlanan UI ağacını şişirme, veri bağlamayı işleme ve benzeri işlemler). Aksi takdirde, var olan örneklerin havuzundan alınır.

Her ölçü geçişinin sonunda, öğe GetOrCreateElementAt yöntemiyle alındığında SuppressAutoRecycle seçeneği kullanılmadığı sürece, "kullanımda" olarak işaretlenmemiş mevcut, gerçekleştirilen öğeler otomatik olarak yeniden kullanım için kullanılabilir olarak kabul edilir. Çerçeve, nesneyi otomatik olarak bir geri dönüşüm havuzuna taşır ve erişilebilir hale getirir. Daha sonra farklı bir kapsayıcı tarafından kullanılmak üzere çekilebilir. Bir öğenin yeniden üst öğeyle ilişkilendirilmesi bazı maliyetlerle birlikte geldiğinden, çerçeve mümkün olduğunda bunu önlemeye çalışır.

Bir sanallaştırma düzeni, her ölçümün başında hangi öğelerin artık oluşturma dikdörtgeni içinde yer almayacağını biliyorsa, yeniden kullanımını optimize edebilir. Çerçevenin varsayılan davranışına güvenmek yerine. Düzen, RecycleElement yöntemini kullanarak öğeleri önceden geri dönüşüm havuzuna taşıyabilir. Yeni öğe istemeden önce bu yöntemi çağırmak, düzen daha sonra bir öğeyle ilişkilendirilmeyen bir dizin için GetOrCreateElementAt isteği gönderirken mevcut öğelerin kullanılabilir olmasına neden olur.

VirtualizingLayoutContext, içeriğe bağımlı bir düzen oluşturan düzen yazarları için tasarlanmış iki ek özellik sağlar. Bunlar daha sonra daha ayrıntılı olarak ele alınmalıdır.

  1. RecommendedAnchorIndex, düzene isteğe bağlı bir giriş sağlar.
  2. Düzenin isteğe bağlı çıkışı olan LayoutOrigin.

Verilere Bağımlı Sanallaştırma Düzenleri

Gösterilecek içeriği ölçmeye gerek kalmadan her öğenin boyutunun ne olması gerektiğini biliyorsanız, sanallaştırma düzeni daha kolaydır. Bu belgede, genellikle verileri incelemeyi içerdiğinden bu düzenleri sanallaştırma kategorisini veri düzenleri olarak adlandıracağız. Verilere bağlı olarak, bir uygulama bilinen boyutta bir görsel gösterim seçebilir; bunun nedeni verilerin bir parçası veya daha önce tasarım tarafından belirlendiği olabilir.

Genel yaklaşım, düzenin şu şekilde olmasıdır:

  1. Her öğenin boyutunu ve konumunu hesaplayın.
  2. MeasureOverride'in bir parçası olarak:
    1. Görünüm penceresi içinde hangi öğelerin görüneceğini belirlemek için RealizationRect'i kullanın.
    2. GetOrCreateElementAt yöntemiyle öğeyi temsil etmesi gereken UIElement'i alın.
    3. UIElement'i önceden hesaplanmış boyutla ölçün.
  3. ArrangeOverride'in bir parçası olarak, gerçekleştirilen her UIElement'i önceden hesaplanmış konumla düzenleyin.

Uyarı

Veri düzeni yaklaşımı genellikle veri sanallaştırma ile uyumsuzdur. Özellikle, belleğe yüklenen tek verinin, kullanıcıya görünür olanı doldurmak için gereken veriler olduğu durumlardır. Veri sanallaştırma, kullanıcı verilerin bulunduğu yerde aşağı kaydırılırken verilerin yavaş veya artımlı yüklenmesini ifade etmemektedir. Bunun yerine, öğelerin görüntü dışına kaydırıldığında bellekten ne zaman serbest bırakıldığına başvurur. Veri düzeninin bir parçası olarak her veri öğesini inceleyen bir veri düzenine sahip olmak, veri sanallaştırmanın beklendiği gibi çalışmasını engeller. Özel durum, UniformGridLayout gibi her şeyin aynı boyuta sahip olduğunu varsayan bir düzendir.

Tip

Çok çeşitli durumlarda başkaları tarafından kullanılacak bir denetim kitaplığı için özel denetim oluşturuyorsanız, veri düzeni sizin için bir seçenek olmayabilir.

Örnek: Xbox Etkinlik Akışı düzeni

Xbox Etkinlik Akışı kullanıcı arabirimi, her satırın bir geniş kutucuğa sahip olduğu ve bu kutucukları takip eden satırda tersine dönen iki dar kutucuğun bulunduğu yinelenen bir desen kullanır. Bu düzende, her öğenin boyutu, öğenin veri kümesindeki konumunun ve kutucuklar için bilinen boyutunun (geniş ve dar) bir işlevidir.

Xbox etkinlik akışı

Aşağıdaki kod, bir veri düzeni için genel yaklaşımı göstermek üzere etkinlik akışı için özel bir sanallaştırma kullanıcı arabiriminin ne olabileceğini göstermektedir.

Tip

WinUI 3 Galeri uygulaması yüklüyse, uygulamayı açmak için buraya tıklayın ve ItemsRepeater'ın çalıştığını görün. uygulamayı Microsoft Store veya GitHub adresinden alın.

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

(İsteğe bağlı) Öğeyi UIElement Eşlemesine Yönetme

Varsayılan olarak VirtualizingLayoutContext , gerçekleştirilen öğeler ile temsil ettikleri veri kaynağındaki dizin arasında bir eşleme tutar. Bir düzen, varsayılan otomatik geri dönüşüm davranışını engelleyen GetOrCreateElementAt yöntemi aracılığıyla bir öğe alınırken her zaman SuppressAutoRecycle seçeneğini isteyerek bu eşlemeyi yönetmeyi seçebilir. Örneğin, bir düzen bunu yapmayı seçebilir, örneğin yalnızca kaydırma tek bir yönle sınırlı olduğunda kullanılacaksa ve dikkate alan öğeler her zaman bitişik olacaktır (örneğin, ilk ve son öğenin dizinini bilmek, gerçekleştirilmesi gereken tüm öğeleri bilmek için yeterlidir).

Örnek: Xbox Etkinlik Akışı ölçüsü

Aşağıdaki kod parçacığı, eşlemeyi yönetmek için önceki örnekteki MeasureOverride öğesine eklenebilecek ek mantığı gösterir.

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

İçeriğe bağımlı Sanallaştırma Düzenleri

Bir öğenin tam boyutunu bulmak için önce kullanıcı arabirimi içeriğini ölçmeniz gerekiyorsa, içeriğe bağımlı bir düzendir. Bunu, öğeye boyutunu söyleyen düzen yerine her öğenin kendisini boyutlandırması gereken bir düzen olarak da düşünebilirsiniz. Bu kategoriye giren düzenlerin sanallaştırılması, daha karmaşıktır.

Uyarı

İçeriğe bağımlı düzenler veri sanallaştırmayı kesmez (olmamalıdır).

Tahminler

İçeriğe bağımlı düzenler, hem gerçekleştirilmemiş içeriğin boyutunu hem de gerçekleştirilen içeriğin konumunu tahmin etmek için tahmine dayanır. Bu tahminler değiştikçe, görüntülenen içeriğin kaydırılabilir alan içinde düzenli olarak yer değiştirmesine neden olur. Bu durum, hafifletilmemesi durumunda çok sinir bozucu ve rahatsız edici bir kullanıcı deneyimine yol açabilir. Olası sorunlar ve azaltmalar burada ele alınıyor.

Uyarı

Tüm öğeleri, gerçekleşmiş olsun ya da olmasın onların tam boyutlarını ve pozisyonlarını bilen veri düzenleri, bu sorunlardan tamamen kaçınabilir.

Kaydırma Sabitlenmesi

XAML, IScrollAnchorProvider arabirimini uygulayarak kaydırma denetimlerinin kaydırma sabitlemeyi desteklemesini sağlayarak ani görünüm penceresi kaydırmalarını azaltmaya yönelik bir mekanizma sağlar. Kullanıcı içeriği işledikçe, kaydırma denetimi sürekli olarak izlenmesi kabul edilen adaylar kümesinden bir öğe seçer. Yer işareti öğesinin konumu düzen sırasında kayıyorsa, kaydırma denetimi, görünüm penceresini korumak için otomatik olarak görünüm penceresini buna göre ayarlar.

Düzene sağlanan RecommendedAnchorIndex değeri, kaydırma denetimi tarafından seçilen tutturucu öğeyi yansıtıyor olabilir. Alternatif olarak, bir geliştirici, ItemsRepeater üzerindeki GetOrCreateElement yöntemi ile bir dizin için açıkça bir öğenin gerçekleştirilmesini isterse, bu dizin sonraki düzen geçişinde RecommendedAnchorIndex olarak verilir. Bu, düzenin bir geliştiricinin bir öğeyi gerçekleştirmesi ve daha sonra StartBringIntoView yöntemi aracılığıyla görünüme getirilmesini istemesi olası senaryo için hazırlanmasını sağlar.

RecommendedAnchorIndex, içerik bağımlı düzenin öğelerinin konumunu tahmin ederken önce konumlandırması gereken veri kaynağındaki öğenin dizinidir. Diğer gerçekleştirilen öğeleri konumlandırmak için başlangıç noktası olarak görev yapmalıdır.

ScrollBar'lar üzerindeki etki

Kaydırma sabitleme tekniği kullanılsa bile, yerleşim tahminleri içerik boyutunun önemli ölçüde değişmesi nedeniyle çok fazla farklılık gösteriyorsa, kaydırma çubuğunun başparmak konumu yer değiştirebilir gibi görünebilir. Başparmak, fare işaretçisinin konumunu sürüklenirken izlemiyor gibi görünüyorsa, bu durum kullanıcı için rahatsız edici olabilir.

Düzen, tahminlerinde ne kadar doğru olabilirse, kullanıcının ScrollBar'ın kaydırma çubuğu sapının zıpladığını görme olasılığı o kadar düşer.

Düzen Düzeltmeleri

İçeriğe bağımlı bir düzen, tahminini gerçeklikle rasyonalize etmeye hazır olmalıdır. Örneğin, kullanıcı içeriğin en üstüne kaydırırken ve düzen ilk öğeyi işlediğinde, öğenin başlangıç öğesine göre beklenen konumunun, başlangıç noktası olan (x:0, y:0) dışında bir yerde görünmesine neden olabileceğini bulabilir. Bu durum oluştuğunda düzen LayoutOrigin özelliğini kullanarak hesaplandığı konumu yeni düzen kaynağı olarak ayarlayabilir. Sonuç olarak, kaydırma denetiminin görünüm penceresi, düzenin bildirdiği şekilde içeriğin konumunu hesaba katacak şekilde otomatik olarak ayarlandığında, bu durum kaydırma sabitlemeye benzer.

LayoutOrigin'i Düzeltme

Bağlantısı Kesilmiş Görünüm Penceresi

Düzenin MeasureOverride yönteminden döndürülen boyut, her bir ardışık düzende değişebilecek içeriğin boyutuyla ilgili en iyi tahmini temsil eder. Kullanıcı kaydırdıkça, güncelleştirilmiş bir RealizationRect ile düzen sürekli olarak yeniden değerlendirilir.

Kullanıcı başparmağı çok hızlı sürüklenirse, düzen perspektifinden görünüm alanı, önceki konumun artık geçerli konumla çakışmadığı büyük atlamalar yapıyormuş gibi görünebilir. Bunun nedeni, kaydırmanın zaman uyumsuz doğasıdır. Düzeni kullanan bir uygulama, şu anda oluşturulmayan ve düzen tarafından izlenen geçerli aralığın dışında olduğu tahmin edilen bir öğenin görünüme getirilmesini talep etmesi de mümkündür.

Düzen, tahmininin yanlış olduğunu ve/veya beklenmeyen bir görünüm penceresi kayması gördüğünü fark ettiğinde, başlangıç konumunu yeniden ayarlaması gerekir. XAML denetimleriyle birlikte gelen sanallaştırma düzenleri, gösterilecek içeriğin doğasına daha az kısıtlama getiren içerik bağımlı düzenler olarak geliştirilmiştir.

Örnek: Farklı Boyutlu Öğeler için Basit Sanallaştırma Yığını Düzeni

Aşağıdaki örnekte, değişken boyutlu öğeler için basit bir yığın düzeni gösterilmektedir:

  • kullanıcı arabirimi sanallaştırmayı destekler,
  • gerçekleşmemiş öğelerin boyutunu tahmin etmek için tahminleri kullanır,
  • kesintili viewport kaymalarının farkındadır ve
  • bu vardiyaları hesaba eklemek için düzen düzeltmeleri uygular.

Kullanım: Etiketleme

<ScrollViewer>

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

      <local:VirtualizingStackLayout />

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

</ScrollViewer>

Codebehind: Main.cs

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

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

repeater.ItemsSource = data;

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