Bläddra i källkod

Refactored virtualization handling into classes

Also take into account the scroll direction of the panel.
Steven Kirk 9 år sedan
förälder
incheckning
91e2f2a0ca

+ 3 - 0
src/Avalonia.Controls/Avalonia.Controls.csproj

@@ -70,7 +70,10 @@
     <Compile Include="Presenters\IContentPresenterHost.cs" />
     <Compile Include="Presenters\IItemsPresenterHost.cs" />
     <Compile Include="Presenters\ItemsPresenterBase.cs" />
+    <Compile Include="Presenters\ItemVirtualizerNone.cs" />
+    <Compile Include="Presenters\ItemVirtualizerSimple.cs" />
     <Compile Include="Presenters\ThingamybobPresenter.cs" />
+    <Compile Include="Presenters\ItemVirtualizer.cs" />
     <Compile Include="Primitives\HeaderedSelectingControl.cs" />
     <Compile Include="Primitives\IScrollable.cs" />
     <Compile Include="Primitives\TabStripItem.cs" />

+ 24 - 1
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
 {
+    /// <summary>
+    /// A panel that can be used to virtualize items.
+    /// </summary>
     public interface IVirtualizingPanel : IPanel
     {
+        /// <summary>
+        /// Gets a value indicating whether the panel is full.
+        /// </summary>
         bool IsFull { get; }
 
+        /// <summary>
+        /// Gets the number of items that can be removed while keeping the panel full.
+        /// </summary>
         int OverflowCount { get; }
 
+        /// <summary>
+        /// Gets the direction of scroll.
+        /// </summary>
+        Orientation ScrollDirection { get; }
+
+        /// <summary>
+        /// Gets the average size of the materialized items in the direction of scroll.
+        /// </summary>
         double AverageItemSize { get; }
 
+        /// <summary>
+        /// Gets or sets the current pixel offset of the items in the direction of scroll.
+        /// </summary>
         double PixelOffset { get; set; }
     }
 }

+ 52 - 0
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;
+        }
+    }
+}

+ 137 - 0
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<ItemContainerInfo> AddContainers(int index, IEnumerable items)
+        {
+            var generator = Owner.ItemContainerGenerator;
+            var result = new List<ItemContainerInfo>();
+            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<ItemContainerInfo> items)
+        {
+            var panel = Owner.Panel;
+
+            foreach (var i in items)
+            {
+                if (i.ContainerControl != null)
+                {
+                    panel.Children.Remove(i.ContainerControl);
+                }
+            }
+        }
+    }
+}

+ 93 - 0
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<object>().Skip(index);
+                var memberSelector = Owner.MemberSelector;
+
+                foreach (var item in items)
+                {
+                    var materialized = generator.Materialize(index++, item, memberSelector);
+                    panel.Children.Add(materialized.ContainerControl);
+
+                    if (panel.IsFull)
+                    {
+                        break;
+                    }
+                }
+
+                LastIndex = index - 1;
+            }
+
+            if (panel.OverflowCount > 0)
+            {
+                var count = panel.OverflowCount;
+                var index = panel.Children.Count - count;
+
+                panel.Children.RemoveRange(index, count);
+                generator.Dematerialize(index, count);
+
+                LastIndex -= count;
+            }
+        }
+    }
+}

+ 7 - 190
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@@ -26,7 +26,7 @@ namespace Avalonia.Controls.Presenters
                 nameof(VirtualizationMode),
                 defaultValue: ItemVirtualizationMode.Simple);
 
-        private VirtualizationInfo _virt;
+        private ItemVirtualizer _virtualizer;
 
         /// <summary>
         /// Initializes static members of the <see cref="ItemsPresenter"/> class.
@@ -50,44 +50,20 @@ namespace Avalonia.Controls.Presenters
         /// <inheritdoc/>
         bool IScrollable.IsLogicalScrollEnabled
         {
-            get { return _virt != null && VirtualizationMode != ItemVirtualizationMode.None; }
+            get { return _virtualizer?.IsLogicalScrollEnabled ?? false; }
         }
 
         /// <inheritdoc/>
         Action IScrollable.InvalidateScroll { get; set; }
 
         /// <inheritdoc/>
-        Size IScrollable.Extent
-        {
-            get
-            {
-                switch (VirtualizationMode)
-                {
-                    case ItemVirtualizationMode.Simple:
-                        return new Size(0, Items?.Count() ?? 0);
-                    default:
-                        return default(Size);
-                }
-            }
-        }
+        Size IScrollable.Extent => _virtualizer.Extent;
 
         /// <inheritdoc/>
         Vector IScrollable.Offset { get; set; }
 
         /// <inheritdoc/>
-        Size IScrollable.Viewport
-        {
-            get
-            {
-                switch (VirtualizationMode)
-                {
-                    case ItemVirtualizationMode.Simple:
-                        return new Size(0, (_virt.LastIndex - _virt.FirstIndex) + 1);
-                    default:
-                        return default(Size);
-                }
-            }
-        }
+        Size IScrollable.Viewport => _virtualizer.Viewport;
 
         /// <inheritdoc/>
         Size IScrollable.ScrollSize => new Size(0, 1);
@@ -99,25 +75,14 @@ namespace Avalonia.Controls.Presenters
         protected override Size ArrangeOverride(Size finalSize)
         {
             var result = base.ArrangeOverride(finalSize);
-
-            if (_virt != null)
-            {
-                CreateRemoveVirtualizedContainers();
-                ((IScrollable)this).InvalidateScroll();
-
-            }
-
+            _virtualizer.Arranging(finalSize);
             return result;
         }
 
         /// <inheritdoc/>
         protected override void PanelCreated(IPanel panel)
         {
-            if (((IScrollable)this).InvalidateScroll != null)
-            {
-                var virtualizingPanel = Panel as IVirtualizingPanel;
-                _virt = virtualizingPanel != null ? new VirtualizationInfo(virtualizingPanel) : null;
-            }
+            _virtualizer = ItemVirtualizer.Create(this);
 
             if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty))
             {
@@ -133,155 +98,7 @@ namespace Avalonia.Controls.Presenters
 
         protected override void ItemsChanged(NotifyCollectionChangedEventArgs e)
         {
-            if (_virt == null)
-            {
-                ItemsChangedNonVirtualized(e);
-            }
-            else
-            {
-                ItemsChangedVirtualized(e);
-            }
-        }
-
-        private void ItemsChangedNonVirtualized(NotifyCollectionChangedEventArgs e)
-        {
-            var generator = ItemContainerGenerator;
-
-            // 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);
-                    }
-
-                    AddContainersNonVirtualized(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 = AddContainersNonVirtualized(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)
-                    {
-                        AddContainersNonVirtualized(0, Items);
-                    }
-
-                    break;
-            }
-
-            InvalidateMeasure();
-        }
-
-        private void ItemsChangedVirtualized(NotifyCollectionChangedEventArgs e)
-        {
-        }
-
-        private IList<ItemContainerInfo> AddContainersNonVirtualized(int index, IEnumerable 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)
-                    {
-                        // TODO: This will insert at the wrong place when there are null items.
-                        this.Panel.Children.Insert(i.Index, i.ContainerControl);
-                    }
-                    else
-                    {
-                        this.Panel.Children.Add(i.ContainerControl);
-                    }
-                }
-
-                result.Add(i);
-            }
-
-            return result;
-        }
-
-        private void CreateRemoveVirtualizedContainers()
-        {
-            var generator = ItemContainerGenerator;
-            var panel = _virt.Panel;
-
-            if (!panel.IsFull)
-            {
-                var index = _virt.LastIndex + 1;
-                var items = Items.Cast<object>().Skip(index);
-                var memberSelector = MemberSelector;
-
-                foreach (var item in items)
-                {
-                    var materialized = generator.Materialize(index++, item, memberSelector);
-                    panel.Children.Add(materialized.ContainerControl);
-
-                    if (panel.IsFull)
-                    {
-                        break;
-                    }
-                }
-
-                _virt.LastIndex = index - 1;
-            }
-
-            if (panel.OverflowCount > 0)
-            {
-                var count = panel.OverflowCount;
-                var index = panel.Children.Count - count;
-
-                panel.Children.RemoveRange(index, count);
-                generator.Dematerialize(index, count);
-
-                _virt.LastIndex -= count;
-            }
-        }
-
-        private void RemoveContainers(IEnumerable<ItemContainerInfo> items)
-        {
-            foreach (var i in items)
-            {
-                if (i.ContainerControl != null)
-                {
-                    this.Panel.Children.Remove(i.ContainerControl);
-                }
-            }
-        }
-
-        private class VirtualizationInfo
-        {
-            public VirtualizationInfo(IVirtualizingPanel panel)
-            {
-                Panel = panel;
-            }
-
-            public IVirtualizingPanel Panel { get; }
-            public int FirstIndex { get; set; }
-            public int LastIndex { get; set; } = -1;
+            _virtualizer?.ItemsChanged(Items, e);
         }
     }
 }

+ 12 - 7
src/Avalonia.Controls/Utils/IEnumerableUtils.cs

@@ -17,17 +17,22 @@ namespace Avalonia.Controls.Utils
 
         public static int Count(this IEnumerable items)
         {
-            Contract.Requires<ArgumentNullException>(items != null);
-
-            var collection = items as ICollection;
-
-            if (collection != null)
+            if (items != null)
             {
-                return collection.Count;
+                var collection = items as ICollection;
+
+                if (collection != null)
+                {
+                    return collection.Count;
+                }
+                else
+                {
+                    return Enumerable.Count(items.Cast<object>());
+                }
             }
             else
             {
-                return Enumerable.Count(items.Cast<object>());
+                return 0;
             }
         }
 

+ 2 - 0
src/Avalonia.Controls/VirtualizingStackPanel.cs

@@ -26,6 +26,8 @@ namespace Avalonia.Controls
 
         int IVirtualizingPanel.OverflowCount => _canBeRemoved;
 
+        Orientation IVirtualizingPanel.ScrollDirection => Orientation;
+
         double IVirtualizingPanel.AverageItemSize => _averageItemSize;
 
         double IVirtualizingPanel.PixelOffset

+ 4 - 3
tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.v2.ncrunchproject

@@ -17,10 +17,11 @@
   <DetectStackOverflow>true</DetectStackOverflow>
   <IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
   <DefaultTestTimeout>60000</DefaultTestTimeout>
-  <UseBuildConfiguration />
-  <UseBuildPlatform />
-  <ProxyProcessPath />
+  <UseBuildConfiguration></UseBuildConfiguration>
+  <UseBuildPlatform></UseBuildPlatform>
+  <ProxyProcessPath></ProxyProcessPath>
   <UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
   <MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
   <BuildProcessArchitecture>x86</BuildProcessArchitecture>
+  <HiddenWarnings>LongTestTimesWithoutParallelExecution</HiddenWarnings>
 </ProjectConfiguration>

+ 34 - 7
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs

@@ -6,7 +6,6 @@ using System.Linq;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
-using Moq;
 using Xunit;
 
 namespace Avalonia.Controls.UnitTests.Presenters
@@ -124,7 +123,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
         public class Simple
         {
             [Fact]
-            public void Should_Return_Items_Count_For_Extent()
+            public void Should_Return_Items_Count_For_Extent_Vertical()
             {
                 var target = CreateTarget();
 
@@ -134,7 +133,17 @@ namespace Avalonia.Controls.UnitTests.Presenters
             }
 
             [Fact]
-            public void Should_Have_Number_Of_Visible_Items_As_Viewport()
+            public void Should_Return_Items_Count_For_Extent_Horizontal()
+            {
+                var target = CreateTarget(orientation: Orientation.Horizontal);
+
+                target.ApplyTemplate();
+
+                Assert.Equal(new Size(20, 0), ((IScrollable)target).Extent);
+            }
+
+            [Fact]
+            public void Should_Have_Number_Of_Visible_Items_As_Viewport_Vertical()
             {
                 var target = CreateTarget();
 
@@ -142,7 +151,19 @@ namespace Avalonia.Controls.UnitTests.Presenters
                 target.Measure(new Size(100, 100));
                 target.Arrange(new Rect(0, 0, 100, 100));
 
-                Assert.Equal(10, ((IScrollable)target).Viewport.Height);
+                Assert.Equal(new Size(0, 10), ((IScrollable)target).Viewport);
+            }
+
+            [Fact]
+            public void Should_Have_Number_Of_Visible_Items_As_Viewport_Horizontal()
+            {
+                var target = CreateTarget(orientation: Orientation.Horizontal);
+
+                target.ApplyTemplate();
+                target.Measure(new Size(100, 100));
+                target.Arrange(new Rect(0, 0, 100, 100));
+
+                Assert.Equal(new Size(10, 0), ((IScrollable)target).Viewport);
             }
 
             [Fact]
@@ -165,6 +186,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
 
         private static ItemsPresenter CreateTarget(
             ItemVirtualizationMode mode = ItemVirtualizationMode.Simple,
+            Orientation orientation = Orientation.Vertical,
             int itemCount = 20)
         {
             ItemsPresenter result;
@@ -175,7 +197,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
                 Content = result = new ItemsPresenter
                 {
                     Items = items,
-                    ItemsPanel = VirtualizingPanelTemplate(),
+                    ItemsPanel = VirtualizingPanelTemplate(orientation),
                     ItemTemplate = ItemTemplate(),
                     VirtualizationMode = mode,
                 }
@@ -190,13 +212,18 @@ namespace Avalonia.Controls.UnitTests.Presenters
         {
             return new FuncDataTemplate<string>(x => new Canvas
             {
+                Width = 10,
                 Height = 10,
             });
         }
 
-        private static ITemplate<IPanel> VirtualizingPanelTemplate()
+        private static ITemplate<IPanel> VirtualizingPanelTemplate(
+            Orientation orientation = Orientation.Vertical)
         {
-            return new FuncTemplate<IPanel>(() => new VirtualizingStackPanel());
+            return new FuncTemplate<IPanel>(() => new VirtualizingStackPanel
+            {
+                Orientation = orientation,
+            });
         }
     }
 }