Browse Source

More WIP on adding virtualization to ItemsPresenter.

Steven Kirk 9 years ago
parent
commit
38afaf1aca

+ 4 - 10
samples/XamlTestApplicationPcl/Views/MainWindow.cs

@@ -1,6 +1,8 @@
 // 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.Collections.Generic;
+using System.Linq;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Diagnostics;
@@ -26,16 +28,8 @@ namespace XamlTestApplication.Views
             _exitMenu = this.FindControl<MenuItem>("exitMenu");
             _exitMenu.Click += (s, e) => Application.Current.Exit();
 
-            var vadd = this.FindControl<Button>("vadd");
-            var vsp = this.FindControl<VirtualizingStackPanel>("vsp");
-            var ivp = (IVirtualizingPanel)vsp;
-            var index = 0;
-
-            vadd.Click += (s, e) =>
-            {
-                vsp.Children.Add(new TextBlock { Text = "Hello " + ++index });
-                vadd.IsEnabled = !ivp.IsFull;
-            };
+            var virtualList = this.FindControl<ListBox>("virtualList");
+            virtualList.Items = Enumerable.Range(0, 200).Select(x => $"Item {x}").ToList();
         }
     }
 }

+ 1 - 2
samples/XamlTestApplicationPcl/Views/MainWindow.xaml

@@ -29,9 +29,8 @@
       <TabItem Header="Virtualization">
         <DockPanel LastChildFill="True">
           <StackPanel DockPanel.Dock="Right" Orientation="Vertical">
-            <Button Name="vadd">Add Item</Button>
           </StackPanel>
-          <Thingamybob></Thingamybob>
+          <ListBox Name="virtualList"/>
         </DockPanel>
       </TabItem>
       

+ 0 - 2
src/Avalonia.Controls/IVirtualizingPanel.cs

@@ -11,7 +11,5 @@ namespace Avalonia.Controls
         double AverageItemSize { get; }
 
         double PixelOffset { get; set; }
-
-        Action ArrangeCompleted { get; set; }
     }
 }

+ 0 - 1
src/Avalonia.Controls/ItemsControl.cs

@@ -25,7 +25,6 @@ namespace Avalonia.Controls
         /// <summary>
         /// The default value for the <see cref="ItemsPanel"/> property.
         /// </summary>
-        [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1202:ElementsMustBeOrderedByAccess", Justification = "Needs to be before or a NullReferenceException is thrown.")]
         private static readonly FuncTemplate<IPanel> DefaultPanel =
             new FuncTemplate<IPanel>(() => new StackPanel());
 

+ 15 - 0
src/Avalonia.Controls/ListBox.cs

@@ -6,6 +6,7 @@ using System.Collections.Generic;
 using Avalonia.Collections;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 
@@ -16,6 +17,12 @@ namespace Avalonia.Controls
     /// </summary>
     public class ListBox : SelectingItemsControl
     {
+        /// <summary>
+        /// The default value for the <see cref="ItemsControl.ItemsPanel"/> property.
+        /// </summary>
+        private static readonly FuncTemplate<IPanel> DefaultPanel =
+            new FuncTemplate<IPanel>(() => new VirtualizingStackPanel());
+
         /// <summary>
         /// Defines the <see cref="SelectedItems"/> property.
         /// </summary>
@@ -28,6 +35,14 @@ namespace Avalonia.Controls
         public static readonly new AvaloniaProperty<SelectionMode> SelectionModeProperty = 
             SelectingItemsControl.SelectionModeProperty;
 
+        /// <summary>
+        /// Initializes static members of the <see cref="ItemsControl"/> class.
+        /// </summary>
+        static ListBox()
+        {
+            ItemsPanelProperty.OverrideDefaultValue<ListBox>(DefaultPanel);
+        }
+
         /// <inheritdoc/>
         public new IList SelectedItems => base.SelectedItems;
 

+ 1 - 2
src/Avalonia.Controls/Presenters/CarouselPresenter.cs

@@ -95,9 +95,8 @@ namespace Avalonia.Controls.Presenters
         }
 
         /// <inheritdoc/>
-        protected override void CreatePanel()
+        protected override void PanelCreated(IPanel panel)
         {
-            base.CreatePanel();
             var task = MoveToPage(-1, SelectedIndex);
         }
 

+ 89 - 22
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@@ -5,6 +5,7 @@ using System;
 using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Specialized;
+using System.Linq;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Utils;
@@ -55,6 +56,7 @@ namespace Avalonia.Controls.Presenters
         /// <inheritdoc/>
         Action IScrollable.InvalidateScroll { get; set; }
 
+        /// <inheritdoc/>
         Size IScrollable.Extent
         {
             get
@@ -69,39 +71,53 @@ namespace Avalonia.Controls.Presenters
             }
         }
 
+        /// <inheritdoc/>
         Vector IScrollable.Offset { get; set; }
 
+        /// <inheritdoc/>
         Size IScrollable.Viewport
         {
             get
             {
-                throw new NotImplementedException();
+                switch (VirtualizationMode)
+                {
+                    case ItemVirtualizationMode.Simple:
+                        return new Size(0, (_virt.LastIndex - _virt.FirstIndex) + 1);
+                    default:
+                        return default(Size);
+                }
             }
         }
 
-        Size IScrollable.ScrollSize
-        {
-            get
-            {
-                throw new NotImplementedException();
-            }
-        }
+        /// <inheritdoc/>
+        Size IScrollable.ScrollSize => new Size(0, 1);
+
+        /// <inheritdoc/>
+        Size IScrollable.PageScrollSize => new Size(0, 1);
 
-        Size IScrollable.PageScrollSize
+        /// <inheritdoc/>
+        protected override Size ArrangeOverride(Size finalSize)
         {
-            get
+            var result = base.ArrangeOverride(finalSize);
+
+            if (_virt != null)
             {
-                throw new NotImplementedException();
+                CreateRemoveVirtualizedContainers();
+                ((IScrollable)this).InvalidateScroll();
+
             }
+
+            return result;
         }
 
         /// <inheritdoc/>
-        protected override void CreatePanel()
+        protected override void PanelCreated(IPanel panel)
         {
-            base.CreatePanel();
-
-            var virtualizingPanel = Panel as IVirtualizingPanel;
-            _virt = virtualizingPanel != null ? new VirtualizationInfo(virtualizingPanel) : null;
+            if (((IScrollable)this).InvalidateScroll != null)
+            {
+                var virtualizingPanel = Panel as IVirtualizingPanel;
+                _virt = virtualizingPanel != null ? new VirtualizationInfo(virtualizingPanel) : null;
+            }
 
             if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty))
             {
@@ -115,8 +131,19 @@ namespace Avalonia.Controls.Presenters
                 KeyboardNavigation.GetTabNavigation(this));
         }
 
-        /// <inheritdoc/>
         protected override void ItemsChanged(NotifyCollectionChangedEventArgs e)
+        {
+            if (_virt == null)
+            {
+                ItemsChangedNonVirtualized(e);
+            }
+            else
+            {
+                ItemsChangedVirtualized(e);
+            }
+        }
+
+        private void ItemsChangedNonVirtualized(NotifyCollectionChangedEventArgs e)
         {
             var generator = ItemContainerGenerator;
 
@@ -129,7 +156,7 @@ namespace Avalonia.Controls.Presenters
                         generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count);
                     }
 
-                    AddContainers(e.NewStartingIndex, e.NewItems);
+                    AddContainersNonVirtualized(e.NewStartingIndex, e.NewItems);
                     break;
 
                 case NotifyCollectionChangedAction.Remove:
@@ -138,7 +165,7 @@ namespace Avalonia.Controls.Presenters
 
                 case NotifyCollectionChangedAction.Replace:
                     RemoveContainers(generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count));
-                    var containers = AddContainers(e.NewStartingIndex, e.NewItems);
+                    var containers = AddContainersNonVirtualized(e.NewStartingIndex, e.NewItems);
 
                     var i = e.NewStartingIndex;
 
@@ -156,7 +183,7 @@ namespace Avalonia.Controls.Presenters
 
                     if (Items != null)
                     {
-                        AddContainers(0, Items);
+                        AddContainersNonVirtualized(0, Items);
                     }
 
                     break;
@@ -165,7 +192,11 @@ namespace Avalonia.Controls.Presenters
             InvalidateMeasure();
         }
 
-        private IList<ItemContainerInfo> AddContainers(int index, IEnumerable items)
+        private void ItemsChangedVirtualized(NotifyCollectionChangedEventArgs e)
+        {
+        }
+
+        private IList<ItemContainerInfo> AddContainersNonVirtualized(int index, IEnumerable items)
         {
             var generator = ItemContainerGenerator;
             var result = new List<ItemContainerInfo>();
@@ -193,6 +224,42 @@ namespace Avalonia.Controls.Presenters
             return result;
         }
 
+        private void CreateRemoveVirtualizedContainers()
+        {
+            var generator = ItemContainerGenerator;
+            var panel = _virt.Panel;
+
+            if (!panel.IsFull)
+            {
+                var index = _virt.LastIndex + 1;
+                var items = Items.Cast<object>().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 remove = panel.OverflowCount;
+
+                panel.Children.RemoveRange(
+                    panel.Children.Count - remove,
+                    panel.OverflowCount);
+                _virt.LastIndex -= remove;
+            }
+        }
+
         private void RemoveContainers(IEnumerable<ItemContainerInfo> items)
         {
             foreach (var i in items)
@@ -213,7 +280,7 @@ namespace Avalonia.Controls.Presenters
 
             public IVirtualizingPanel Panel { get; }
             public int FirstIndex { get; set; }
-            public int LastIndex { get; set; }
+            public int LastIndex { get; set; } = -1;
         }
     }
 }

+ 25 - 9
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@@ -102,7 +102,13 @@ namespace Avalonia.Controls.Presenters
                 if (_generator == null)
                 {
                     var i = TemplatedParent as ItemsControl;
-                    _generator = (i?.ItemContainerGenerator) ?? new ItemContainerGenerator(this);
+                    _generator = i?.ItemContainerGenerator;
+
+                    if (_generator == null)
+                    {
+                        _generator = new ItemContainerGenerator(this);
+                        _generator.ItemTemplate = ItemTemplate;
+                    }
                 }
 
                 return _generator;
@@ -178,11 +184,26 @@ namespace Avalonia.Controls.Presenters
             return finalSize;
         }
 
+        /// <summary>
+        /// Called when the <see cref="Panel"/> is created.
+        /// </summary>
+        /// <param name="panel">The panel.</param>
+        protected virtual void PanelCreated(IPanel panel)
+        {
+        }
+
+        /// <summary>
+        /// Called when the items for the presenter change, either because <see cref="Items"/>
+        /// has been set, the items collection has been modified, or the panel has been created.
+        /// </summary>
+        /// <param name="e">A description of the change.</param>
+        protected abstract void ItemsChanged(NotifyCollectionChangedEventArgs e);
+
         /// <summary>
         /// Creates the <see cref="Panel"/> when <see cref="ApplyTemplate"/> is called for the first
         /// time.
         /// </summary>
-        protected virtual void CreatePanel()
+        private void CreatePanel()
         {
             Panel = ItemsPanel.Build();
             Panel.SetValue(TemplatedParentProperty, TemplatedParent);
@@ -201,16 +222,11 @@ namespace Avalonia.Controls.Presenters
                 incc.CollectionChanged += ItemsCollectionChanged;
             }
 
+            PanelCreated(Panel);
+
             ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
         }
 
-        /// <summary>
-        /// Called when the items for the presenter change, either because <see cref="Items"/>
-        /// has been set, the items collection has been modified, or the panel has been created.
-        /// </summary>
-        /// <param name="e">A description of the change.</param>
-        protected abstract void ItemsChanged(NotifyCollectionChangedEventArgs e);
-
         /// <summary>
         /// Called when the <see cref="Items"/> collection changes.
         /// </summary>

+ 1 - 0
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@@ -213,6 +213,7 @@ namespace Avalonia.Controls.Presenters
             else if (child != null)
             {
                 child.Arrange(new Rect(finalSize));
+                return finalSize;
             }
 
             return new Size();

+ 0 - 1
src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs

@@ -15,7 +15,6 @@ namespace Avalonia.Controls.Presenters
             if (_panel == null)
             {
                 _panel = new VirtualizingStackPanel();
-                _panel.ArrangeCompleted = CheckPanel;
                 Child = _panel;
                 CheckPanel();
             }

+ 0 - 3
src/Avalonia.Controls/VirtualizingStackPanel.cs

@@ -42,8 +42,6 @@ namespace Avalonia.Controls
             }
         }
 
-        Action IVirtualizingPanel.ArrangeCompleted { get; set; }
-
         protected override Size ArrangeOverride(Size finalSize)
         {
             _canBeRemoved = 0;
@@ -51,7 +49,6 @@ namespace Avalonia.Controls
             _averageItemSize = 0;
             _averageCount = 0;
             var result = base.ArrangeOverride(finalSize);
-            ((IVirtualizingPanel)this).ArrangeCompleted?.Invoke();
             return result;
         }
 

+ 124 - 25
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs

@@ -1,6 +1,8 @@
 // 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.Collections.Generic;
+using System.Linq;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
@@ -14,9 +16,8 @@ namespace Avalonia.Controls.UnitTests.Presenters
         [Fact]
         public void Should_Return_IsLogicalScrollEnabled_False_When_Has_No_Virtualizing_Panel()
         {
-            var target = new ItemsPresenter
-            {
-            };
+            var target = CreateTarget();
+            target.ClearValue(ItemsPresenter.ItemsPanelProperty);
 
             target.ApplyTemplate();
 
@@ -26,11 +27,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
         [Fact]
         public void Should_Return_IsLogicalScrollEnabled_False_When_VirtualizationMode_None()
         {
-            var target = new ItemsPresenter
-            {
-                ItemsPanel = VirtualizingPanelTemplate(),
-                VirtualizationMode = ItemVirtualizationMode.None,
-            };
+            var target = CreateTarget(ItemVirtualizationMode.None);
 
             target.ApplyTemplate();
 
@@ -38,45 +35,108 @@ namespace Avalonia.Controls.UnitTests.Presenters
         }
 
         [Fact]
-        public void Should_Return_IsLogicalScrollEnabled_True_When_Has_Virtualizing_Panel()
+        public void Should_Return_IsLogicalScrollEnabled_False_When_Doesnt_Have_ScrollPresenter_Parent()
         {
             var target = new ItemsPresenter
             {
                 ItemsPanel = VirtualizingPanelTemplate(),
+                ItemTemplate = ItemTemplate(),
+                VirtualizationMode = ItemVirtualizationMode.Simple,
             };
 
             target.ApplyTemplate();
 
+            Assert.False(((IScrollable)target).IsLogicalScrollEnabled);
+        }
+
+        [Fact]
+        public void Should_Return_IsLogicalScrollEnabled_True()
+        {
+            var target = CreateTarget();
+
+            target.ApplyTemplate();
+
             Assert.True(((IScrollable)target).IsLogicalScrollEnabled);
         }
 
+        [Fact]
+        public void Should_Fill_Panel_With_Containers()
+        {
+            var target = CreateTarget();
+
+            target.ApplyTemplate();
+            target.Measure(new Size(100, 100));
+            target.Arrange(new Rect(0, 0, 100, 100));
+
+            Assert.Equal(10, target.Panel.Children.Count);
+        }
+
+        [Fact]
+        public void Should_Only_Create_Enough_Containers_To_Display_All_Items()
+        {
+            var target = CreateTarget(itemCount: 2);
+
+            target.ApplyTemplate();
+            target.Measure(new Size(100, 100));
+            target.Arrange(new Rect(0, 0, 100, 100));
+
+            Assert.Equal(2, target.Panel.Children.Count);
+        }
+
+        [Fact]
+        public void Initial_Item_DataContexts_Should_Be_Correct()
+        {
+            var target = CreateTarget();
+            var items = (IList<string>)target.Items;
+
+            target.ApplyTemplate();
+            target.Measure(new Size(100, 100));
+            target.Arrange(new Rect(0, 0, 100, 100));
+
+            for (var i = 0; i < target.Panel.Children.Count; ++i)
+            {
+                Assert.Equal(items[i], target.Panel.Children[i].DataContext);
+            }
+        }
+
+        [Fact]
+        public void Should_Add_New_Items_When_Control_Is_Enlarged()
+        {
+            var target = CreateTarget();
+            var items = (IList<string>)target.Items;
+
+            target.ApplyTemplate();
+            target.Measure(new Size(100, 100));
+            target.Arrange(new Rect(0, 0, 100, 100));
+
+            Assert.Equal(10, target.Panel.Children.Count);
+
+            target.Arrange(new Rect(0, 0, 100, 120));
+
+            Assert.Equal(12, target.Panel.Children.Count);
+
+            for (var i = 0; i < target.Panel.Children.Count; ++i)
+            {
+                Assert.Equal(items[i], target.Panel.Children[i].DataContext);
+            }
+        }
+
         public class Simple
         {
             [Fact]
             public void Should_Return_Items_Count_For_Extent()
             {
-                var target = new ItemsPresenter
-                {
-                    Items = new string[10],
-                    ItemsPanel = VirtualizingPanelTemplate(),
-                    VirtualizationMode = ItemVirtualizationMode.Simple,
-                };
+                var target = CreateTarget();
 
                 target.ApplyTemplate();
 
-                Assert.Equal(new Size(0, 10), ((IScrollable)target).Extent);
+                Assert.Equal(new Size(0, 20), ((IScrollable)target).Extent);
             }
 
             [Fact]
             public void Should_Have_Number_Of_Visible_Items_As_Viewport()
             {
-                var target = new ItemsPresenter
-                {
-                    Items = new string[20],
-                    ItemsPanel = VirtualizingPanelTemplate(),
-                    ItemTemplate = ItemTemplate(),
-                    VirtualizationMode = ItemVirtualizationMode.Simple,
-                };
+                var target = CreateTarget();
 
                 target.ApplyTemplate();
                 target.Measure(new Size(100, 100));
@@ -84,13 +144,52 @@ namespace Avalonia.Controls.UnitTests.Presenters
 
                 Assert.Equal(10, ((IScrollable)target).Viewport.Height);
             }
+
+            [Fact]
+            public void Should_Remove_Items_When_Control_Is_Shrank()
+            {
+                var target = CreateTarget();
+                var items = (IList<string>)target.Items;
+
+                target.ApplyTemplate();
+                target.Measure(new Size(100, 100));
+                target.Arrange(new Rect(0, 0, 100, 100));
+
+                Assert.Equal(10, target.Panel.Children.Count);
+
+                target.Arrange(new Rect(0, 0, 100, 80));
+
+                Assert.Equal(8, target.Panel.Children.Count);
+            }
+        }
+
+        private static ItemsPresenter CreateTarget(
+            ItemVirtualizationMode mode = ItemVirtualizationMode.Simple,
+            int itemCount = 20)
+        {
+            ItemsPresenter result;
+            var items = Enumerable.Range(0, itemCount).Select(x => $"Item {x}").ToList();
+
+            var scroller = new ScrollContentPresenter
+            {
+                Content = result = new ItemsPresenter
+                {
+                    Items = items,
+                    ItemsPanel = VirtualizingPanelTemplate(),
+                    ItemTemplate = ItemTemplate(),
+                    VirtualizationMode = mode,
+                }
+            };
+
+            scroller.UpdateChild();
+
+            return result;
         }
 
         private static IDataTemplate ItemTemplate()
         {
-            return new FuncDataTemplate<string>(x => new TextBlock
+            return new FuncDataTemplate<string>(x => new Canvas
             {
-                Text = x,
                 Height = 10,
             });
         }