From 110b536738e5ca16776eb7b9d8bddd4822a42c77 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 28 Apr 2023 11:55:37 +0200 Subject: [PATCH 1/4] fix(Animation): fix Issue #11156 System.InvalidOperationException: Call from invalid thread --- src/Avalonia.Base/Animation/Animation.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Animation/Animation.cs b/src/Avalonia.Base/Animation/Animation.cs index d62acc0d52..dd99c40cd3 100644 --- a/src/Avalonia.Base/Animation/Animation.cs +++ b/src/Avalonia.Base/Animation/Animation.cs @@ -200,7 +200,7 @@ namespace Avalonia.Animation /// /// The animation setter. /// The property animator value. - public static void SetAnimator(IAnimationSetter setter, + public static void SetAnimator(IAnimationSetter setter, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicMethods)] Type value) { @@ -319,7 +319,7 @@ namespace Avalonia.Animation if (animators.Count == 1) { var subscription = animators[0].Apply(this, control, clock, match, onComplete); - + if (subscription is not null) { subscriptions.Add(subscription); @@ -348,9 +348,11 @@ namespace Avalonia.Animation if (onComplete != null) { - Task.WhenAll(completionTasks!).ContinueWith( - (_, state) => ((Action)state!).Invoke(), - onComplete); + Task.WhenAll(completionTasks!) + .ContinueWith((_, state) => ((Action)state!).Invoke() + , onComplete + , TaskScheduler.FromCurrentSynchronizationContext() + ); } } return new CompositeDisposable(subscriptions); From c4a5567090357c49d0b69634406d6cb3195cd625 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 28 Apr 2023 12:32:20 +0200 Subject: [PATCH 2/4] Added failing test for #11161. --- .../Layout/LayoutManagerTests.cs | 33 +++++++++++++++++++ .../Layout/LayoutTestControl.cs | 32 ++++++++++++++---- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs index 09f78c5a6c..45a6efdd4a 100644 --- a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs @@ -514,5 +514,38 @@ namespace Avalonia.Base.UnitTests.Layout Assert.True(parent.IsMeasureValid); Assert.True(parent.IsArrangeValid); } + + [Fact] + public void Grandparent_Can_Invalidate_Root_Measure_During_Arrange() + { + // Issue #11161. + var child = new LayoutTestControl(); + var parent = new LayoutTestControl { Child = child }; + var grandparent = new LayoutTestControl { Child = parent }; + var root = new LayoutTestRoot { Child = grandparent }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + + grandparent.DoArrangeOverride = (_, s) => + { + root.InvalidateMeasure(); + return s; + }; + grandparent.CallBaseArrange = true; + + child.InvalidateMeasure(); + grandparent.InvalidateMeasure(); + + root.LayoutManager.ExecuteLayoutPass(); + + Assert.True(child.IsMeasureValid); + Assert.True(child.IsArrangeValid); + Assert.True(parent.IsMeasureValid); + Assert.True(parent.IsArrangeValid); + Assert.True(grandparent.IsMeasureValid); + Assert.True(grandparent.IsArrangeValid); + Assert.True(root.IsMeasureValid); + Assert.True(root.IsArrangeValid); + } } } diff --git a/tests/Avalonia.Base.UnitTests/Layout/LayoutTestControl.cs b/tests/Avalonia.Base.UnitTests/Layout/LayoutTestControl.cs index 62de81006e..d85c7ed9bc 100644 --- a/tests/Avalonia.Base.UnitTests/Layout/LayoutTestControl.cs +++ b/tests/Avalonia.Base.UnitTests/Layout/LayoutTestControl.cs @@ -10,21 +10,41 @@ namespace Avalonia.Base.UnitTests.Layout public bool Arranged { get; set; } public Func DoMeasureOverride { get; set; } public Func DoArrangeOverride { get; set; } + public bool CallBaseMeasure { get; set; } + public bool CallBaseArrange { get; set; } protected override Size MeasureOverride(Size availableSize) { Measured = true; - return DoMeasureOverride != null ? - DoMeasureOverride(this, availableSize) : - base.MeasureOverride(availableSize); + + if (DoMeasureOverride is not null) + { + var overrideResult = DoMeasureOverride(this, availableSize); + return CallBaseMeasure ? + base.MeasureOverride(overrideResult) : + overrideResult; + } + else + { + return base.MeasureOverride(availableSize); + } } protected override Size ArrangeOverride(Size finalSize) { Arranged = true; - return DoArrangeOverride != null ? - DoArrangeOverride(this, finalSize) : - base.ArrangeOverride(finalSize); + + if (DoArrangeOverride is not null) + { + var overrideResult = DoArrangeOverride(this, finalSize); + return CallBaseArrange ? + base.ArrangeOverride(overrideResult) : + overrideResult; + } + else + { + return base.ArrangeOverride(finalSize); + } } } } From a03b73a7347fe442e141ead2662328cf686f3eee Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 28 Apr 2023 13:02:37 +0200 Subject: [PATCH 3/4] Always add control in measure queue to arrange queue. And also removed the checks for `IsAttachedToVisualTree`: that's going to be checked in `Arrange` and `Measure`. Fixes #11161 --- src/Avalonia.Base/Layout/LayoutManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index 747ee1c082..f47738f2e4 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -249,10 +249,12 @@ namespace Avalonia.Layout { var control = _toMeasure.Dequeue(); - if (!control.IsMeasureValid && control.IsAttachedToVisualTree) + if (!control.IsMeasureValid) { Measure(control); } + + _toArrange.Enqueue(control); } } @@ -262,7 +264,7 @@ namespace Avalonia.Layout { var control = _toArrange.Dequeue(); - if (!control.IsArrangeValid && control.IsAttachedToVisualTree) + if (!control.IsArrangeValid) { Arrange(control); } @@ -297,8 +299,6 @@ namespace Avalonia.Layout { control.Measure(control.PreviousMeasure.Value); } - - _toArrange.Enqueue(control); } return true; From 9a062901425a5166c23a5334df649d4d50412903 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 28 Apr 2023 17:51:41 +0200 Subject: [PATCH 4/4] Correctly remove ContentPresenter's content from its parent host When the content is updated while being detached from the visual tree. --- src/Avalonia.Controls/Presenters/ContentPresenter.cs | 9 ++++++--- tests/Avalonia.Controls.UnitTests/ContentControlTests.cs | 5 +++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 329a0fa6ab..736c338c10 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -1,5 +1,5 @@ using System; - +using Avalonia.Collections; using Avalonia.Controls.Documents; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; @@ -442,7 +442,7 @@ namespace Avalonia.Controls.Presenters var contentTemplate = ContentTemplate; var oldChild = Child; var newChild = CreateChild(content, oldChild, contentTemplate); - var logicalChildren = Host?.LogicalChildren ?? LogicalChildren; + var logicalChildren = GetEffectiveLogicalChildren(); // Remove the old child if we're not recycling it. if (newChild != oldChild) @@ -488,6 +488,9 @@ namespace Avalonia.Controls.Presenters } + private IAvaloniaList GetEffectiveLogicalChildren() + => Host?.LogicalChildren ?? LogicalChildren; + /// protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { @@ -692,7 +695,7 @@ namespace Avalonia.Controls.Presenters else if (Child != null) { VisualChildren.Remove(Child); - LogicalChildren.Remove(Child); + GetEffectiveLogicalChildren().Remove(Child); ((ISetInheritanceParent)Child).SetParent(Child.Parent); Child = null; _recyclingDataTemplate = null; diff --git a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs index bece711426..d5e4693666 100644 --- a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs @@ -359,16 +359,21 @@ namespace Avalonia.Controls.UnitTests target.Presenter.ApplyTemplate(); Assert.Equal(target, target.Presenter.Child.GetLogicalParent()); + Assert.Equal(new[] { target.Presenter.Child }, target.LogicalChildren); root.Child = null; Assert.Null(target.Template); target.Content = null; + + Assert.Empty(target.LogicalChildren); + root.Child = target; target.Content = "Bar"; Assert.Equal(target, target.Presenter.Child.GetLogicalParent()); + Assert.Equal(new[] { target.Presenter.Child }, target.LogicalChildren); } private static FuncControlTemplate GetTemplate()