From 91e2f2a0ca786d4d57b7a875c405b8bc7a97b52f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 23 May 2016 09:56:03 +0200 Subject: [PATCH] Refactored virtualization handling into classes Also take into account the scroll direction of the panel. --- .../Avalonia.Controls.csproj | 3 + src/Avalonia.Controls/IVirtualizingPanel.cs | 25 ++- .../Presenters/ItemVirtualizer.cs | 52 +++++ .../Presenters/ItemVirtualizerNone.cs | 137 ++++++++++++ .../Presenters/ItemVirtualizerSimple.cs | 93 +++++++++ .../Presenters/ItemsPresenter.cs | 197 +----------------- .../Utils/IEnumerableUtils.cs | 19 +- .../VirtualizingStackPanel.cs | 2 + ...lonia.Controls.UnitTests.v2.ncrunchproject | 7 +- .../ItemsPresenterTests_Virtualization.cs | 41 +++- 10 files changed, 368 insertions(+), 208 deletions(-) create mode 100644 src/Avalonia.Controls/Presenters/ItemVirtualizer.cs create mode 100644 src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs create mode 100644 src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs 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().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; + } + } + } +} diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 23fe8d0a69..fa05d1bbd0 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/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; /// /// Initializes static members of the class. @@ -50,44 +50,20 @@ namespace Avalonia.Controls.Presenters /// bool IScrollable.IsLogicalScrollEnabled { - get { return _virt != null && VirtualizationMode != ItemVirtualizationMode.None; } + get { return _virtualizer?.IsLogicalScrollEnabled ?? false; } } /// 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); - } - } - } + Size IScrollable.Extent => _virtualizer.Extent; /// Vector IScrollable.Offset { get; set; } /// - 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; /// 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; } /// 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 AddContainersNonVirtualized(int index, IEnumerable items) - { - var generator = ItemContainerGenerator; - var result = new List(); - - 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().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 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); } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs index 1e05c23e5a..ad95b0269a 100644 --- a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs +++ b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs @@ -17,17 +17,22 @@ namespace Avalonia.Controls.Utils public static int Count(this IEnumerable items) { - Contract.Requires(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()); + } } else { - return Enumerable.Count(items.Cast()); + return 0; } } diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 865ad5d895..afb691d2e5 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/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 diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.v2.ncrunchproject b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.v2.ncrunchproject index 30815b1937..b5cd70b13f 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.v2.ncrunchproject +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.v2.ncrunchproject @@ -17,10 +17,11 @@ true true 60000 - - - + + + AutoDetect STA x86 + LongTestTimesWithoutParallelExecution \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index aba9dbb7cf..097ce08171 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/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(x => new Canvas { + Width = 10, Height = 10, }); } - private static ITemplate VirtualizingPanelTemplate() + private static ITemplate VirtualizingPanelTemplate( + Orientation orientation = Orientation.Vertical) { - return new FuncTemplate(() => new VirtualizingStackPanel()); + return new FuncTemplate(() => new VirtualizingStackPanel + { + Orientation = orientation, + }); } } }