diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 22d17bf633..47a56fcc82 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -60,6 +60,7 @@ + diff --git a/src/Avalonia.Controls/IVirtualizingController.cs b/src/Avalonia.Controls/IVirtualizingController.cs new file mode 100644 index 0000000000..0b997f4948 --- /dev/null +++ b/src/Avalonia.Controls/IVirtualizingController.cs @@ -0,0 +1,22 @@ +// 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. + +namespace Avalonia.Controls +{ + /// + /// Interface implemented by controls that act as controllers for an + /// . + /// + public interface IVirtualizingController + { + /// + /// Called when the 's controls should be updated. + /// + /// + /// The controller should respond to this method being called by either adding + /// children up until becomes true or + /// removing controls. + /// + void UpdateControls(); + } +} diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs index ce320c0da7..ca75517240 100644 --- a/src/Avalonia.Controls/IVirtualizingPanel.cs +++ b/src/Avalonia.Controls/IVirtualizingPanel.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.Controls.Primitives; namespace Avalonia.Controls { @@ -10,6 +11,17 @@ namespace Avalonia.Controls /// public interface IVirtualizingPanel : IPanel { + /// + /// Gets or sets the controller for the virtualizing panel. + /// + /// + /// A virtualizing controller is responsible for maintaing the controls in the virtualizing + /// panel. This property will be set by the controller when virtualization is initialized. + /// Note that this property may remain null if the panel is added to a control that does + /// not act as a virtualizing controller. + /// + IVirtualizingController Controller { get; set; } + /// /// Gets a value indicating whether the panel is full. /// diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index dd7cf3bf1d..8f77cff32b 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls.Presenters /// /// Base class for classes which handle virtualization for an . /// - internal abstract class ItemVirtualizer + internal abstract class ItemVirtualizer : IVirtualizingController { /// /// Initializes a new instance of the class. @@ -115,34 +115,34 @@ namespace Avalonia.Controls.Presenters { var virtualizingPanel = owner.Panel as IVirtualizingPanel; var scrollable = (ILogicalScrollable)owner; + ItemVirtualizer result = null; if (virtualizingPanel != null && scrollable.InvalidateScroll != null) { switch (owner.VirtualizationMode) { case ItemVirtualizationMode.Simple: - return new ItemVirtualizerSimple(owner); + result = new ItemVirtualizerSimple(owner); + break; } } - return new ItemVirtualizerNone(owner); - } + if (result == null) + { + result = new ItemVirtualizerNone(owner); + } - /// - /// Called by the when it carries out an arrange. - /// - /// The final size passed to the arrange. - public abstract void Arranging(Size finalSize); + if (virtualizingPanel != null) + { + virtualizingPanel.Controller = result; + } - /// - /// Called when a request is made to bring an item into view. - /// - /// The item to bring into view. - /// The rect on the item to bring into view. - /// True if the request was handled; otherwise false. - public virtual bool BringIntoView(IVisual target, Rect targetRect) + return result; + } + + /// + public virtual void UpdateControls() { - return false; } /// @@ -157,5 +157,16 @@ namespace Avalonia.Controls.Presenters Items = items; ItemCount = items.Count(); } + + /// + /// Called when a request is made to bring an item into view. + /// + /// The item to bring into view. + /// The rect on the item to bring into view. + /// True if the request was handled; otherwise false. + public virtual bool BringIntoView(IVisual target, Rect targetRect) + { + return false; + } } } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs index 303c7442c2..7450fdb2b4 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs @@ -52,12 +52,6 @@ namespace Avalonia.Controls.Presenters get { throw new NotSupportedException(); } } - /// - public override void Arranging(Size finalSize) - { - // We don't need to do anything here. - } - /// public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 7750ed7f4a..78ac75e428 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -84,7 +84,7 @@ namespace Avalonia.Controls.Presenters } /// - public override void Arranging(Size finalSize) + public override void UpdateControls() { CreateAndRemoveContainers(); ((ILogicalScrollable)Owner).InvalidateScroll(); diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 817d734506..b442c5bc61 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -78,14 +78,6 @@ namespace Avalonia.Controls.Presenters return _virtualizer?.BringIntoView(target, targetRect) ?? false; } - /// - protected override Size ArrangeOverride(Size finalSize) - { - var result = base.ArrangeOverride(finalSize); - _virtualizer.Arranging(finalSize); - return result; - } - /// protected override void PanelCreated(IPanel panel) { diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 2915ff07d5..77aae2548d 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -4,11 +4,13 @@ using System; using System.Collections.Specialized; using Avalonia.Layout; +using Avalonia.VisualTree; namespace Avalonia.Controls { public class VirtualizingStackPanel : StackPanel, IVirtualizingPanel { + private Size _availableSpace; private double _takenSpace; private int _canBeRemoved; private double _averageItemSize; @@ -20,15 +22,14 @@ namespace Avalonia.Controls get { return Orientation == Orientation.Horizontal ? - _takenSpace >= AvailableSpace.Width : - _takenSpace >= AvailableSpace.Height; + _takenSpace >= _availableSpace.Width : + _takenSpace >= _availableSpace.Height; } } + IVirtualizingController IVirtualizingPanel.Controller { get; set; } int IVirtualizingPanel.OverflowCount => _canBeRemoved; - Orientation IVirtualizingPanel.ScrollDirection => Orientation; - double IVirtualizingPanel.AverageItemSize => _averageItemSize; double IVirtualizingPanel.PixelOverflow @@ -55,16 +56,30 @@ namespace Avalonia.Controls } } - // TODO: We need to put a reasonable limit on this, probably based on the max window size. - private Size AvailableSpace => ((ILayoutable)this).PreviousMeasure ?? Bounds.Size; + private IVirtualizingController Controller => ((IVirtualizingPanel)this).Controller; + + protected override Size MeasureOverride(Size availableSize) + { + if (availableSize != ((ILayoutable)this).PreviousMeasure) + { + // TODO: We need to put a reasonable limit on this, probably based on the max + // window size. + _availableSpace = availableSize; + Controller?.UpdateControls(); + } + + return base.MeasureOverride(availableSize); + } protected override Size ArrangeOverride(Size finalSize) { + _availableSpace = finalSize; _canBeRemoved = 0; _takenSpace = 0; _averageItemSize = 0; _averageCount = 0; var result = base.ArrangeOverride(finalSize); + Controller?.UpdateControls(); return result; } @@ -103,7 +118,7 @@ namespace Avalonia.Controls rect = new Rect(rect.X, rect.Y - _pixelOffset, rect.Width, rect.Height); child.Arrange(rect); - if (rect.Y >= AvailableSpace.Height) + if (rect.Y >= _availableSpace.Height) { ++_canBeRemoved; } @@ -120,7 +135,7 @@ namespace Avalonia.Controls rect = new Rect(rect.X - _pixelOffset, rect.Y, rect.Width, rect.Height); child.Arrange(rect); - if (rect.X >= AvailableSpace.Width) + if (rect.X >= _availableSpace.Width) { ++_canBeRemoved; } @@ -139,7 +154,7 @@ namespace Avalonia.Controls var bounds = Bounds; var gap = Gap; - child.Measure(AvailableSpace); + child.Measure(_availableSpace); ++_averageCount; if (Orientation == Orientation.Vertical) @@ -173,6 +188,11 @@ namespace Avalonia.Controls _takenSpace -= width + gap; RemoveFromAverageItemSize(width); } + + if (_canBeRemoved > 0) + { + --_canBeRemoved; + } } private void AddToAverageItemSize(double value) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index d1443fb2ae..db2bb0f886 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -224,7 +224,7 @@ namespace Avalonia.Controls.UnitTests // and re-arrange everything. foreach (IControl i in target.GetSelfAndVisualDescendents()) { - i.InvalidateArrange(); + i.InvalidateMeasure(); } target.Arrange(new Rect(0, 0, 100, 100)); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 72ec2e6618..ef3297b5c3 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -65,9 +65,11 @@ namespace Avalonia.Controls.UnitTests.Presenters 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); + target.Arrange(new Rect(0, 0, 100, 100)); Assert.Equal(10, target.Panel.Children.Count); } @@ -83,6 +85,20 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(2, target.Panel.Children.Count); } + [Fact] + public void Should_Expand_To_Fit_Containers_When_Flexible_Size() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Size(10, 200), target.DesiredSize); + Assert.Equal(new Size(10, 200), target.Bounds.Size); + Assert.Equal(20, target.Panel.Children.Count); + } + [Fact] public void Initial_Item_DataContexts_Should_Be_Correct() { diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 900f098198..d5217e007b 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -1,6 +1,7 @@ // 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 Moq; using Xunit; namespace Avalonia.Controls.UnitTests @@ -9,25 +10,52 @@ namespace Avalonia.Controls.UnitTests { public class Vertical { + [Fact] + public void Measure_Invokes_Controller_UpdateControls() + { + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + var controller = new Mock(); + + target.Controller = controller.Object; + target.Measure(new Size(100, 100)); + + controller.Verify(x => x.UpdateControls()); + } + + [Fact] + public void Arrange_Invokes_Controller_UpdateControls() + { + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + var controller = new Mock(); + + target.Controller = controller.Object; + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 110, 110)); + + controller.Verify(x => x.UpdateControls()); + } + [Fact] public void Reports_IsFull_False_Until_Measure_Height_Is_Reached() { var target = (IVirtualizingPanel)new VirtualizingStackPanel(); target.Measure(new Size(100, 100)); - target.Arrange(new Rect(target.DesiredSize)); Assert.Equal(new Size(0, 0), target.Bounds.Size); Assert.False(target.IsFull); + Assert.Equal(0, target.OverflowCount); target.Children.Add(new Canvas { Width = 50, Height = 50 }); Assert.False(target.IsFull); + Assert.Equal(0, target.OverflowCount); target.Children.Add(new Canvas { Width = 50, Height = 50 }); Assert.True(target.IsFull); + Assert.Equal(0, target.OverflowCount); } [Fact] - public void Reports_Overflow_Only_After_Arrange() + public void Reports_Overflow_After_Arrange() { var target = (IVirtualizingPanel)new VirtualizingStackPanel();