Browse Source

Merge pull request #554 from VitalElement/grokys/item-virtualization-avalonia

WIP Further Implementation of INCC handling
Steven Kirk 9 years ago
parent
commit
96c5578690

+ 3 - 2
samples/VirtualizationTest/MainWindow.xaml

@@ -20,11 +20,12 @@
             <TextBox Watermark="Item to Create"
                      UseFloatingWatermark="True"
                      Text="{Binding NewItemString}"/>
-            <Button Command="{Binding NewItemCommand}">Insert Item</Button>
+            <Button Command="{Binding AddItemCommand}">Add Item</Button>
+            <Button Command="{Binding RemoveItemCommand}">Remove Item</Button>
             <Button Command="{Binding RecreateCommand}">Recreate</Button>
         </StackPanel>
 
-        <ListBox Name="listBox" Items="{Binding Items}">
+        <ListBox Name="listBox" Items="{Binding Items}" SelectedItem="{Binding SelectedItem}">
             <ListBox.ItemTemplate>
                 <DataTemplate>
                     <TextBlock Text="{Binding Header}"/>

+ 44 - 0
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<ItemViewModel> _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<ItemViewModel> Items
         {
             get { return _items; }
             private set { this.RaiseAndSetIfChanged(ref _items, value); }
         }
 
+        public ReactiveCommand<object> AddItemCommand { get; private set; }
+
         public ReactiveCommand<object> RecreateCommand { get; private set; }
 
+        public ReactiveCommand<object> 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";

+ 55 - 6
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;
+
             }
         }
 

+ 81 - 0
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<string>)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<string>)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<string>)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]