diff --git a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs index 2e02bdc647..b5ac8aef6e 100644 --- a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs @@ -37,7 +37,7 @@ namespace Avalonia.Controls.Generators /// Creates a container control for an item. /// /// - /// The index of the item of the data in the containing collection. + /// The index of the item of data in the control's items. /// /// The item. /// An optional member selector. @@ -51,7 +51,7 @@ namespace Avalonia.Controls.Generators /// Removes a set of created containers. /// /// - /// The index of the first item of the data in the containing collection. + /// The index of the first item in the control's items. /// /// The the number of items to remove. /// The removed containers. @@ -69,12 +69,18 @@ namespace Avalonia.Controls.Generators /// the gap. /// /// - /// The index of the first item of the data in the containing collection. + /// The index of the first item in the control's items. /// /// The the number of items to remove. /// The removed containers. IEnumerable RemoveRange(int startingIndex, int count); + bool TryRecycle( + int oldIndex, + int newIndex, + object item, + IMemberSelector selector); + /// /// Clears all created containers and returns the removed controls. /// diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index 3bf69d910a..801f237804 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Subjects; using Avalonia.Controls.Templates; +using Avalonia.Controls.Utils; namespace Avalonia.Controls.Generators { @@ -102,6 +103,16 @@ namespace Avalonia.Controls.Generators return result; } + /// + public virtual bool TryRecycle( + int oldIndex, + int newIndex, + object item, + IMemberSelector selector) + { + return false; + } + /// public virtual IEnumerable Clear() { @@ -189,6 +200,21 @@ namespace Avalonia.Controls.Generators } } + /// + /// Moves a container. + /// + /// The old index. + /// The new index. + /// The new item. + /// The container info. + protected void MoveContainer(int oldIndex, int newIndex, object item) + { + var container = _containers[oldIndex]; + var newContainer = new ItemContainerInfo(container.ContainerControl, item, newIndex); + _containers[oldIndex] = null; + AddContainer(newContainer); + } + /// /// Gets all containers with an index that fall within a range. /// diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs index c4bc730e15..76922bfc55 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs @@ -75,5 +75,27 @@ namespace Avalonia.Controls.Generators return result; } } + + /// + public override bool TryRecycle( + int oldIndex, + int newIndex, + object item, + IMemberSelector selector) + { + var container = ContainerFromIndex(oldIndex); + var i = selector != null ? selector.Select(item) : item; + + container.SetValue(ContentProperty, i); + + if (!(item is IControl)) + { + container.DataContext = i; + } + + MoveContainer(oldIndex, newIndex, i); + + return true; + } } } diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index 68e410ae19..e26a4fb0d6 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -118,6 +118,11 @@ namespace Avalonia.Controls.Generators return base.RemoveRange(startingIndex, count); } + public override bool TryRecycle(int oldIndex, int newIndex, object item, IMemberSelector selector) + { + return false; + } + /// /// Gets the data template for the specified item. /// diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index fa2b987a07..d2572e4088 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -53,50 +53,10 @@ namespace Avalonia.Controls.Presenters var scroll = (VirtualizingPanel.ScrollDirection == Orientation.Vertical) ? value.Y : value.X; var delta = (int)(scroll - FirstIndex); - var panel = VirtualizingPanel; if (delta != 0) { - if (delta >= panel.Children.Count) - { - var index = FirstIndex + delta; - - foreach (var container in panel.Children) - { - container.DataContext = Items.ElementAt(index++); - } - } - else if (delta > 0) - { - var containers = panel.Children.GetRange(0, delta).ToList(); - panel.Children.RemoveRange(0, delta); - - var index = LastIndex + 1; - - foreach (var container in containers) - { - container.DataContext = Items.ElementAt(index++); - } - - panel.Children.AddRange(containers); - } - else - { - var first = panel.Children.Count + delta; - var count = -delta; - var containers = panel.Children.GetRange(first, count).ToList(); - panel.Children.RemoveRange(first, count); - - var index = FirstIndex + delta; - - foreach (var container in containers) - { - container.DataContext = Items.ElementAt(index++); - } - - panel.Children.InsertRange(0, containers); - } - + RecycleContainers(delta); FirstIndex += delta; LastIndex += delta; } @@ -167,5 +127,42 @@ namespace Avalonia.Controls.Presenters LastIndex -= count; } } + + private void RecycleContainers(int delta) + { + var panel = VirtualizingPanel; + var generator = Owner.ItemContainerGenerator; + var selector = Owner.MemberSelector; + var sign = delta < 0 ? -1 : 1; + var first = delta < 0 ? panel.Children.Count + delta : 0; + var count = Math.Abs(delta); + var containers = panel.Children.GetRange(first, count).ToList(); + + for (var i = 0; i < containers.Count; ++i) + { + var oldItemIndex = FirstIndex + first + i; + var newItemIndex = oldItemIndex + delta + ((panel.Children.Count - count) * sign); + var item = Items.ElementAt(newItemIndex); + + if (!generator.TryRecycle(oldItemIndex, newItemIndex, item, selector)) + { + throw new NotImplementedException(); + } + } + + if (delta < panel.Children.Count) + { + panel.Children.RemoveRange(first, count); + + if (delta > 0) + { + panel.Children.AddRange(containers); + } + else + { + panel.Children.InsertRange(0, containers); + } + } + } } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index 5fbed4d553..abebe85080 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -101,14 +101,7 @@ namespace Avalonia.Controls.Presenters { if (_generator == null) { - var i = TemplatedParent as ItemsControl; - _generator = i?.ItemContainerGenerator; - - if (_generator == null) - { - _generator = new ItemContainerGenerator(this); - _generator.ItemTemplate = ItemTemplate; - } + _generator = CreateItemContainerGenerator(); } return _generator; @@ -170,6 +163,26 @@ namespace Avalonia.Controls.Presenters } } + /// + /// Creates the for the control. + /// + /// + /// An or null. + /// + protected virtual IItemContainerGenerator CreateItemContainerGenerator() + { + var i = TemplatedParent as ItemsControl; + var result = i?.ItemContainerGenerator; + + if (result == null) + { + result = new ItemContainerGenerator(this); + result.ItemTemplate = ItemTemplate; + } + + return result; + } + /// protected override Size MeasureOverride(Size availableSize) { diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index fe56cd95ac..3072770127 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -205,7 +206,7 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(new Vector(0, 5), ((IScrollable)target).Offset); Assert.Equal(scrolledContainers, target.Panel.Children); - + for (var i = 0; i < target.Panel.Children.Count; ++i) { Assert.Equal(items[i + 5], target.Panel.Children[i].DataContext); @@ -215,6 +216,8 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(new Vector(0, 0), ((IScrollable)target).Offset); Assert.Equal(containers, target.Panel.Children); + var dcs = target.Panel.Children.Select(x => x.DataContext).ToList(); + for (var i = 0; i < target.Panel.Children.Count; ++i) { Assert.Equal(items[i], target.Panel.Children[i].DataContext); @@ -249,6 +252,7 @@ namespace Avalonia.Controls.UnitTests.Presenters private static ItemsPresenter CreateTarget( ItemVirtualizationMode mode = ItemVirtualizationMode.Simple, Orientation orientation = Orientation.Vertical, + bool useContainers = true, int itemCount = 20) { ItemsPresenter result; @@ -256,7 +260,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var scroller = new ScrollContentPresenter { - Content = result = new ItemsPresenter + Content = result = new TestItemsPresenter(useContainers) { Items = items, ItemsPanel = VirtualizingPanelTemplate(orientation), @@ -287,5 +291,31 @@ namespace Avalonia.Controls.UnitTests.Presenters Orientation = orientation, }); } + + private class TestItemsPresenter : ItemsPresenter + { + private bool _useContainers; + + public TestItemsPresenter(bool useContainers) + { + _useContainers = useContainers; + } + + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return _useContainers ? + new ItemContainerGenerator(this, TestContainer.ContentProperty, null) : + new ItemContainerGenerator(this); + } + } + + private class TestContainer : ContentControl + { + public TestContainer() + { + Width = 10; + Height = 10; + } + } } }