diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationTest/MainWindow.xaml index b8fffe6688..a6fd6918ef 100644 --- a/samples/VirtualizationTest/MainWindow.xaml +++ b/samples/VirtualizationTest/MainWindow.xaml @@ -17,6 +17,10 @@ + + diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 3c972e922f..1ff85c50ac 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -47,7 +47,7 @@ namespace Avalonia.Controls.Presenters if (delta != 0) { - RecycleContainers(delta); + RecycleMoveContainers(delta); FirstIndex += delta; NextIndex += delta; } @@ -56,7 +56,7 @@ namespace Avalonia.Controls.Presenters { // We're moving to a partially obscured item at the end of the list. var firstIndex = ItemCount - panel.Children.Count; - RecycleContainers(firstIndex - FirstIndex); + RecycleMoveContainers(firstIndex - FirstIndex); NextIndex = ItemCount; FirstIndex = NextIndex - panel.Children.Count; panel.PixelOffset = VirtualizingPanel.PixelOverflow; @@ -85,14 +85,26 @@ namespace Avalonia.Controls.Presenters { base.ItemsChanged(items, e); - if (e.Action == NotifyCollectionChangedAction.Reset) + switch (e.Action) { - // We could recycle items here if this proves to be inefficient, but - // Reset indicates a large change and should (?) be quite rare. - VirtualizingPanel.Children.Clear(); - Owner.ItemContainerGenerator.Clear(); - FirstIndex = NextIndex = 0; - CreateRemoveContainers(); + case NotifyCollectionChangedAction.Add: + if (e.NewStartingIndex >= FirstIndex && + e.NewStartingIndex + e.NewItems.Count < NextIndex) + { + CreateRemoveContainers(); + RecycleContainers(); + } + + break; + + case NotifyCollectionChangedAction.Reset: + // We could recycle items here if this proves to be inefficient, but + // Reset indicates a large change and should (?) be quite rare. + VirtualizingPanel.Children.Clear(); + Owner.ItemContainerGenerator.Clear(); + FirstIndex = NextIndex = 0; + CreateRemoveContainers(); + break; } ((ILogicalScrollable)Owner).InvalidateScroll(); @@ -161,7 +173,31 @@ namespace Avalonia.Controls.Presenters } } - private void RecycleContainers(int delta) + private void RecycleContainers() + { + var panel = VirtualizingPanel; + var generator = Owner.ItemContainerGenerator; + var selector = Owner.MemberSelector; + var containers = generator.Containers.ToList(); + var itemIndex = FirstIndex; + + foreach (var container in containers) + { + var item = Items.ElementAt(itemIndex); + + if (!object.Equals(container.Item, item)) + { + if (!generator.TryRecycle(itemIndex, itemIndex, item, selector)) + { + throw new NotImplementedException(); + } + } + + ++itemIndex; + } + } + + private void RecycleMoveContainers(int delta) { var panel = VirtualizingPanel; var generator = Owner.ItemContainerGenerator; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 013487f8fc..036ecdce6f 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; @@ -154,6 +155,32 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(5, ((IVirtualizingPanel)target.Panel).PixelOffset); } + [Fact] + public void Inserting_Items_Should_Update_Containers() + { + var target = CreateTarget(itemCount: 20); + + target.ApplyTemplate(); + target.Measure(new Size(100, 95)); + target.Arrange(new Rect(0, 0, 100, 95)); + + ((ILogicalScrollable)target).Offset = new Vector(0, 5); + + var expected = Enumerable.Range(5, 10).Select(x => $"Item {x}").ToList(); + var items = (ObservableCollection)target.Items; + + Assert.Equal( + expected, + target.Panel.Children.Select(x => x.DataContext)); + + items.Insert(6, "Inserted"); + expected.Insert(1, "Inserted"); + expected.RemoveAt(expected.Count - 1); + + var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + Assert.Equal(expected, actual); + } + public class WithContainers { [Fact] @@ -237,7 +264,9 @@ namespace Avalonia.Controls.UnitTests.Presenters int itemCount = 20) { ItemsPresenter result; - var items = Enumerable.Range(0, itemCount).Select(x => $"Item {x}").ToList(); + + var items = new ObservableCollection( + Enumerable.Range(0, itemCount).Select(x => $"Item {x}")); var scroller = new ScrollContentPresenter {