From 684020ae2d7d6d366bf9e5ff0e91e3a8b1f2a6a9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 10 Jun 2017 15:43:04 +0200 Subject: [PATCH 1/7] Updated benchmarks - Update BenchmarkDotNet - Added measure benchmark - Add memory diagnoser --- .../Avalonia.Benchmarks.csproj | 3 +- tests/Avalonia.Benchmarks/Layout/Measure.cs | 65 +++++++++++++++++++ .../Styling/ApplyStyling.cs | 1 + 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 tests/Avalonia.Benchmarks/Layout/Measure.cs diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 1f5ebac203..21d7b186b4 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -49,6 +49,7 @@ + @@ -100,7 +101,7 @@ - + \ No newline at end of file diff --git a/tests/Avalonia.Benchmarks/Layout/Measure.cs b/tests/Avalonia.Benchmarks/Layout/Measure.cs new file mode 100644 index 0000000000..d1fdae9971 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Layout/Measure.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.UnitTests; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Layout +{ + [MemoryDiagnoser] + public class Measure : IDisposable + { + private IDisposable _app; + private TestRoot root; + private List controls = new List(); + + public Measure() + { + _app = UnitTestApplication.Start(TestServices.RealLayoutManager); + + var panel = new StackPanel(); + root = new TestRoot { Child = panel }; + controls.Add(panel); + CreateChildren(panel, 3, 5); + LayoutManager.Instance.ExecuteInitialLayoutPass(root); + } + + public void Dispose() + { + _app.Dispose(); + } + + [Benchmark] + public void Remeasure_Half() + { + var random = new Random(1); + + foreach (var control in controls) + { + if (random.Next(2) == 0) + { + control.InvalidateMeasure(); + } + } + + LayoutManager.Instance.ExecuteLayoutPass(); + } + + private void CreateChildren(IPanel parent, int childCount, int iterations) + { + for (var i = 0; i < childCount; ++i) + { + var control = new StackPanel(); + parent.Children.Add(control); + + if (iterations > 0) + { + CreateChildren(control, childCount, iterations - 1); + } + + controls.Add(control); + } + } + } +} diff --git a/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs b/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs index 0af451efd2..33af55fdf9 100644 --- a/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs +++ b/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs @@ -11,6 +11,7 @@ using Avalonia.VisualTree; namespace Avalonia.Benchmarks.Styling { + [MemoryDiagnoser] public class ApplyStyling : IDisposable { private IDisposable _app; From 309c9f7a4b9cc6d8ad376edc7c7e68e9f8716287 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 8 Jun 2017 00:40:11 +0200 Subject: [PATCH 2/7] Added some LayoutManager tests. Some passing, some failing. --- .../LayoutManagerTests.cs | 241 +++++++++++++++++- .../LayoutTestControl.cs | 29 +++ .../LayoutTestRoot.cs | 43 ++++ .../TestLayoutRoot.cs | 24 -- tests/Avalonia.UnitTests/TestRoot.cs | 2 +- 5 files changed, 307 insertions(+), 32 deletions(-) create mode 100644 tests/Avalonia.Layout.UnitTests/LayoutTestControl.cs create mode 100644 tests/Avalonia.Layout.UnitTests/LayoutTestRoot.cs delete mode 100644 tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index f67c5a353f..45e8803f16 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -2,25 +2,245 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Avalonia.Controls; +using Avalonia.UnitTests; +using System; using Xunit; +using System.Collections.Generic; namespace Avalonia.Layout.UnitTests { public class LayoutManagerTests { [Fact] - public void Invalidating_Child_Should_Remeasure_Parent() + public void Measures_And_Arranges_InvalidateMeasured_Control() { - var layoutManager = new LayoutManager(); + var target = new LayoutManager(); - using (AvaloniaLocator.EnterScope()) + using (Start(target)) { - AvaloniaLocator.CurrentMutable.Bind().ToConstant(layoutManager); + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + target.ExecuteInitialLayoutPass(root); + control.Measured = control.Arranged = false; + + control.InvalidateMeasure(); + target.ExecuteLayoutPass(); + + Assert.True(control.Measured); + Assert.True(control.Arranged); + } + } + + [Fact] + public void Arranges_InvalidateArranged_Control() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + target.ExecuteInitialLayoutPass(root); + control.Measured = control.Arranged = false; + + control.InvalidateArrange(); + target.ExecuteLayoutPass(); + + Assert.False(control.Measured); + Assert.True(control.Arranged); + } + } + + [Fact] + public void Measures_Parent_Of_Newly_Added_Control() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot(); + + target.ExecuteInitialLayoutPass(root); + root.Child = control; + root.Measured = root.Arranged = false; + + target.ExecuteLayoutPass(); + + Assert.True(root.Measured); + Assert.True(root.Arranged); + Assert.True(control.Measured); + Assert.True(control.Arranged); + } + } + + [Fact] + public void Measures_In_Correct_Order() + { + var target = new LayoutManager(); + + using (Start(target)) + { + LayoutTestControl control1; + LayoutTestControl control2; + var root = new LayoutTestRoot + { + Child = control1 = new LayoutTestControl + { + Child = control2 = new LayoutTestControl(), + } + }; + + + var order = new List(); + Size MeasureOverride(ILayoutable control, Size size) + { + order.Add(control); + return new Size(10, 10); + } + + root.DoMeasureOverride = MeasureOverride; + control1.DoMeasureOverride = MeasureOverride; + control2.DoMeasureOverride = MeasureOverride; + target.ExecuteInitialLayoutPass(root); + + control2.InvalidateMeasure(); + control1.InvalidateMeasure(); + root.InvalidateMeasure(); + + order.Clear(); + target.ExecuteLayoutPass(); + + Assert.Equal(new ILayoutable[] { root, control1, control2 }, order); + } + } + + [Fact] + public void Measures_Root_And_Grandparent_In_Correct_Order() + { + var target = new LayoutManager(); + + using (Start(target)) + { + LayoutTestControl control1; + LayoutTestControl control2; + var root = new LayoutTestRoot + { + Child = control1 = new LayoutTestControl + { + Child = control2 = new LayoutTestControl(), + } + }; + + + var order = new List(); + Size MeasureOverride(ILayoutable control, Size size) + { + order.Add(control); + return new Size(10, 10); + } + + root.DoMeasureOverride = MeasureOverride; + control1.DoMeasureOverride = MeasureOverride; + control2.DoMeasureOverride = MeasureOverride; + target.ExecuteInitialLayoutPass(root); + + control2.InvalidateMeasure(); + root.InvalidateMeasure(); + + order.Clear(); + target.ExecuteLayoutPass(); + + Assert.Equal(new ILayoutable[] { root, control2 }, order); + } + } + + [Fact] + public void Doesnt_Measure_Non_Invalidated_Root() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + target.ExecuteInitialLayoutPass(root); + root.Measured = root.Arranged = false; + control.Measured = control.Arranged = false; + + control.InvalidateMeasure(); + target.ExecuteLayoutPass(); + + Assert.False(root.Measured); + Assert.False(root.Arranged); + Assert.True(control.Measured); + Assert.True(control.Arranged); + } + } + + [Fact] + public void Doesnt_Measure_Removed_Control() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + target.ExecuteInitialLayoutPass(root); + control.Measured = control.Arranged = false; + + control.InvalidateMeasure(); + root.Child = null; + target.ExecuteLayoutPass(); + + Assert.False(control.Measured); + Assert.False(control.Arranged); + } + } + + [Fact] + public void Measures_Root_With_Infinity() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var root = new LayoutTestRoot(); + var availableSize = default(Size); + + // Should not measure with this size. + root.MaxClientSize = new Size(123, 456); + + root.DoMeasureOverride = (_, s) => + { + availableSize = s; + return new Size(100, 100); + }; + + target.ExecuteInitialLayoutPass(root); + + Assert.Equal(Size.Infinity, availableSize); + } + } + + [Fact] + public void Invalidating_Child_Remeasures_Parent() + { + var target = new LayoutManager(); + + using (Start(target)) + { + AvaloniaLocator.CurrentMutable.Bind().ToConstant(target); Border border; StackPanel panel; - var root = new TestLayoutRoot + var root = new LayoutTestRoot { Child = panel = new StackPanel { @@ -31,15 +251,22 @@ namespace Avalonia.Layout.UnitTests } }; - layoutManager.ExecuteInitialLayoutPass(root); + target.ExecuteInitialLayoutPass(root); Assert.Equal(new Size(0, 0), root.DesiredSize); border.Width = 100; border.Height = 100; - layoutManager.ExecuteLayoutPass(); + target.ExecuteLayoutPass(); Assert.Equal(new Size(100, 100), panel.DesiredSize); } } + + private IDisposable Start(LayoutManager layoutManager) + { + var result = AvaloniaLocator.EnterScope(); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(layoutManager); + return result; + } } } diff --git a/tests/Avalonia.Layout.UnitTests/LayoutTestControl.cs b/tests/Avalonia.Layout.UnitTests/LayoutTestControl.cs new file mode 100644 index 0000000000..f7f072eb1e --- /dev/null +++ b/tests/Avalonia.Layout.UnitTests/LayoutTestControl.cs @@ -0,0 +1,29 @@ +using System; +using Avalonia.Controls; + +namespace Avalonia.Layout.UnitTests +{ + internal class LayoutTestControl : Decorator + { + public bool Measured { get; set; } + public bool Arranged { get; set; } + public Func DoMeasureOverride { get; set; } + public Func DoArrangeOverride { get; set; } + + protected override Size MeasureOverride(Size availableSize) + { + Measured = true; + return DoMeasureOverride != null ? + DoMeasureOverride(this, availableSize) : + base.MeasureOverride(availableSize); + } + + protected override Size ArrangeOverride(Size finalSize) + { + Arranged = true; + return DoArrangeOverride != null ? + DoArrangeOverride(this, finalSize) : + base.ArrangeOverride(finalSize); + } + } +} diff --git a/tests/Avalonia.Layout.UnitTests/LayoutTestRoot.cs b/tests/Avalonia.Layout.UnitTests/LayoutTestRoot.cs new file mode 100644 index 0000000000..07476844e0 --- /dev/null +++ b/tests/Avalonia.Layout.UnitTests/LayoutTestRoot.cs @@ -0,0 +1,43 @@ +// 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; +using Avalonia.UnitTests; + +namespace Avalonia.Layout.UnitTests +{ + internal class LayoutTestRoot : TestRoot, ILayoutable + { + public bool Measured { get; set; } + public bool Arranged { get; set; } + public Func DoMeasureOverride { get; set; } + public Func DoArrangeOverride { get; set; } + + void ILayoutable.Measure(Size availableSize) + { + Measured = true; + Measure(availableSize); + } + + void ILayoutable.Arrange(Rect rect) + { + Arranged = true; + Arrange(rect); + } + + protected override Size MeasureOverride(Size availableSize) + { + return DoMeasureOverride != null ? + DoMeasureOverride(this, availableSize) : + base.MeasureOverride(availableSize); + } + + protected override Size ArrangeOverride(Size finalSize) + { + Arranged = true; + return DoArrangeOverride != null ? + DoArrangeOverride(this, finalSize) : + base.ArrangeOverride(finalSize); + } + } +} diff --git a/tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs b/tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs deleted file mode 100644 index fab1647c5d..0000000000 --- a/tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs +++ /dev/null @@ -1,24 +0,0 @@ -// 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.Controls; - -namespace Avalonia.Layout.UnitTests -{ - internal class TestLayoutRoot : Decorator, ILayoutRoot - { - public TestLayoutRoot() - { - ClientSize = new Size(500, 500); - } - - public Size ClientSize - { - get; - set; - } - - public Size MaxClientSize => Size.Infinity; - public double LayoutScaling => 1; - } -} diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 8a711c415e..399870aef9 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -43,7 +43,7 @@ namespace Avalonia.UnitTests public Size ClientSize => new Size(100, 100); - public Size MaxClientSize => Size.Infinity; + public Size MaxClientSize { get; set; } = Size.Infinity; public double LayoutScaling => 1; From ac3ca7ca292d4e59ca4f373ee04918a8d6da0ee5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 11 Jun 2017 00:53:54 +0200 Subject: [PATCH 3/7] Make LayoutManager pass new tests. --- src/Avalonia.Layout/LayoutManager.cs | 38 +++++++++---------- .../LayoutManagerTests.cs | 31 +++++++++++++++ 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index b7b83bf852..e8fc7acf2a 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -124,21 +124,21 @@ namespace Avalonia.Layout private void Measure(ILayoutable control) { - var root = control as ILayoutRoot; - var parent = control.VisualParent as ILayoutable; - - if (root != null) - { - root.Measure(root.MaxClientSize); - } - else if (parent != null) + if (control.VisualParent is ILayoutable parent) { Measure(parent); } if (!control.IsMeasureValid) { - control.Measure(control.PreviousMeasure.Value); + if (control is ILayoutRoot root) + { + root.Measure(Size.Infinity); + } + else if (!control.IsMeasureValid && control.IsAttachedToVisualTree) + { + control.Measure(control.PreviousMeasure.Value); + } } _toMeasure.Remove(control); @@ -146,21 +146,21 @@ namespace Avalonia.Layout private void Arrange(ILayoutable control) { - var root = control as ILayoutRoot; - var parent = control.VisualParent as ILayoutable; - - if (root != null) - { - root.Arrange(new Rect(root.DesiredSize)); - } - else if (parent != null) + if (control.VisualParent is ILayoutable parent) { Arrange(parent); } - if (control.PreviousArrange.HasValue) + if (!control.IsArrangeValid) { - control.Arrange(control.PreviousArrange.Value); + if (control is ILayoutRoot root) + { + root.Arrange(new Rect(control.DesiredSize)); + } + else if (!control.IsArrangeValid && control.IsAttachedToVisualTree) + { + control.Arrange(control.PreviousArrange.Value); + } } _toArrange.Remove(control); diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index 45e8803f16..361e7678be 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -228,6 +228,37 @@ namespace Avalonia.Layout.UnitTests } } + [Fact] + public void Arranges_Root_With_DesiredSize() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var root = new LayoutTestRoot + { + Width = 100, + Height = 100, + }; + + var arrangeSize = default(Size); + + root.DoArrangeOverride = (_, s) => + { + arrangeSize = s; + return s; + }; + + target.ExecuteInitialLayoutPass(root); + Assert.Equal(new Size(100, 100), arrangeSize); + + root.Width = 120; + + target.ExecuteLayoutPass(); + Assert.Equal(new Size(120, 100), arrangeSize); + } + } + [Fact] public void Invalidating_Child_Remeasures_Parent() { From f97ebe961b5433d621d76b310334f7270902d6bf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 11 Jun 2017 15:16:17 +0200 Subject: [PATCH 4/7] Fixed some stupid mistakes in algorithm. --- src/Avalonia.Layout/LayoutManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index e8fc7acf2a..0933af7d7e 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -129,13 +129,13 @@ namespace Avalonia.Layout Measure(parent); } - if (!control.IsMeasureValid) + if (!control.IsMeasureValid && control.IsAttachedToVisualTree) { if (control is ILayoutRoot root) { root.Measure(Size.Infinity); } - else if (!control.IsMeasureValid && control.IsAttachedToVisualTree) + else { control.Measure(control.PreviousMeasure.Value); } @@ -151,13 +151,13 @@ namespace Avalonia.Layout Arrange(parent); } - if (!control.IsArrangeValid) + if (!control.IsArrangeValid && control.IsAttachedToVisualTree) { if (control is ILayoutRoot root) { root.Arrange(new Rect(control.DesiredSize)); } - else if (!control.IsArrangeValid && control.IsAttachedToVisualTree) + else { control.Arrange(control.PreviousArrange.Value); } From a1d46a7784bbab4424b69f995faace7131618a09 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 11 Jun 2017 15:34:36 +0200 Subject: [PATCH 5/7] Use a stack instead of HashSet. Controls that are already invalid will not invalidate themselves again to with the `LayoutManager`, so we don't need to worry about duplicates. --- src/Avalonia.Layout/LayoutManager.cs | 40 +++++++++++++++++----------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index 0933af7d7e..2158a06992 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -14,8 +14,8 @@ namespace Avalonia.Layout /// public class LayoutManager : ILayoutManager { - private readonly HashSet _toMeasure = new HashSet(); - private readonly HashSet _toArrange = new HashSet(); + private readonly Queue _toMeasure = new Queue(); + private readonly Queue _toArrange = new Queue(); private bool _queued; private bool _running; @@ -30,9 +30,12 @@ namespace Avalonia.Layout Contract.Requires(control != null); Dispatcher.UIThread.VerifyAccess(); - _toMeasure.Add(control); - _toArrange.Add(control); - QueueLayoutPass(); + if (control.IsAttachedToVisualTree) + { + _toMeasure.Enqueue(control); + _toArrange.Enqueue(control); + QueueLayoutPass(); + } } /// @@ -41,8 +44,11 @@ namespace Avalonia.Layout Contract.Requires(control != null); Dispatcher.UIThread.VerifyAccess(); - _toArrange.Add(control); - QueueLayoutPass(); + if (control.IsAttachedToVisualTree) + { + _toArrange.Enqueue(control); + QueueLayoutPass(); + } } /// @@ -108,8 +114,12 @@ namespace Avalonia.Layout { while (_toMeasure.Count > 0) { - var next = _toMeasure.First(); - Measure(next); + var control = _toMeasure.Dequeue(); + + if (!control.IsMeasureValid && control.IsAttachedToVisualTree) + { + Measure(control); + } } } @@ -117,8 +127,12 @@ namespace Avalonia.Layout { while (_toArrange.Count > 0 && _toMeasure.Count == 0) { - var next = _toArrange.First(); - Arrange(next); + var control = _toArrange.Dequeue(); + + if (!control.IsArrangeValid && control.IsAttachedToVisualTree) + { + Arrange(control); + } } } @@ -140,8 +154,6 @@ namespace Avalonia.Layout control.Measure(control.PreviousMeasure.Value); } } - - _toMeasure.Remove(control); } private void Arrange(ILayoutable control) @@ -162,8 +174,6 @@ namespace Avalonia.Layout control.Arrange(control.PreviousArrange.Value); } } - - _toArrange.Remove(control); } private void QueueLayoutPass() From 18f9e2840d47c771042b2a25a4d7668dcc62fdf0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 11 Jun 2017 15:53:23 +0200 Subject: [PATCH 6/7] Explain the algoithm a bit. --- src/Avalonia.Layout/LayoutManager.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index 2158a06992..146542698f 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -138,11 +138,18 @@ namespace Avalonia.Layout private void Measure(ILayoutable control) { + // Controls closest to the visual root need to be arranged first. We don't try to store + // ordered invalidation lists, instead we traverse the tree upwards, measuring the + // controls closest to the root first. This has been shown by benchmarks to be the + // fastest and most memory-efficent algorithm. if (control.VisualParent is ILayoutable parent) { Measure(parent); } + // If the control being measured has IsMeasureValid == true here then its measure was + // handed by an ancestor and can be ignored. The measure may have also caused the + // control to be removed. if (!control.IsMeasureValid && control.IsAttachedToVisualTree) { if (control is ILayoutRoot root) From 40c342989b538022a82d6c12151ea1995499d44f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 13 Jun 2017 01:01:20 +0200 Subject: [PATCH 7/7] Assert control invalidation behavior. Controls not attached to the visual tree should not notify the `LayoutManager` that they have had their layout invalidated. Similarly when added to the visual tree their parents and themselves should have their layout invalidated. --- src/Avalonia.Layout/LayoutManager.cs | 28 ++++-- src/Avalonia.Layout/Layoutable.cs | 16 +++- .../LayoutableTests.cs | 90 +++++++++++++++++++ 3 files changed, 123 insertions(+), 11 deletions(-) create mode 100644 tests/Avalonia.Layout.UnitTests/LayoutableTests.cs diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index 146542698f..965ab3eee6 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -30,12 +30,19 @@ namespace Avalonia.Layout Contract.Requires(control != null); Dispatcher.UIThread.VerifyAccess(); - if (control.IsAttachedToVisualTree) + if (!control.IsAttachedToVisualTree) { - _toMeasure.Enqueue(control); - _toArrange.Enqueue(control); - QueueLayoutPass(); +#if DEBUG + throw new AvaloniaInternalException( + "LayoutManager.InvalidateMeasure called on a control that is detached from the visual tree."); +#else + return; +#endif } + + _toMeasure.Enqueue(control); + _toArrange.Enqueue(control); + QueueLayoutPass(); } /// @@ -44,11 +51,18 @@ namespace Avalonia.Layout Contract.Requires(control != null); Dispatcher.UIThread.VerifyAccess(); - if (control.IsAttachedToVisualTree) + if (!control.IsAttachedToVisualTree) { - _toArrange.Enqueue(control); - QueueLayoutPass(); +#if DEBUG + throw new AvaloniaInternalException( + "LayoutManager.InvalidateArrange called on a control that is detached from the visual tree."); +#else + return; +#endif } + + _toArrange.Enqueue(control); + QueueLayoutPass(); } /// diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index 20050058bf..dad00d93d4 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -378,8 +378,12 @@ namespace Avalonia.Layout IsMeasureValid = false; IsArrangeValid = false; - LayoutManager.Instance?.InvalidateMeasure(this); - InvalidateVisual(); + + if (((ILayoutable)this).IsAttachedToVisualTree) + { + LayoutManager.Instance?.InvalidateMeasure(this); + InvalidateVisual(); + } } } @@ -393,8 +397,12 @@ namespace Avalonia.Layout Logger.Verbose(LogArea.Layout, this, "Invalidated arrange"); IsArrangeValid = false; - LayoutManager.Instance?.InvalidateArrange(this); - InvalidateVisual(); + + if (((ILayoutable)this).IsAttachedToVisualTree) + { + LayoutManager.Instance?.InvalidateArrange(this); + InvalidateVisual(); + } } } diff --git a/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs new file mode 100644 index 0000000000..dcc65edc74 --- /dev/null +++ b/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs @@ -0,0 +1,90 @@ +using System; +using Avalonia.Controls; +using Moq; +using Xunit; + +namespace Avalonia.Layout.UnitTests +{ + public class LayoutableTests + { + [Fact] + public void Only_Calls_LayoutManager_InvalidateMeasure_Once() + { + var target = new Mock(); + + using (Start(target.Object)) + { + var control = new Decorator(); + var root = new LayoutTestRoot { Child = control }; + + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + target.ResetCalls(); + + control.InvalidateMeasure(); + control.InvalidateMeasure(); + + target.Verify(x => x.InvalidateMeasure(control), Times.Once()); + } + } + + [Fact] + public void Only_Calls_LayoutManager_InvalidateArrange_Once() + { + var target = new Mock(); + + using (Start(target.Object)) + { + var control = new Decorator(); + var root = new LayoutTestRoot { Child = control }; + + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + target.ResetCalls(); + + control.InvalidateArrange(); + control.InvalidateArrange(); + + target.Verify(x => x.InvalidateArrange(control), Times.Once()); + } + } + + [Fact] + public void Attaching_Control_To_Tree_Invalidates_Parent_Measure() + { + var target = new Mock(); + + using (Start(target.Object)) + { + var control = new Decorator(); + var root = new LayoutTestRoot { Child = control }; + + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + Assert.True(control.IsMeasureValid); + + root.Child = null; + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + Assert.False(control.IsMeasureValid); + Assert.True(root.IsMeasureValid); + + target.ResetCalls(); + + root.Child = control; + + Assert.False(root.IsMeasureValid); + Assert.False(control.IsMeasureValid); + target.Verify(x => x.InvalidateMeasure(root), Times.Once()); + } + } + + private IDisposable Start(ILayoutManager layoutManager) + { + var result = AvaloniaLocator.EnterScope(); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(layoutManager); + return result; + } + } +}