浏览代码

WIP: Adding virtualization to ItemsPresenter.

Steven Kirk 9 年之前
父节点
当前提交
f9b3d2ac03

+ 2 - 1
src/Avalonia.Controls/Avalonia.Controls.csproj

@@ -50,13 +50,14 @@
     <Compile Include="Design.cs" />
     <Compile Include="DockPanel.cs" />
     <Compile Include="Expander.cs" />
-    <Compile Include="Generators\ItemContainer.cs" />
+    <Compile Include="Generators\ItemContainerInfo.cs" />
     <Compile Include="Generators\TreeContainerIndex.cs" />
     <Compile Include="HotkeyManager.cs" />
     <Compile Include="IApplicationLifecycle.cs" />
     <Compile Include="INameScope.cs" />
     <Compile Include="IPseudoClasses.cs" />
     <Compile Include="DropDownItem.cs" />
+    <Compile Include="ItemVirtualizationMode.cs" />
     <Compile Include="IVirtualizingPanel.cs" />
     <Compile Include="LayoutTransformControl.cs" />
     <Compile Include="Mixins\ContentControlMixin.cs" />

+ 11 - 11
src/Avalonia.Controls/Generators/IItemContainerGenerator.cs

@@ -16,7 +16,7 @@ namespace Avalonia.Controls.Generators
         /// <summary>
         /// Gets the currently realized containers.
         /// </summary>
-        IEnumerable<ItemContainer> Containers { get; }
+        IEnumerable<ItemContainerInfo> Containers { get; }
 
         /// <summary>
         /// Gets or sets the data template used to display the items in the control.
@@ -34,17 +34,17 @@ namespace Avalonia.Controls.Generators
         event EventHandler<ItemContainerEventArgs> Dematerialized;
 
         /// <summary>
-        /// Creates container controls for a collection of items.
+        /// Creates a container control for an item.
         /// </summary>
-        /// <param name="startingIndex">
-        /// The index of the first item of the data in the containing collection.
+        /// <param name="index">
+        /// The index of the item of the data in the containing collection.
         /// </param>
-        /// <param name="items">The items.</param>
+        /// <param name="item">The item.</param>
         /// <param name="selector">An optional member selector.</param>
         /// <returns>The created controls.</returns>
-        IEnumerable<ItemContainer> Materialize(
-            int startingIndex,
-            IEnumerable items,
+        ItemContainerInfo Materialize(
+            int index,
+            object item,
             IMemberSelector selector);
 
         /// <summary>
@@ -55,7 +55,7 @@ namespace Avalonia.Controls.Generators
         /// </param>
         /// <param name="count">The the number of items to remove.</param>
         /// <returns>The removed containers.</returns>
-        IEnumerable<ItemContainer> Dematerialize(int startingIndex, int count);
+        IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count);
 
         /// <summary>
         /// Inserts space for newly inserted containers in the index.
@@ -73,13 +73,13 @@ namespace Avalonia.Controls.Generators
         /// </param>
         /// <param name="count">The the number of items to remove.</param>
         /// <returns>The removed containers.</returns>
-        IEnumerable<ItemContainer> RemoveRange(int startingIndex, int count);
+        IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count);
 
         /// <summary>
         /// Clears all created containers and returns the removed controls.
         /// </summary>
         /// <returns>The removed controls.</returns>
-        IEnumerable<ItemContainer> Clear();
+        IEnumerable<ItemContainerInfo> Clear();
 
         /// <summary>
         /// Gets the container control representing the item with the specified index.

+ 3 - 3
src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs

@@ -19,7 +19,7 @@ namespace Avalonia.Controls.Generators
         /// <param name="container">The container.</param>
         public ItemContainerEventArgs(
             int startingIndex,
-            ItemContainer container)
+            ItemContainerInfo container)
         {
             StartingIndex = startingIndex;
             Containers = new[] { container };
@@ -32,7 +32,7 @@ namespace Avalonia.Controls.Generators
         /// <param name="containers">The containers.</param>
         public ItemContainerEventArgs(
             int startingIndex, 
-            IList<ItemContainer> containers)
+            IList<ItemContainerInfo> containers)
         {
             StartingIndex = startingIndex;
             Containers = containers;
@@ -41,7 +41,7 @@ namespace Avalonia.Controls.Generators
         /// <summary>
         /// Gets the containers.
         /// </summary>
-        public IList<ItemContainer> Containers { get; }
+        public IList<ItemContainerInfo> Containers { get; }
 
         /// <summary>
         /// Gets the index of the first container in the source items.

+ 36 - 48
src/Avalonia.Controls/Generators/ItemContainerGenerator.cs

@@ -15,7 +15,7 @@ namespace Avalonia.Controls.Generators
     /// </summary>
     public class ItemContainerGenerator : IItemContainerGenerator
     {
-        private List<ItemContainer> _containers = new List<ItemContainer>();
+        private List<ItemContainerInfo> _containers = new List<ItemContainerInfo>();
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ItemContainerGenerator"/> class.
@@ -29,7 +29,7 @@ namespace Avalonia.Controls.Generators
         }
 
         /// <inheritdoc/>
-        public IEnumerable<ItemContainer> Containers => _containers.Where(x => x != null);
+        public IEnumerable<ItemContainerInfo> Containers => _containers.Where(x => x != null);
 
         /// <inheritdoc/>
         public event EventHandler<ItemContainerEventArgs> Materialized;
@@ -48,33 +48,24 @@ namespace Avalonia.Controls.Generators
         public IControl Owner { get; }
 
         /// <inheritdoc/>
-        public IEnumerable<ItemContainer> Materialize(
-            int startingIndex,
-            IEnumerable items,
+        public ItemContainerInfo Materialize(
+            int index,
+            object item,
             IMemberSelector selector)
         {
-            Contract.Requires<ArgumentNullException>(items != null);
+            var i = selector != null ? selector.Select(item) : item;
+            var container = new ItemContainerInfo(CreateContainer(i), item, index);
 
-            int index = startingIndex;
-            var result = new List<ItemContainer>();
+            AddContainer(container);
+            Materialized?.Invoke(this, new ItemContainerEventArgs(index, container));
 
-            foreach (var item in items)
-            {
-                var i = selector != null ? selector.Select(item) : item;
-                var container = new ItemContainer(CreateContainer(i), item, index++);
-                result.Add(container);
-            }
-
-            AddContainers(result);
-            Materialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result));
-
-            return result.Where(x => x != null).ToList();
+            return container;
         }
 
         /// <inheritdoc/>
-        public virtual IEnumerable<ItemContainer> Dematerialize(int startingIndex, int count)
+        public virtual IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count)
         {
-            var result = new List<ItemContainer>();
+            var result = new List<ItemContainerInfo>();
 
             for (int i = startingIndex; i < startingIndex + count; ++i)
             {
@@ -93,13 +84,13 @@ namespace Avalonia.Controls.Generators
         /// <inheritdoc/>
         public virtual void InsertSpace(int index, int count)
         {
-            _containers.InsertRange(index, Enumerable.Repeat<ItemContainer>(null, count));
+            _containers.InsertRange(index, Enumerable.Repeat<ItemContainerInfo>(null, count));
         }
 
         /// <inheritdoc/>
-        public virtual IEnumerable<ItemContainer> RemoveRange(int startingIndex, int count)
+        public virtual IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count)
         {
-            List<ItemContainer> result = new List<ItemContainer>();
+            List<ItemContainerInfo> result = new List<ItemContainerInfo>();
 
             if (startingIndex < _containers.Count)
             {
@@ -112,10 +103,10 @@ namespace Avalonia.Controls.Generators
         }
 
         /// <inheritdoc/>
-        public virtual IEnumerable<ItemContainer> Clear()
+        public virtual IEnumerable<ItemContainerInfo> Clear()
         {
             var result = _containers.Where(x => x != null).ToList();
-            _containers = new List<ItemContainer>();
+            _containers = new List<ItemContainerInfo>();
 
             if (result.Count > 0)
             {
@@ -172,32 +163,29 @@ namespace Avalonia.Controls.Generators
         }
 
         /// <summary>
-        /// Adds a collection of containers to the index.
+        /// Adds a container to the index.
         /// </summary>
-        /// <param name="containers">The containers.</param>
-        protected void AddContainers(IList<ItemContainer> containers)
+        /// <param name="container">The container.</param>
+        protected void AddContainer(ItemContainerInfo container)
         {
-            Contract.Requires<ArgumentNullException>(containers != null);
+            Contract.Requires<ArgumentNullException>(container != null);
 
-            foreach (var c in containers)
+            while (_containers.Count < container.Index)
             {
-                while (_containers.Count < c.Index)
-                {
-                    _containers.Add(null);
-                }
+                _containers.Add(null);
+            }
 
-                if (_containers.Count == c.Index)
-                {
-                    _containers.Add(c);
-                }
-                else if (_containers[c.Index] == null)
-                {
-                    _containers[c.Index] = c;
-                }
-                else
-                {
-                    throw new InvalidOperationException("Container already created.");
-                }
+            if (_containers.Count == container.Index)
+            {
+                _containers.Add(container);
+            }
+            else if (_containers[container.Index] == null)
+            {
+                _containers[container.Index] = container;
+            }
+            else
+            {
+                throw new InvalidOperationException("Container already created.");
             }
         }
 
@@ -207,7 +195,7 @@ namespace Avalonia.Controls.Generators
         /// <param name="index">The first index.</param>
         /// <param name="count">The number of elements in the range.</param>
         /// <returns>The containers.</returns>
-        protected IEnumerable<ItemContainer> GetContainerRange(int index, int count)
+        protected IEnumerable<ItemContainerInfo> GetContainerRange(int index, int count)
         {
             return _containers.GetRange(index, count);
         }

+ 3 - 3
src/Avalonia.Controls/Generators/ItemContainer.cs → src/Avalonia.Controls/Generators/ItemContainerInfo.cs

@@ -7,17 +7,17 @@ namespace Avalonia.Controls.Generators
     /// Holds information about an item container generated by an 
     /// <see cref="IItemContainerGenerator"/>.
     /// </summary>
-    public class ItemContainer
+    public class ItemContainerInfo
     {
         /// <summary>
-        /// Initializes a new instance of the <see cref="ItemContainer"/> class.
+        /// Initializes a new instance of the <see cref="ItemContainerInfo"/> class.
         /// </summary>
         /// <param name="container">The container control.</param>
         /// <param name="item">The item that the container represents.</param>
         /// <param name="index">
         /// The index of the item in the <see cref="ItemsControl.Items"/> collection.
         /// </param>
-        public ItemContainer(IControl container, object item, int index)
+        public ItemContainerInfo(IControl container, object item, int index)
         {
             ContainerControl = container;
             Item = item;

+ 3 - 3
src/Avalonia.Controls/Generators/TreeContainerIndex.cs

@@ -47,7 +47,7 @@ namespace Avalonia.Controls.Generators
 
             Materialized?.Invoke(
                 this, 
-                new ItemContainerEventArgs(0, new ItemContainer(container, item, 0)));
+                new ItemContainerEventArgs(0, new ItemContainerInfo(container, item, 0)));
         }
 
         /// <summary>
@@ -62,14 +62,14 @@ namespace Avalonia.Controls.Generators
 
             Dematerialized?.Invoke(
                 this, 
-                new ItemContainerEventArgs(0, new ItemContainer(container, item, 0)));
+                new ItemContainerEventArgs(0, new ItemContainerInfo(container, item, 0)));
         }
 
         /// <summary>
         /// Removes a set of containers from the index.
         /// </summary>
         /// <param name="containers">The item containers.</param>
-        public void Remove(IEnumerable<ItemContainer> containers)
+        public void Remove(IEnumerable<ItemContainerInfo> containers)
         {
             foreach (var container in containers)
             {

+ 3 - 3
src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs

@@ -99,20 +99,20 @@ namespace Avalonia.Controls.Generators
             }
         }
 
-        public override IEnumerable<ItemContainer> Clear()
+        public override IEnumerable<ItemContainerInfo> Clear()
         {
             var items = base.Clear();
             Index.Remove(items);
             return items;
         }
 
-        public override IEnumerable<ItemContainer> Dematerialize(int startingIndex, int count)
+        public override IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count)
         {
             Index.Remove(GetContainerRange(startingIndex, count));
             return base.Dematerialize(startingIndex, count);
         }
 
-        public override IEnumerable<ItemContainer> RemoveRange(int startingIndex, int count)
+        public override IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count)
         {
             Index.Remove(GetContainerRange(startingIndex, count));
             return base.RemoveRange(startingIndex, count);

+ 21 - 0
src/Avalonia.Controls/ItemVirtualizationMode.cs

@@ -0,0 +1,21 @@
+// 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.
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Describes the item virtualization method to use for a list.
+    /// </summary>
+    public enum ItemVirtualizationMode
+    {
+        /// <summary>
+        /// Do not virtualize items.
+        /// </summary>
+        None,
+
+        /// <summary>
+        /// Virtualize items without smooth scrolling.
+        /// </summary>
+        Simple,
+    }
+}

+ 2 - 6
src/Avalonia.Controls/Presenters/CarouselPresenter.cs

@@ -175,12 +175,8 @@ namespace Avalonia.Controls.Presenters
             if (container == null)
             {
                 var item = Items.Cast<object>().ElementAt(index);
-                var materialized = ItemContainerGenerator.Materialize(
-                    index,
-                    new[] { item },
-                    MemberSelector);
-                container = materialized.First().ContainerControl;
-                Panel.Children.Add(container);
+                var materialized = ItemContainerGenerator.Materialize(index, item, MemberSelector);
+                Panel.Children.Add(materialized.ContainerControl);
             }
 
             return container;

+ 99 - 28
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@@ -1,9 +1,12 @@
 // 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.Primitives;
 using Avalonia.Controls.Utils;
 using Avalonia.Input;
 
@@ -12,8 +15,18 @@ namespace Avalonia.Controls.Presenters
     /// <summary>
     /// Displays items inside an <see cref="ItemsControl"/>.
     /// </summary>
-    public class ItemsPresenter : ItemsPresenterBase
+    public class ItemsPresenter : ItemsPresenterBase, IScrollable
     {
+        /// <summary>
+        /// Defines the <see cref="VirtualizationMode"/> property.
+        /// </summary>
+        public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty =
+            AvaloniaProperty.Register<ItemsPresenter, ItemVirtualizationMode>(
+                nameof(VirtualizationMode),
+                defaultValue: ItemVirtualizationMode.Simple);
+
+        private VirtualizationInfo _virt;
+
         /// <summary>
         /// Initializes static members of the <see cref="ItemsPresenter"/> class.
         /// </summary>
@@ -24,11 +37,72 @@ namespace Avalonia.Controls.Presenters
                 KeyboardNavigationMode.Once);
         }
 
+        /// <summary>
+        /// Gets or sets the virtualization mode for the items.
+        /// </summary>
+        public ItemVirtualizationMode VirtualizationMode
+        {
+            get { return GetValue(VirtualizationModeProperty); }
+            set { SetValue(VirtualizationModeProperty, value); }
+        }
+
+        /// <inheritdoc/>
+        bool IScrollable.IsLogicalScrollEnabled
+        {
+            get { return _virt != null && VirtualizationMode != ItemVirtualizationMode.None; }
+        }
+
+        /// <inheritdoc/>
+        Action IScrollable.InvalidateScroll { get; set; }
+
+        Size IScrollable.Extent
+        {
+            get
+            {
+                switch (VirtualizationMode)
+                {
+                    case ItemVirtualizationMode.Simple:
+                        return new Size(0, Items?.Count() ?? 0);
+                    default:
+                        return default(Size);
+                }
+            }
+        }
+
+        Vector IScrollable.Offset { get; set; }
+
+        Size IScrollable.Viewport
+        {
+            get
+            {
+                throw new NotImplementedException();
+            }
+        }
+
+        Size IScrollable.ScrollSize
+        {
+            get
+            {
+                throw new NotImplementedException();
+            }
+        }
+
+        Size IScrollable.PageScrollSize
+        {
+            get
+            {
+                throw new NotImplementedException();
+            }
+        }
+
         /// <inheritdoc/>
         protected override void CreatePanel()
         {
             base.CreatePanel();
 
+            var virtualizingPanel = Panel as IVirtualizingPanel;
+            _virt = virtualizingPanel != null ? new VirtualizationInfo(virtualizingPanel) : null;
+
             if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty))
             {
                 KeyboardNavigation.SetDirectionalNavigation(
@@ -55,7 +129,7 @@ namespace Avalonia.Controls.Presenters
                         generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count);
                     }
 
-                    AddContainers(generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector));
+                    AddContainers(e.NewStartingIndex, e.NewItems);
                     break;
 
                 case NotifyCollectionChangedAction.Remove:
@@ -64,8 +138,7 @@ namespace Avalonia.Controls.Presenters
 
                 case NotifyCollectionChangedAction.Replace:
                     RemoveContainers(generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count));
-                    var containers = generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector);
-                    AddContainers(containers);
+                    var containers = AddContainers(e.NewStartingIndex, e.NewItems);
 
                     var i = e.NewStartingIndex;
 
@@ -83,7 +156,7 @@ namespace Avalonia.Controls.Presenters
 
                     if (Items != null)
                     {
-                        AddContainers(generator.Materialize(0, Items, MemberSelector));
+                        AddContainers(0, Items);
                     }
 
                     break;
@@ -92,17 +165,20 @@ namespace Avalonia.Controls.Presenters
             InvalidateMeasure();
         }
 
-        private void AddContainersToPanel(IEnumerable<ItemContainer> items)
+        private IList<ItemContainerInfo> AddContainers(int index, IEnumerable items)
         {
-            foreach (var i in items)
+            var generator = ItemContainerGenerator;
+            var result = new List<ItemContainerInfo>();
+
+            foreach (var item in items)
             {
+                var i = generator.Materialize(index++, item, MemberSelector);
+
                 if (i.ContainerControl != null)
                 {
                     if (i.Index < this.Panel.Children.Count)
                     {
-                        // HACK: This will insert at the wrong place when there are null items,
-                        // but all of this will need to be rewritten when we implement 
-                        // virtualization so hope no-one notices until then :)
+                        // TODO: This will insert at the wrong place when there are null items.
                         this.Panel.Children.Insert(i.Index, i.ContainerControl);
                     }
                     else
@@ -110,39 +186,34 @@ namespace Avalonia.Controls.Presenters
                         this.Panel.Children.Add(i.ContainerControl);
                     }
                 }
+
+                result.Add(i);
             }
+
+            return result;
         }
 
-        private void AddContainers(IEnumerable<ItemContainer> items)
+        private void RemoveContainers(IEnumerable<ItemContainerInfo> items)
         {
             foreach (var i in items)
             {
                 if (i.ContainerControl != null)
                 {
-                    if (i.Index < this.Panel.Children.Count)
-                    {
-                        // HACK: This will insert at the wrong place when there are null items,
-                        // but all of this will need to be rewritten when we implement 
-                        // virtualization so hope no-one notices until then :)
-                        this.Panel.Children.Insert(i.Index, i.ContainerControl);
-                    }
-                    else
-                    {
-                        this.Panel.Children.Add(i.ContainerControl);
-                    }
+                    this.Panel.Children.Remove(i.ContainerControl);
                 }
             }
         }
 
-        private void RemoveContainers(IEnumerable<ItemContainer> items)
+        private class VirtualizationInfo
         {
-            foreach (var i in items)
+            public VirtualizationInfo(IVirtualizingPanel panel)
             {
-                if (i.ContainerControl != null)
-                {
-                    this.Panel.Children.Remove(i.ContainerControl);
-                }
+                Panel = panel;
             }
+
+            public IVirtualizingPanel Panel { get; }
+            public int FirstIndex { get; set; }
+            public int LastIndex { get; set; }
         }
     }
 }

+ 1 - 1
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@@ -206,7 +206,7 @@ namespace Avalonia.Controls.Presenters
 
         /// <summary>
         /// Called when the items for the presenter change, either because <see cref="Items"/>
-        /// has been set, or the items collection has been modified.
+        /// has been set, the items collection has been modified, or the panel has been created.
         /// </summary>
         /// <param name="e">A description of the change.</param>
         protected abstract void ItemsChanged(NotifyCollectionChangedEventArgs e);

+ 1 - 0
tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj

@@ -91,6 +91,7 @@
   <ItemGroup>
     <Compile Include="ClassesTests.cs" />
     <Compile Include="LayoutTransformControlTests.cs" />
+    <Compile Include="Presenters\ItemsPresenterTests_Virtualization.cs" />
     <Compile Include="TextBoxTests_ValidationState.cs" />
     <Compile Include="UserControlTests.cs" />
     <Compile Include="DockPanelTests.cs" />

+ 25 - 6
tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs

@@ -1,8 +1,11 @@
 // 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.Generic;
 using System.Linq;
 using Avalonia.Controls.Generators;
+using Avalonia.Controls.Templates;
 using Xunit;
 
 namespace Avalonia.Controls.UnitTests.Generators
@@ -15,7 +18,7 @@ namespace Avalonia.Controls.UnitTests.Generators
             var items = new[] { "foo", "bar", "baz" };
             var owner = new Decorator();
             var target = new ItemContainerGenerator(owner);
-            var containers = target.Materialize(0, items, null);
+            var containers = Materialize(target, 0, items);
             var result = containers
                 .Select(x => x.ContainerControl)
                 .OfType<TextBlock>()
@@ -31,20 +34,36 @@ namespace Avalonia.Controls.UnitTests.Generators
             var items = new[] { "foo", "bar", "baz" };
             var owner = new Decorator();
             var target = new ItemContainerGenerator(owner);
-            var containers = target.Materialize(0, items, null).ToList();
+            var containers = Materialize(target, 0, items);
 
             Assert.Equal(containers[0].ContainerControl, target.ContainerFromIndex(0));
             Assert.Equal(containers[1].ContainerControl, target.ContainerFromIndex(1));
             Assert.Equal(containers[2].ContainerControl, target.ContainerFromIndex(2));
         }
 
+        private IList<ItemContainerInfo> Materialize(
+            IItemContainerGenerator generator,
+            int index,
+            string[] items)
+        {
+            var result = new List<ItemContainerInfo>();
+
+            foreach (var item in items)
+            {
+                var container = generator.Materialize(index++, item, null);
+                result.Add(container);
+            }
+
+            return result;
+        }
+
         [Fact]
         public void IndexFromContainer_Should_Return_Index()
         {
             var items = new[] { "foo", "bar", "baz" };
             var owner = new Decorator();
             var target = new ItemContainerGenerator(owner);
-            var containers = target.Materialize(0, items, null).ToList();
+            var containers = Materialize(target, 0, items);
 
             Assert.Equal(0, target.IndexFromContainer(containers[0].ContainerControl));
             Assert.Equal(1, target.IndexFromContainer(containers[1].ContainerControl));
@@ -57,7 +76,7 @@ namespace Avalonia.Controls.UnitTests.Generators
             var items = new[] { "foo", "bar", "baz" };
             var owner = new Decorator();
             var target = new ItemContainerGenerator(owner);
-            var containers = target.Materialize(0, items, null).ToList();
+            var containers = Materialize(target, 0, items);
 
             target.Dematerialize(1, 1);
 
@@ -72,7 +91,7 @@ namespace Avalonia.Controls.UnitTests.Generators
             var items = new[] { "foo", "bar", "baz" };
             var owner = new Decorator();
             var target = new ItemContainerGenerator(owner);
-            var containers = target.Materialize(0, items, null);
+            var containers = Materialize(target, 0, items);
             var expected = target.Containers.Take(2).ToList();
             var result = target.Dematerialize(0, 2);
 
@@ -85,7 +104,7 @@ namespace Avalonia.Controls.UnitTests.Generators
             var items = new[] { "foo", "bar", "baz" };
             var owner = new Decorator();
             var target = new ItemContainerGenerator(owner);
-            var containers = target.Materialize(0, items, null).ToList();
+            var containers = Materialize(target, 0, items);
 
             var removed = target.RemoveRange(1, 1).Single();
 

+ 18 - 1
tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs

@@ -1,6 +1,7 @@
 // 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.Collections.Generic;
 using System.Linq;
 using Avalonia.Controls.Generators;
 using Xunit;
@@ -15,7 +16,7 @@ namespace Avalonia.Controls.UnitTests.Generators
             var items = new[] { "foo", "bar", "baz" };
             var owner = new Decorator();
             var target = new ItemContainerGenerator<ListBoxItem>(owner, ListBoxItem.ContentProperty, null);
-            var containers = target.Materialize(0, items, null);
+            var containers = Materialize(target, 0, items);
             var result = containers
                 .Select(x => x.ContainerControl)
                 .OfType<ListBoxItem>()
@@ -24,5 +25,21 @@ namespace Avalonia.Controls.UnitTests.Generators
 
             Assert.Equal(items, result);
         }
+
+        private IList<ItemContainerInfo> Materialize(
+            IItemContainerGenerator generator,
+            int index,
+            string[] items)
+        {
+            var result = new List<ItemContainerInfo>();
+
+            foreach (var item in items)
+            {
+                var container = generator.Materialize(index++, item, null);
+                result.Add(container);
+            }
+
+            return result;
+        }
     }
 }

+ 103 - 0
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs

@@ -0,0 +1,103 @@
+// 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 Avalonia.Controls.Presenters;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
+using Moq;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests.Presenters
+{
+    public class ItemsPresenterTests_Virtualization
+    {
+        [Fact]
+        public void Should_Return_IsLogicalScrollEnabled_False_When_Has_No_Virtualizing_Panel()
+        {
+            var target = new ItemsPresenter
+            {
+            };
+
+            target.ApplyTemplate();
+
+            Assert.False(((IScrollable)target).IsLogicalScrollEnabled);
+        }
+
+        [Fact]
+        public void Should_Return_IsLogicalScrollEnabled_False_When_VirtualizationMode_None()
+        {
+            var target = new ItemsPresenter
+            {
+                ItemsPanel = VirtualizingPanelTemplate(),
+                VirtualizationMode = ItemVirtualizationMode.None,
+            };
+
+            target.ApplyTemplate();
+
+            Assert.False(((IScrollable)target).IsLogicalScrollEnabled);
+        }
+
+        [Fact]
+        public void Should_Return_IsLogicalScrollEnabled_True_When_Has_Virtualizing_Panel()
+        {
+            var target = new ItemsPresenter
+            {
+                ItemsPanel = VirtualizingPanelTemplate(),
+            };
+
+            target.ApplyTemplate();
+
+            Assert.True(((IScrollable)target).IsLogicalScrollEnabled);
+        }
+
+        public class Simple
+        {
+            [Fact]
+            public void Should_Return_Items_Count_For_Extent()
+            {
+                var target = new ItemsPresenter
+                {
+                    Items = new string[10],
+                    ItemsPanel = VirtualizingPanelTemplate(),
+                    VirtualizationMode = ItemVirtualizationMode.Simple,
+                };
+
+                target.ApplyTemplate();
+
+                Assert.Equal(new Size(0, 10), ((IScrollable)target).Extent);
+            }
+
+            [Fact]
+            public void Should_Have_Number_Of_Visible_Items_As_Viewport()
+            {
+                var target = new ItemsPresenter
+                {
+                    Items = new string[20],
+                    ItemsPanel = VirtualizingPanelTemplate(),
+                    ItemTemplate = ItemTemplate(),
+                    VirtualizationMode = ItemVirtualizationMode.Simple,
+                };
+
+                target.ApplyTemplate();
+                target.Measure(new Size(100, 100));
+                target.Arrange(new Rect(0, 0, 100, 100));
+
+                Assert.Equal(10, ((IScrollable)target).Viewport.Height);
+            }
+        }
+
+        private static IDataTemplate ItemTemplate()
+        {
+            return new FuncDataTemplate<string>(x => new TextBlock
+            {
+                Text = x,
+                Height = 10,
+            });
+        }
+
+        private static ITemplate<IPanel> VirtualizingPanelTemplate()
+        {
+            return new FuncTemplate<IPanel>(() => new VirtualizingStackPanel());
+        }
+    }
+}