diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationTest/MainWindow.xaml index a6fd6918ef..aa882c052a 100644 --- a/samples/VirtualizationTest/MainWindow.xaml +++ b/samples/VirtualizationTest/MainWindow.xaml @@ -20,11 +20,12 @@ - + + - + diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs index 9b0565401a..953f533065 100644 --- a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs @@ -10,7 +10,9 @@ namespace VirtualizationTest.ViewModels internal class MainWindowViewModel : ReactiveObject { private int _itemCount = 200; + private string _newItemString; private IReactiveList _items; + private ItemViewModel _selectedItem; private string _prefix = "Item"; public MainWindowViewModel() @@ -18,6 +20,18 @@ namespace VirtualizationTest.ViewModels this.WhenAnyValue(x => x.ItemCount).Subscribe(ResizeItems); RecreateCommand = ReactiveCommand.Create(); RecreateCommand.Subscribe(_ => Recreate()); + + AddItemCommand = ReactiveCommand.Create(); + AddItemCommand.Subscribe(_ => AddItem()); + + RemoveItemCommand = ReactiveCommand.Create(); + RemoveItemCommand.Subscribe(_ => Remove()); + } + + public string NewItemString + { + get { return _newItemString; } + set { this.RaiseAndSetIfChanged(ref _newItemString, value); } } public int ItemCount @@ -26,14 +40,24 @@ namespace VirtualizationTest.ViewModels set { this.RaiseAndSetIfChanged(ref _itemCount, value); } } + public ItemViewModel SelectedItem + { + get { return _selectedItem; } + set { this.RaiseAndSetIfChanged(ref _selectedItem, value); } + } + public IReactiveList Items { get { return _items; } private set { this.RaiseAndSetIfChanged(ref _items, value); } } + public ReactiveCommand AddItemCommand { get; private set; } + public ReactiveCommand RecreateCommand { get; private set; } + public ReactiveCommand RemoveItemCommand { get; private set; } + private void ResizeItems(int count) { if (Items == null) @@ -54,6 +78,26 @@ namespace VirtualizationTest.ViewModels } } + private void AddItem() + { + var index = Items.Count; + + if (SelectedItem != null) + { + index = Items.IndexOf(SelectedItem) + 1; + } + + Items.Insert(index, new ItemViewModel(index, NewItemString)); + } + + private void Remove() + { + if (SelectedItem != null) + { + Items.Remove(SelectedItem); + } + } + private void Recreate() { _prefix = _prefix == "Item" ? "Recreated" : "Item"; diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 1ff85c50ac..5f32f5d10c 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -42,7 +42,7 @@ namespace Avalonia.Controls.Presenters if (panel.PixelOffset > 0) { panel.PixelOffset = 0; - delta += 1; + delta += 1; } if (delta != 0) @@ -87,6 +87,47 @@ namespace Avalonia.Controls.Presenters switch (e.Action) { + case NotifyCollectionChangedAction.Remove: + if(e.OldStartingIndex >= FirstIndex && + e.OldStartingIndex + e.OldItems.Count <= NextIndex) + { + if (e.OldStartingIndex == FirstIndex) + { + // We are removing the first in the list. + VirtualizingPanel.Children.RemoveAt(e.OldStartingIndex - FirstIndex); + Owner.ItemContainerGenerator.Dematerialize(e.OldStartingIndex - FirstIndex, 1); + FirstIndex++; // This may not be necessary, but cant get to work without this. + + // If all items are visible we need to reduce the NextIndex too. + if(NextIndex > ItemCount) + { + NextIndex = ItemCount; + } + + CreateRemoveContainers(); + RecycleContainers(); + } + else if (e.OldStartingIndex + e.OldItems.Count == NextIndex) + { + // We are removing the last one in the list. + VirtualizingPanel.Children.RemoveAt(e.OldStartingIndex - FirstIndex); + Owner.ItemContainerGenerator.Dematerialize(e.OldStartingIndex - FirstIndex, 1); + NextIndex--; + } + else + { + // If all items are visible we need to reduce the NextIndex too. + if (NextIndex > ItemCount) + { + NextIndex = ItemCount; + } + + CreateRemoveContainers(); + RecycleContainers(); + } + } + break; + case NotifyCollectionChangedAction.Add: if (e.NewStartingIndex >= FirstIndex && e.NewStartingIndex + e.NewItems.Count < NextIndex) @@ -183,17 +224,25 @@ namespace Avalonia.Controls.Presenters foreach (var container in containers) { - var item = Items.ElementAt(itemIndex); - - if (!object.Equals(container.Item, item)) + if (itemIndex < ItemCount) { - if (!generator.TryRecycle(itemIndex, itemIndex, item, selector)) + var item = Items.ElementAt(itemIndex); + + if (!object.Equals(container.Item, item)) { - throw new NotImplementedException(); + if (!generator.TryRecycle(itemIndex, itemIndex, item, selector)) + { + throw new NotImplementedException(); + } } } + else + { + panel.Children.RemoveAt(panel.Children.Count - 1); + } ++itemIndex; + } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 036ecdce6f..36d43cd6fe 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -181,6 +181,87 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(expected, actual); } + [Fact] + public void Removing_First_Item_When_Visible_Should_UpdateContainers() + { + var target = CreateTarget(itemCount: 20); + + target.ApplyTemplate(); + target.Measure(new Size(100, 195)); + target.Arrange(new Rect(0, 0, 100, 195)); + + ((ILogicalScrollable)target).Offset = new Vector(0, 5); + + var expected = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(); + var items = (ObservableCollection)target.Items; + + Assert.Equal( + expected, + target.Panel.Children.Select(x => x.DataContext)); + + items.Remove(items.First()); + expected.Remove(expected.First()); + + var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + Assert.Equal(expected, actual); + } + + [Fact] + public void Removing_Items_From_Middle_Should_Update_Containers() + { + var target = CreateTarget(itemCount: 20); + + target.ApplyTemplate(); + target.Measure(new Size(100, 195)); + target.Arrange(new Rect(0, 0, 100, 195)); + + ((ILogicalScrollable)target).Offset = new Vector(0, 5); + + var expected = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(); + var items = (ObservableCollection)target.Items; + + Assert.Equal( + expected, + target.Panel.Children.Select(x => x.DataContext)); + + items.RemoveAt(2); + expected.RemoveAt(2); + + var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + Assert.Equal(expected, actual); + + items.RemoveAt(items.Count - 2); + expected.RemoveAt(expected.Count -2); + + actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + Assert.Equal(expected, actual); + } + + [Fact] + public void Removing_Last_Item_When_Visible_Should_UpdateContainers() + { + var target = CreateTarget(itemCount: 20); + + target.ApplyTemplate(); + target.Measure(new Size(100, 195)); + target.Arrange(new Rect(0, 0, 100, 195)); + + ((ILogicalScrollable)target).Offset = new Vector(0, 5); + + var expected = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(); + var items = (ObservableCollection)target.Items; + + Assert.Equal( + expected, + target.Panel.Children.Select(x => x.DataContext)); + + items.Remove(items.Last()); + expected.Remove(expected.Last()); + + var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + Assert.Equal(expected, actual); + } + public class WithContainers { [Fact]