From 3c59935a4469cae673f5e5694eaa6c6d97e6624c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 31 Aug 2016 23:06:58 +0200 Subject: [PATCH 1/5] Added missing [Fact] attribute. --- .../Presenters/ItemsPresenterTests_Virtualization_Simple.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 02c415d068..2a59227b52 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -658,6 +658,7 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(expected, actual); } + [Fact] public void Should_Add_Containers_For_Items_After_Clear() { var target = CreateTarget(itemCount: 10); From 03eb617d3709a9652586ebbdfc82aa7347d00dd5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 31 Aug 2016 23:12:38 +0200 Subject: [PATCH 2/5] Added failing test for #709. --- .../VirtualizingStackPanelTests.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index b8cd868252..820ce10629 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -118,6 +118,20 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(2, target.PixelOverflow); } + [Fact] + public void Reports_PixelOverflow_After_Arrange_Smaller_Than_Measure() + { + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + + target.Children.Add(new Canvas { Width = 50, Height = 50 }); + target.Children.Add(new Canvas { Width = 50, Height = 52 }); + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 50, 50)); + + Assert.Equal(52, target.PixelOverflow); + } + [Fact] public void Reports_PixelOverflow_With_PixelOffset() { From f6e288f72a0ecdce1f96af5fd2d8fc02540a1d81 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 1 Sep 2016 00:55:06 +0200 Subject: [PATCH 3/5] Update available space in arrange. This fixes #709 but breaks the Should_Add_Containers_For_Items_After_Clear. Fix for that incoming. --- src/Avalonia.Controls/VirtualizingStackPanel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 6daad0b90e..840ed06c73 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -74,6 +74,7 @@ namespace Avalonia.Controls protected override Size ArrangeOverride(Size finalSize) { + _availableSpace = finalSize; _canBeRemoved = 0; _takenSpace = 0; _averageItemSize = 0; From 55d76d6cb9985d356af78937f637fe993890d10f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 1 Sep 2016 00:59:19 +0200 Subject: [PATCH 4/5] 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] From 2ab8941426a5fa96e3ce0a4dc5cc2fc1d15c875a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 7 Sep 2016 21:58:39 +0200 Subject: [PATCH 5/5] Move MaxClientSize to ITopLevelImpl. This allows the maximum size calculation constraint to be applied when an `ItemsPresenter` is in a `Popup`, meaning that all items don't get materialized when the popup is shown. Fixes #719, hopefully. --- src/Avalonia.Controls/Platform/ITopLevelImpl.cs | 5 +++++ src/Avalonia.Controls/Platform/IWindowImpl.cs | 5 ----- src/Avalonia.Controls/Presenters/ItemsPresenter.cs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index f5d8344bda..b06c4b9695 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -22,6 +22,11 @@ namespace Avalonia.Platform /// Size ClientSize { get; set; } + /// + /// Gets the maximum size of a window on the system. + /// + Size MaxClientSize { get; } + /// /// Gets the scaling factor for the window. /// diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 05a51d02aa..609e9834cb 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -11,11 +11,6 @@ namespace Avalonia.Platform /// public interface IWindowImpl : ITopLevelImpl { - /// - /// Gets the maximum size of a window on the system. - /// - Size MaxClientSize { get; } - /// /// Gets or sets the minimized/maximized state of the window. /// diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 2e586a25e3..fa15766e4d 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -99,7 +99,7 @@ namespace Avalonia.Controls.Presenters // the available size. if (availableSize == Size.Infinity && VirtualizationMode != ItemVirtualizationMode.None) { - var window = VisualRoot as Window; + var window = VisualRoot as TopLevel; if (window != null) {