diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj
index 52e329189a..9fe4fecc68 100644
--- a/src/Avalonia.Controls/Avalonia.Controls.csproj
+++ b/src/Avalonia.Controls/Avalonia.Controls.csproj
@@ -70,7 +70,10 @@
+
+
+
diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs
index ca7ffae145..2aa356d38c 100644
--- a/src/Avalonia.Controls/IVirtualizingPanel.cs
+++ b/src/Avalonia.Controls/IVirtualizingPanel.cs
@@ -1,15 +1,38 @@
-using System;
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
namespace Avalonia.Controls
{
+ ///
+ /// A panel that can be used to virtualize items.
+ ///
public interface IVirtualizingPanel : IPanel
{
+ ///
+ /// Gets a value indicating whether the panel is full.
+ ///
bool IsFull { get; }
+ ///
+ /// Gets the number of items that can be removed while keeping the panel full.
+ ///
int OverflowCount { get; }
+ ///
+ /// Gets the direction of scroll.
+ ///
+ Orientation ScrollDirection { get; }
+
+ ///
+ /// Gets the average size of the materialized items in the direction of scroll.
+ ///
double AverageItemSize { get; }
+ ///
+ /// Gets or sets the current pixel offset of the items in the direction of scroll.
+ ///
double PixelOffset { get; set; }
}
}
diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
new file mode 100644
index 0000000000..b95511e635
--- /dev/null
+++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
@@ -0,0 +1,52 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.Collections.Specialized;
+using Avalonia.Controls.Primitives;
+
+namespace Avalonia.Controls.Presenters
+{
+ internal abstract class ItemVirtualizer
+ {
+ public ItemVirtualizer(ItemsPresenter owner)
+ {
+ Owner = owner;
+ }
+
+ public ItemsPresenter Owner { get; }
+ public IVirtualizingPanel VirtualizingPanel => Owner.Panel as IVirtualizingPanel;
+ public IEnumerable Items { get; private set; }
+ public int FirstIndex { get; set; }
+ public int LastIndex { get; set; } = -1;
+
+ public abstract bool IsLogicalScrollEnabled { get; }
+ public abstract Size Extent { get; }
+ public abstract Size Viewport { get; }
+
+ public static ItemVirtualizer Create(ItemsPresenter owner)
+ {
+ var virtualizingPanel = owner.Panel as IVirtualizingPanel;
+ var scrollable = (IScrollable)owner;
+
+ if (virtualizingPanel != null && scrollable.InvalidateScroll != null)
+ {
+ switch (owner.VirtualizationMode)
+ {
+ case ItemVirtualizationMode.Simple:
+ return new ItemVirtualizerSimple(owner);
+ }
+ }
+
+ return new ItemVirtualizerNone(owner);
+ }
+
+ public abstract void Arranging(Size finalSize);
+
+ public virtual void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e)
+ {
+ Items = items;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs
new file mode 100644
index 0000000000..c5ccb2ec0b
--- /dev/null
+++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs
@@ -0,0 +1,137 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using Avalonia.Controls.Generators;
+using Avalonia.Controls.Utils;
+
+namespace Avalonia.Controls.Presenters
+{
+ internal class ItemVirtualizerNone : ItemVirtualizer
+ {
+ public ItemVirtualizerNone(ItemsPresenter owner)
+ : base(owner)
+ {
+ }
+
+ public override bool IsLogicalScrollEnabled => false;
+
+ public override Size Extent
+ {
+ get
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ public override Size Viewport
+ {
+ get
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ public override void Arranging(Size finalSize)
+ {
+ // We don't need to do anything here.
+ }
+
+ public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e)
+ {
+ base.ItemsChanged(items, e);
+
+ var generator = Owner.ItemContainerGenerator;
+ var panel = Owner.Panel;
+
+ // TODO: Handle Move and Replace etc.
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ if (e.NewStartingIndex + e.NewItems.Count < Items.Count())
+ {
+ generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count);
+ }
+
+ AddContainers(e.NewStartingIndex, e.NewItems);
+ break;
+
+ case NotifyCollectionChangedAction.Remove:
+ RemoveContainers(generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count));
+ break;
+
+ case NotifyCollectionChangedAction.Replace:
+ RemoveContainers(generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count));
+ var containers = AddContainers(e.NewStartingIndex, e.NewItems);
+
+ var i = e.NewStartingIndex;
+
+ foreach (var container in containers)
+ {
+ panel.Children[i++] = container.ContainerControl;
+ }
+
+ break;
+
+ case NotifyCollectionChangedAction.Move:
+ // TODO: Implement Move in a more efficient manner.
+ case NotifyCollectionChangedAction.Reset:
+ RemoveContainers(generator.Clear());
+
+ if (Items != null)
+ {
+ AddContainers(0, Items);
+ }
+
+ break;
+ }
+
+ Owner.InvalidateMeasure();
+ }
+
+ private IList AddContainers(int index, IEnumerable items)
+ {
+ var generator = Owner.ItemContainerGenerator;
+ var result = new List();
+ var panel = Owner.Panel;
+
+ foreach (var item in items)
+ {
+ var i = generator.Materialize(index++, item, Owner.MemberSelector);
+
+ if (i.ContainerControl != null)
+ {
+ if (i.Index < panel.Children.Count)
+ {
+ // TODO: This will insert at the wrong place when there are null items.
+ panel.Children.Insert(i.Index, i.ContainerControl);
+ }
+ else
+ {
+ panel.Children.Add(i.ContainerControl);
+ }
+ }
+
+ result.Add(i);
+ }
+
+ return result;
+ }
+
+ private void RemoveContainers(IEnumerable items)
+ {
+ var panel = Owner.Panel;
+
+ foreach (var i in items)
+ {
+ if (i.ContainerControl != null)
+ {
+ panel.Children.Remove(i.ContainerControl);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
new file mode 100644
index 0000000000..fb40960778
--- /dev/null
+++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
@@ -0,0 +1,93 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Linq;
+using Avalonia.Controls.Utils;
+
+namespace Avalonia.Controls.Presenters
+{
+ internal class ItemVirtualizerSimple : ItemVirtualizer
+ {
+ public ItemVirtualizerSimple(ItemsPresenter owner)
+ : base(owner)
+ {
+ }
+
+ public override bool IsLogicalScrollEnabled => true;
+
+ public override Size Extent
+ {
+ get
+ {
+ if (VirtualizingPanel.ScrollDirection == Orientation.Vertical)
+ {
+ return new Size(0, Items.Count());
+ }
+ else
+ {
+ return new Size(Items.Count(), 0);
+ }
+ }
+ }
+
+ public override Size Viewport
+ {
+ get
+ {
+ var panel = VirtualizingPanel;
+
+ if (panel.ScrollDirection == Orientation.Vertical)
+ {
+ return new Size(0, panel.Children.Count);
+ }
+ else
+ {
+ return new Size(panel.Children.Count, 0);
+ }
+ }
+ }
+
+ public override void Arranging(Size finalSize)
+ {
+ CreateRemoveContainers();
+ }
+
+ private void CreateRemoveContainers()
+ {
+ var generator = Owner.ItemContainerGenerator;
+ var panel = VirtualizingPanel;
+
+ if (!panel.IsFull)
+ {
+ var index = LastIndex + 1;
+ var items = Items.Cast