From 55d76d6cb9985d356af78937f637fe993890d10f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 1 Sep 2016 00:59:19 +0200 Subject: [PATCH] Make sure panel is remeasured after add/remove. After items are added or removed then the virtualizing panel must be remeasured. It's usually not remeasured if the available size is the same as on the last measure as that causes an infinite loop; this adds a `ForceInvalidateMeasure` to `IVirtualizingPanel` which will force a remeasure even if the available size hasn't changed. Also adds tests for the behavior. --- src/Avalonia.Controls/IVirtualizingPanel.cs | 19 +++++++- .../Presenters/ItemVirtualizerSimple.cs | 3 ++ .../VirtualizingStackPanel.cs | 10 +++- .../VirtualizingStackPanelTests.cs | 48 ++++++++++++++++++- 4 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs index 5d35fa1ec8..792dee8ae8 100644 --- a/src/Avalonia.Controls/IVirtualizingPanel.cs +++ b/src/Avalonia.Controls/IVirtualizingPanel.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 Avalonia.Layout; + namespace Avalonia.Controls { /// @@ -24,8 +26,8 @@ namespace Avalonia.Controls /// /// /// This property should return false until enough children are added to fill the space - /// passed into the last measure in the direction of scroll. It should be updated - /// immediately after a child is added or removed. + /// passed into the last measure or arrange in the direction of scroll. It should be + /// updated immediately after a child is added or removed. /// bool IsFull { get; } @@ -63,5 +65,18 @@ namespace Avalonia.Controls /// Gets or sets the current pixel offset of the items in the direction of scroll. /// double PixelOffset { get; set; } + + /// + /// Invalidates the measure of the control and forces a call to + /// on the next measure. + /// + /// + /// The implementation for this method should call + /// and also ensure that the next call to + /// calls + /// on the next measure even if + /// the available size hasn't changed. + /// + void ForceInvalidateMeasure(); } } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 83a0313dd5..d2e5a720b7 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -121,6 +121,7 @@ namespace Avalonia.Controls.Presenters RecycleContainers(); } + panel.ForceInvalidateMeasure(); break; case NotifyCollectionChangedAction.Remove: @@ -130,6 +131,7 @@ namespace Avalonia.Controls.Presenters RecycleContainersOnRemove(); } + panel.ForceInvalidateMeasure(); break; case NotifyCollectionChangedAction.Move: @@ -140,6 +142,7 @@ namespace Avalonia.Controls.Presenters case NotifyCollectionChangedAction.Reset: RecycleContainersOnRemove(); CreateAndRemoveContainers(); + panel.ForceInvalidateMeasure(); break; } } diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 840ed06c73..2e5afaf170 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -19,6 +19,7 @@ namespace Avalonia.Controls private double _averageItemSize; private int _averageCount; private double _pixelOffset; + private bool _forceRemeasure; bool IVirtualizingPanel.IsFull { @@ -61,10 +62,17 @@ namespace Avalonia.Controls private IVirtualizingController Controller => ((IVirtualizingPanel)this).Controller; + void IVirtualizingPanel.ForceInvalidateMeasure() + { + InvalidateMeasure(); + _forceRemeasure = true; + } + protected override Size MeasureOverride(Size availableSize) { - if (availableSize != ((ILayoutable)this).PreviousMeasure) + if (_forceRemeasure || availableSize != ((ILayoutable)this).PreviousMeasure) { + _forceRemeasure = false; _availableSpace = availableSize; Controller?.UpdateControls(); } diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 820ce10629..b0ae3df8a2 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -22,7 +22,51 @@ namespace Avalonia.Controls.UnitTests target.Controller = controller.Object; target.Measure(new Size(100, 100)); - controller.Verify(x => x.UpdateControls()); + controller.Verify(x => x.UpdateControls(), Times.Once()); + } + + [Fact] + public void Measure_Invokes_Controller_UpdateControls_If_AvailableSize_Changes() + { + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + var controller = new Mock(); + + target.Controller = controller.Object; + target.Measure(new Size(100, 100)); + target.InvalidateMeasure(); + target.Measure(new Size(100, 100)); + target.InvalidateMeasure(); + target.Measure(new Size(100, 101)); + + controller.Verify(x => x.UpdateControls(), Times.Exactly(2)); + } + + [Fact] + public void Measure_Does_Not_Invoke_Controller_UpdateControls_If_AvailableSize_Is_The_Same() + { + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + var controller = new Mock(); + + target.Controller = controller.Object; + target.Measure(new Size(100, 100)); + target.InvalidateMeasure(); + target.Measure(new Size(100, 100)); + + controller.Verify(x => x.UpdateControls(), Times.Once()); + } + + [Fact] + public void Measure_Invokes_Controller_UpdateControls_If_AvailableSize_Is_The_Same_After_ForceInvalidateMeasure() + { + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + var controller = new Mock(); + + target.Controller = controller.Object; + target.Measure(new Size(100, 100)); + target.ForceInvalidateMeasure(); + target.Measure(new Size(100, 100)); + + controller.Verify(x => x.UpdateControls(), Times.Exactly(2)); } [Fact] @@ -35,7 +79,7 @@ namespace Avalonia.Controls.UnitTests target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 110, 110)); - controller.Verify(x => x.UpdateControls()); + controller.Verify(x => x.UpdateControls(), Times.Exactly(2)); } [Fact]