diff --git a/src/Controls/src/Core/ContentPresenter.cs b/src/Controls/src/Core/ContentPresenter.cs
index 590a0802e913..fb517a905715 100644
--- a/src/Controls/src/Core/ContentPresenter.cs
+++ b/src/Controls/src/Core/ContentPresenter.cs
@@ -130,10 +130,5 @@ Size ICrossPlatformLayout.CrossPlatformArrange(Rect bounds)
this.ArrangeContent(bounds);
return bounds.Size;
}
-
- private protected override void InvalidateMeasureLegacy(InvalidationTrigger trigger, int depth, int depthLeveltoInvalidate)
- {
- base.InvalidateMeasureLegacy(trigger, depth, 1);
- }
}
}
\ No newline at end of file
diff --git a/src/Controls/src/Core/InvalidationEventArgs.cs b/src/Controls/src/Core/InvalidationEventArgs.cs
index 701d725c5290..050c7548fd63 100644
--- a/src/Controls/src/Core/InvalidationEventArgs.cs
+++ b/src/Controls/src/Core/InvalidationEventArgs.cs
@@ -10,15 +10,7 @@ public InvalidationEventArgs(InvalidationTrigger trigger)
{
Trigger = trigger;
}
- public InvalidationEventArgs(InvalidationTrigger trigger, int depth) : this(trigger)
- {
- CurrentInvalidationDepth = depth;
- }
-
public InvalidationTrigger Trigger { get; private set; }
-
-
- public int CurrentInvalidationDepth { set; get; }
}
}
\ No newline at end of file
diff --git a/src/Controls/src/Core/LegacyLayouts/Layout.cs b/src/Controls/src/Core/LegacyLayouts/Layout.cs
index 8b9ac2371c06..e02c311f545b 100644
--- a/src/Controls/src/Core/LegacyLayouts/Layout.cs
+++ b/src/Controls/src/Core/LegacyLayouts/Layout.cs
@@ -202,8 +202,12 @@ public override SizeRequest Measure(double widthConstraint, double heightConstra
SizeRequest size = base.Measure(widthConstraint - Padding.HorizontalThickness, heightConstraint - Padding.VerticalThickness, flags);
#pragma warning restore CS0618 // Type or member is obsolete
#pragma warning disable CS0618 // Type or member is obsolete
- return new SizeRequest(new Size(size.Request.Width + Padding.HorizontalThickness, size.Request.Height + Padding.VerticalThickness),
- new Size(size.Minimum.Width + Padding.HorizontalThickness, size.Minimum.Height + Padding.VerticalThickness));
+ var request = new Size(size.Request.Width + Padding.HorizontalThickness, size.Request.Height + Padding.VerticalThickness);
+ var minimum = new Size(size.Minimum.Width + Padding.HorizontalThickness, size.Minimum.Height + Padding.VerticalThickness);
+
+ DesiredSize = request;
+
+ return new SizeRequest(request, minimum);
#pragma warning restore CS0618 // Type or member is obsolete
}
#pragma warning restore CS0672 // Member overrides obsolete member
@@ -320,7 +324,7 @@ public void RaiseChild(View view)
[Obsolete("Use InvalidateMeasure depending on your scenario")]
protected virtual void InvalidateLayout()
{
- _hasDoneLayout = false;
+ SetNeedsLayout();
InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
if (!_hasDoneLayout)
{
@@ -328,6 +332,11 @@ protected virtual void InvalidateLayout()
}
}
+ void SetNeedsLayout()
+ {
+ _hasDoneLayout = false;
+ }
+
///
/// Positions and sizes the children of a layout.
///
@@ -341,10 +350,18 @@ protected virtual void InvalidateLayout()
[Obsolete("Use ArrangeOverride")]
protected abstract void LayoutChildren(double x, double y, double width, double height);
- internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger, int depth)
+ internal override void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger)
{
- // TODO: once we remove old Xamarin public signatures we can invoke `OnChildMeasureInvalidated(VisualElement, InvalidationTrigger)` directly
- OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger, depth));
+ SetNeedsLayout();
+ InvalidateMeasureCache();
+
+ OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger));
+
+ var propagatedTrigger = GetPropagatedTrigger(trigger);
+ InvokeMeasureInvalidated(propagatedTrigger);
+
+ // Behavior of legacy layouts is to always propagate the measure invalidation to the parent
+ (Parent as VisualElement)?.OnChildMeasureInvalidated(this, propagatedTrigger);
}
///
@@ -356,19 +373,6 @@ internal override void OnChildMeasureInvalidatedInternal(VisualElement child, In
/// This method has a default implementation and application developers must call the base implementation.
protected void OnChildMeasureInvalidated(object sender, EventArgs e)
{
- var depth = 0;
- InvalidationTrigger trigger;
- if (e is InvalidationEventArgs args)
- {
- trigger = args.Trigger;
- depth = args.CurrentInvalidationDepth;
- }
- else
- {
- trigger = InvalidationTrigger.Undefined;
- }
-
- OnChildMeasureInvalidated((VisualElement)sender, trigger, depth);
OnChildMeasureInvalidated();
}
@@ -542,55 +546,6 @@ internal static void LayoutChildIntoBoundingRegion(View child, Rect region, Size
child.Layout(region);
}
- internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger, int depth)
- {
- IReadOnlyList children = LogicalChildrenInternal;
- int count = children.Count;
- for (var index = 0; index < count; index++)
- {
- if (LogicalChildrenInternal[index] is VisualElement v && v.IsVisible && (!v.IsPlatformEnabled || !v.IsPlatformStateConsistent))
- {
- return;
- }
- }
-
- if (child is View view)
- {
- // we can ignore the request if we are either fully constrained or when the size request changes and we were already fully constrained
- if ((trigger == InvalidationTrigger.MeasureChanged && view.Constraint == LayoutConstraint.Fixed) ||
- (trigger == InvalidationTrigger.SizeRequestChanged && view.ComputedConstraint == LayoutConstraint.Fixed))
- {
- return;
- }
- if (trigger == InvalidationTrigger.HorizontalOptionsChanged || trigger == InvalidationTrigger.VerticalOptionsChanged)
- {
- ComputeConstraintForView(view);
- }
- }
-
- InvalidateMeasureLegacy(trigger, depth, int.MaxValue);
- }
-
- // This lets us override the rules for invalidation on MAUI controls that unfortunately still inheirt from the legacy layout
- private protected virtual void InvalidateMeasureLegacy(InvalidationTrigger trigger, int depth, int depthLeveltoInvalidate)
- {
- if (depth <= depthLeveltoInvalidate)
- {
- if (trigger == InvalidationTrigger.RendererReady)
- {
- InvalidateMeasureInternal(new InvalidationEventArgs(InvalidationTrigger.RendererReady, depth));
- }
- else
- {
- InvalidateMeasureInternal(new InvalidationEventArgs(InvalidationTrigger.MeasureChanged, depth));
- }
- }
- else
- {
- FireMeasureChanged(trigger, depth);
- }
- }
-
internal override void OnIsVisibleChanged(bool oldValue, bool newValue)
{
base.OnIsVisibleChanged(oldValue, newValue);
@@ -708,19 +663,6 @@ bool ShouldLayoutChildren()
return true;
}
- protected override void InvalidateMeasureOverride()
- {
- base.InvalidateMeasureOverride();
-
- foreach (var child in ((IElementController)this).LogicalChildren)
- {
- if (child is IView fe)
- {
- fe.InvalidateMeasure();
- }
- }
- }
-
protected override Size ArrangeOverride(Rect bounds)
{
base.ArrangeOverride(bounds);
diff --git a/src/Controls/src/Core/LegacyLayouts/StackLayout.cs b/src/Controls/src/Core/LegacyLayouts/StackLayout.cs
index 146826a97502..3b948478a6a0 100644
--- a/src/Controls/src/Core/LegacyLayouts/StackLayout.cs
+++ b/src/Controls/src/Core/LegacyLayouts/StackLayout.cs
@@ -92,12 +92,18 @@ protected override SizeRequest OnMeasure(double widthConstraint, double heightCo
return result;
}
+ internal override void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger)
+ {
+ _layoutInformation = new LayoutInformation();
+ base.OnChildMeasureInvalidated(child, trigger);
+ }
+
internal override void ComputeConstraintForView(View view)
{
ComputeConstraintForView(view, false);
}
- internal override void InvalidateMeasureInternal(InvalidationEventArgs trigger)
+ internal override void InvalidateMeasureInternal(InvalidationTrigger trigger)
{
_layoutInformation = new LayoutInformation();
base.InvalidateMeasureInternal(trigger);
diff --git a/src/Controls/src/Core/Page/Page.cs b/src/Controls/src/Core/Page/Page.cs
index 941f47c52dfb..985bc26842cf 100644
--- a/src/Controls/src/Core/Page/Page.cs
+++ b/src/Controls/src/Core/Page/Page.cs
@@ -506,10 +506,11 @@ protected override void OnBindingContextChanged()
}
- internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger, int depth)
+ internal override void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger)
{
- // TODO: once we remove old Xamarin public signatures we can invoke `OnChildMeasureInvalidated(VisualElement, InvalidationTrigger)` directly
- OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger, depth));
+ OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger));
+ var propagatedTrigger = GetPropagatedTrigger(trigger);
+ InvokeMeasureInvalidated(propagatedTrigger);
}
///
@@ -519,19 +520,6 @@ internal override void OnChildMeasureInvalidatedInternal(VisualElement child, In
/// The event arguments.
protected virtual void OnChildMeasureInvalidated(object sender, EventArgs e)
{
- var depth = 0;
- InvalidationTrigger trigger;
- if (e is InvalidationEventArgs args)
- {
- trigger = args.Trigger;
- depth = args.CurrentInvalidationDepth;
- }
- else
- {
- trigger = InvalidationTrigger.Undefined;
- }
-
- OnChildMeasureInvalidated((VisualElement)sender, trigger, depth);
}
///
@@ -610,36 +598,6 @@ protected void UpdateChildrenLayout()
}
}
- internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger, int depth)
- {
- var container = this as IPageContainer;
- if (container != null)
- {
- Page page = container.CurrentPage;
- if (page != null && page.IsVisible && (!page.IsPlatformEnabled || !page.IsPlatformStateConsistent))
- return;
- }
- else
- {
- var logicalChildren = this.InternalChildren;
- for (var i = 0; i < logicalChildren.Count; i++)
- {
- var v = logicalChildren[i] as VisualElement;
- if (v != null && v.IsVisible && (!v.IsPlatformEnabled || !v.IsPlatformStateConsistent))
- return;
- }
- }
-
- if (depth <= 1)
- {
- InvalidateMeasureInternal(new InvalidationEventArgs(InvalidationTrigger.MeasureChanged, depth));
- }
- else
- {
- FireMeasureChanged(trigger, depth);
- }
- }
-
internal void OnAppearing(Action action)
{
if (_hasAppeared)
diff --git a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt
index f48b2b26b2c2..5b8f9021d63e 100644
--- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt
@@ -69,6 +69,7 @@ const Microsoft.Maui.Controls.TitleBar.TrailingHiddenState = "TrailingContentCol
const Microsoft.Maui.Controls.TitleBar.TrailingVisibleState = "TrailingContentVisible" -> string!
Microsoft.Maui.Controls.Embedding.EmbeddingExtensions
Microsoft.Maui.Controls.HandlerProperties
+*REMOVED*override Microsoft.Maui.Controls.Compatibility.Layout.InvalidateMeasureOverride() -> void
Microsoft.Maui.Controls.HybridWebView
Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string?
Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void
diff --git a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt
index 63a6c7ddd87a..4eeaee9652d9 100644
--- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt
@@ -205,6 +205,7 @@ const Microsoft.Maui.Controls.TitleBar.TrailingHiddenState = "TrailingContentCol
const Microsoft.Maui.Controls.TitleBar.TrailingVisibleState = "TrailingContentVisible" -> string!
Microsoft.Maui.Controls.Embedding.EmbeddingExtensions
Microsoft.Maui.Controls.HandlerProperties
+*REMOVED*override Microsoft.Maui.Controls.Compatibility.Layout.InvalidateMeasureOverride() -> void
*REMOVED*Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker
*REMOVED*Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker.Dispose() -> void
*REMOVED*Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker.OnLayoutSubviews() -> void
diff --git a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
index 4d313d1b0c85..6610d9f6732e 100644
--- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
@@ -206,6 +206,7 @@ const Microsoft.Maui.Controls.TitleBar.TrailingHiddenState = "TrailingContentCol
const Microsoft.Maui.Controls.TitleBar.TrailingVisibleState = "TrailingContentVisible" -> string!
Microsoft.Maui.Controls.Embedding.EmbeddingExtensions
Microsoft.Maui.Controls.HandlerProperties
+*REMOVED*override Microsoft.Maui.Controls.Compatibility.Layout.InvalidateMeasureOverride() -> void
*REMOVED*Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker
*REMOVED*Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker.Dispose() -> void
*REMOVED*Microsoft.Maui.Controls.Handlers.Compatibility.ShellScrollViewTracker.OnLayoutSubviews() -> void
diff --git a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
index 275b44d25503..4ee051439ef7 100644
--- a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
@@ -68,6 +68,7 @@ const Microsoft.Maui.Controls.TitleBar.TitleVisibleState = "TitleVisible" -> str
const Microsoft.Maui.Controls.TitleBar.TrailingHiddenState = "TrailingContentCollapsed" -> string!
const Microsoft.Maui.Controls.TitleBar.TrailingVisibleState = "TrailingContentVisible" -> string!
Microsoft.Maui.Controls.HandlerProperties
+*REMOVED*override Microsoft.Maui.Controls.Compatibility.Layout.InvalidateMeasureOverride() -> void
Microsoft.Maui.Controls.HybridWebView
Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string?
Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void
diff --git a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt
index b1dbe9d0aa07..c2190ccab4e5 100644
--- a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt
@@ -70,6 +70,7 @@ const Microsoft.Maui.Controls.TitleBar.TrailingHiddenState = "TrailingContentCol
const Microsoft.Maui.Controls.TitleBar.TrailingVisibleState = "TrailingContentVisible" -> string!
Microsoft.Maui.Controls.Embedding.EmbeddingExtensions
Microsoft.Maui.Controls.HandlerProperties
+*REMOVED*override Microsoft.Maui.Controls.Compatibility.Layout.InvalidateMeasureOverride() -> void
Microsoft.Maui.Controls.HybridWebView
Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string?
Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void
diff --git a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt
index 27745f220371..0950e21053b6 100644
--- a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt
@@ -68,6 +68,7 @@ const Microsoft.Maui.Controls.TitleBar.TitleVisibleState = "TitleVisible" -> str
const Microsoft.Maui.Controls.TitleBar.TrailingHiddenState = "TrailingContentCollapsed" -> string!
const Microsoft.Maui.Controls.TitleBar.TrailingVisibleState = "TrailingContentVisible" -> string!
Microsoft.Maui.Controls.HandlerProperties
+*REMOVED*override Microsoft.Maui.Controls.Compatibility.Layout.InvalidateMeasureOverride() -> void
Microsoft.Maui.Controls.HybridWebView
Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string?
Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void
diff --git a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt
index 275b44d25503..4ee051439ef7 100644
--- a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt
@@ -68,6 +68,7 @@ const Microsoft.Maui.Controls.TitleBar.TitleVisibleState = "TitleVisible" -> str
const Microsoft.Maui.Controls.TitleBar.TrailingHiddenState = "TrailingContentCollapsed" -> string!
const Microsoft.Maui.Controls.TitleBar.TrailingVisibleState = "TrailingContentVisible" -> string!
Microsoft.Maui.Controls.HandlerProperties
+*REMOVED*override Microsoft.Maui.Controls.Compatibility.Layout.InvalidateMeasureOverride() -> void
Microsoft.Maui.Controls.HybridWebView
Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string?
Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void
diff --git a/src/Controls/src/Core/ScrollView/ScrollView.cs b/src/Controls/src/Core/ScrollView/ScrollView.cs
index 23489e87fb54..7c226a3b7565 100644
--- a/src/Controls/src/Core/ScrollView/ScrollView.cs
+++ b/src/Controls/src/Core/ScrollView/ScrollView.cs
@@ -478,11 +478,6 @@ Size ICrossPlatformLayout.CrossPlatformArrange(Rect bounds)
return bounds.Size;
}
- private protected override void InvalidateMeasureLegacy(InvalidationTrigger trigger, int depth, int depthLeveltoInvalidate)
- {
- base.InvalidateMeasureLegacy(trigger, depth, 1);
- }
-
private protected override string GetDebuggerDisplay()
{
var debugText = DebuggerDisplayHelpers.GetDebugText(nameof(Content), Content);
diff --git a/src/Controls/src/Core/TemplatedView/TemplatedView.cs b/src/Controls/src/Core/TemplatedView/TemplatedView.cs
index 23ef916eafbd..3e8cd53943bf 100644
--- a/src/Controls/src/Core/TemplatedView/TemplatedView.cs
+++ b/src/Controls/src/Core/TemplatedView/TemplatedView.cs
@@ -150,11 +150,6 @@ Size ICrossPlatformLayout.CrossPlatformArrange(Rect bounds)
return bounds.Size;
}
- private protected override void InvalidateMeasureLegacy(InvalidationTrigger trigger, int depth, int depthLeveltoInvalidate)
- {
- base.InvalidateMeasureLegacy(trigger, depth, 1);
- }
-
#nullable disable
}
diff --git a/src/Controls/src/Core/VisualElement/VisualElement.cs b/src/Controls/src/Core/VisualElement/VisualElement.cs
index 6161fcd8a8c1..74e941ce8a6b 100644
--- a/src/Controls/src/Core/VisualElement/VisualElement.cs
+++ b/src/Controls/src/Core/VisualElement/VisualElement.cs
@@ -1384,6 +1384,7 @@ internal void ComputeConstrainsForChildren()
}
}
+ // TODO: .NET10 this should be made public so whoever implements a custom layout can leverage this
internal virtual void ComputeConstraintForView(View view) => view.ComputedConstraint = LayoutConstraint.None;
///
@@ -1408,23 +1409,25 @@ public void InvalidateMeasureNonVirtual(InvalidationTrigger trigger)
InvalidateMeasureInternal(trigger);
}
- internal void InvalidateMeasureInternal(InvalidationTrigger trigger)
+ internal virtual void InvalidateMeasureInternal(InvalidationTrigger trigger)
{
- InvalidateMeasureInternal(new InvalidationEventArgs(trigger, 0));
- }
-
- internal virtual void InvalidateMeasureInternal(InvalidationEventArgs eventArgs)
- {
- _measureCache.Clear();
+ InvalidateMeasureCache();
- // TODO ezhart Once we get InvalidateArrange sorted, HorizontalOptionsChanged and
- // VerticalOptionsChanged will need to call ParentView.InvalidateArrange() instead
- switch (eventArgs.Trigger)
+ switch (trigger)
{
case InvalidationTrigger.MarginChanged:
+ ParentView?.InvalidateMeasure();
+ break;
case InvalidationTrigger.HorizontalOptionsChanged:
case InvalidationTrigger.VerticalOptionsChanged:
+ if (this is View thisView && Parent is VisualElement visualParent)
+ {
+ visualParent.ComputeConstraintForView(thisView);
+ }
+
+ // TODO ezhart Once we get InvalidateArrange sorted, HorizontalOptionsChanged and
+ // VerticalOptionsChanged will need to call ParentView.InvalidateArrange() instead
ParentView?.InvalidateMeasure();
break;
default:
@@ -1432,50 +1435,47 @@ internal virtual void InvalidateMeasureInternal(InvalidationEventArgs eventArgs)
break;
}
- FireMeasureChanged(eventArgs);
+ InvokeMeasureInvalidated(trigger);
+#pragma warning disable CS0618 // Type or member is obsolete
+ (Parent as VisualElement)?.OnChildMeasureInvalidated(this, trigger);
+#pragma warning restore CS0618 // Type or member is obsolete
}
- private protected void FireMeasureChanged(InvalidationTrigger trigger, int depth)
+ private protected void InvokeMeasureInvalidated(InvalidationTrigger trigger)
{
- FireMeasureChanged(new InvalidationEventArgs(trigger, depth));
+ MeasureInvalidated?.Invoke(this, new InvalidationEventArgs(trigger));
}
+ ///
+ /// A flag that determines whether the measure invalidated event should not be propagated up the visual tree.
+ ///
+ ///
+ /// Propagation will still occur within legacy layout subtrees.
+ ///
+ internal static bool SkipMeasureInvalidatedPropagation { get; set /* for testing purpose */; } =
+ AppContext.TryGetSwitch("Microsoft.Maui.RuntimeFeature.SkipMeasureInvalidatedPropagation", out var enabled) && enabled;
- private protected void FireMeasureChanged(InvalidationEventArgs args)
+ internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger)
{
- var depth = args.CurrentInvalidationDepth;
- MeasureInvalidated?.Invoke(this, args);
- (Parent as VisualElement)?.OnChildMeasureInvalidatedInternal(this, args.Trigger, ++depth);
+ if (SkipMeasureInvalidatedPropagation)
+ {
+ return;
+ }
+
+ var propagatedTrigger = GetPropagatedTrigger(trigger);
+ InvokeMeasureInvalidated(propagatedTrigger);
+ (Parent as VisualElement)?.OnChildMeasureInvalidated(this, propagatedTrigger);
}
- // We don't want to change the execution path of Page or Layout when they are calling "InvalidationMeasure"
- // If you look at page it calls OnChildMeasureInvalidated from OnChildMeasureInvalidatedInternal
- // Because OnChildMeasureInvalidated is public API and the user might override it, we need to keep it as is
- //private protected int CurrentInvalidationDepth { get; set; }
+ private protected static InvalidationTrigger GetPropagatedTrigger(InvalidationTrigger trigger)
+ {
+ var propagatedTrigger = trigger == InvalidationTrigger.RendererReady ? trigger : InvalidationTrigger.MeasureChanged;
+ return propagatedTrigger;
+ }
- internal virtual void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger, int depth)
+ private protected void InvalidateMeasureCache()
{
- switch (trigger)
- {
- case InvalidationTrigger.VerticalOptionsChanged:
- case InvalidationTrigger.HorizontalOptionsChanged:
- // When a child changes its HorizontalOptions or VerticalOptions
- // the size of the parent won't change, so we don't have to invalidate the measure
- return;
- case InvalidationTrigger.RendererReady:
- // Undefined happens in many cases, including when `IsVisible` changes
- case InvalidationTrigger.Undefined:
- FireMeasureChanged(trigger, depth);
- return;
- default:
- // When visibility changes `InvalidationTrigger.Undefined` is used,
- // so here we're sure that visibility didn't change
- if (child.IsVisible)
- {
- FireMeasureChanged(InvalidationTrigger.MeasureChanged, depth);
- }
- return;
- }
+ _measureCache.Clear();
}
///
diff --git a/src/Controls/tests/Core.UnitTests/PageTests.cs b/src/Controls/tests/Core.UnitTests/PageTests.cs
index 7c530229ec8f..22b758d94205 100644
--- a/src/Controls/tests/Core.UnitTests/PageTests.cs
+++ b/src/Controls/tests/Core.UnitTests/PageTests.cs
@@ -567,16 +567,16 @@ public void LogicalChildrenDontAddToPagesInternalChildren()
}
[Fact]
- public void MeasureInvalidatedPropagatesUpTree()
+ public void MeasureInvalidatedPropagatesUpTreeWithCompatibilityLayouts()
{
- var label = new Label()
+ var label = new LabelInvalidateMeasureCheck
{
IsPlatformEnabled = true
};
- var scrollView = new ScrollViewInvalidationMeasureCheck()
+ var scrollView = new ScrollViewInvalidationMeasureCheck
{
- Content = new VerticalStackLayout()
+ Content = new Compatibility.StackLayout
{
Children = { new ContentView { Content = label, IsPlatformEnabled = true } },
IsPlatformEnabled = true
@@ -584,74 +584,171 @@ public void MeasureInvalidatedPropagatesUpTree()
IsPlatformEnabled = true
};
- var page = new InvalidatePageInvalidateMeasureCheck()
+ var page = new InvalidatePageInvalidateMeasureCheck
{
Content = scrollView
};
- var window = new TestWindow(page);
-
- int fired = 0;
- page.MeasureInvalidated += (sender, args) =>
- {
- fired++;
- };
+ // Set up the window
+ _ = new TestWindow(page);
+ // Reset counters
+ label.InvalidateMeasureCount = 0;
+ label.PlatformInvalidateMeasureCount = 0;
page.InvalidateMeasureCount = 0;
+ page.PlatformInvalidateMeasureCount = 0;
scrollView.InvalidateMeasureCount = 0;
+ scrollView.PlatformInvalidateMeasureCount = 0;
+
+ // Invalidate the label
label.InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
- Assert.Equal(1, fired);
- Assert.Equal(0, page.InvalidateMeasureCount);
- Assert.Equal(0, scrollView.InvalidateMeasureCount);
- page.Content.InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
+ Assert.Equal(1, label.InvalidateMeasureCount);
+ Assert.Equal(1, label.PlatformInvalidateMeasureCount);
Assert.Equal(1, page.InvalidateMeasureCount);
+ Assert.Equal(0, page.PlatformInvalidateMeasureCount);
+ Assert.Equal(1, scrollView.InvalidateMeasureCount);
+ Assert.Equal(0, scrollView.PlatformInvalidateMeasureCount);
+
+ // Invalidate page content
+ page.Content.InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
+ Assert.Equal(2, page.InvalidateMeasureCount);
+ Assert.Equal(0, page.PlatformInvalidateMeasureCount);
+ }
+
+ [Theory]
+ [InlineData(true, 0)]
+ [InlineData(false, 1)]
+ public void MeasureInvalidatedPropagatesUpTreeOnAppSwitch(bool skipMeasureInvalidatedPropagation, int expectedAncestorMeasureInvalidatedEvents)
+ {
+ try
+ {
+ VisualElement.SkipMeasureInvalidatedPropagation = skipMeasureInvalidatedPropagation;
+
+ var label = new LabelInvalidateMeasureCheck { IsPlatformEnabled = true };
+
+ var contentView = new ContentViewInvalidationMeasureCheck { Content = label, IsPlatformEnabled = true };
+
+ var scrollView = new ScrollViewInvalidationMeasureCheck
+ {
+ // VerticalStackLayout is not a CompatibilityLayout so it will not propagate the MeasureInvalidated
+ // event up the tree unless VisualElement.IsMeasureInvalidatedPropagationEnabled switch is set to true
+ Content = new VerticalStackLayout
+ {
+ Children = { contentView },
+ IsPlatformEnabled = true
+ },
+ IsPlatformEnabled = true
+ };
+
+ var page = new InvalidatePageInvalidateMeasureCheck { Content = scrollView };
+
+ // Set up the window
+ _ = new TestWindow(page);
+
+ // Reset counters
+ label.InvalidateMeasureCount = 0;
+ label.PlatformInvalidateMeasureCount = 0;
+ contentView.InvalidateMeasureCount = 0;
+ contentView.PlatformInvalidateMeasureCount = 0;
+ scrollView.InvalidateMeasureCount = 0;
+ scrollView.PlatformInvalidateMeasureCount = 0;
+ page.InvalidateMeasureCount = 0;
+ page.PlatformInvalidateMeasureCount = 0;
+
+ // Invalidate the label
+ label.InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
+ Assert.Equal(1, label.InvalidateMeasureCount);
+ Assert.Equal(1, label.PlatformInvalidateMeasureCount);
+ Assert.Equal(1, contentView.InvalidateMeasureCount);
+ Assert.Equal(0, contentView.PlatformInvalidateMeasureCount);
+ Assert.Equal(expectedAncestorMeasureInvalidatedEvents, scrollView.InvalidateMeasureCount);
+ Assert.Equal(0, scrollView.PlatformInvalidateMeasureCount);
+ Assert.Equal(expectedAncestorMeasureInvalidatedEvents, page.InvalidateMeasureCount);
+ Assert.Equal(0, page.PlatformInvalidateMeasureCount);
+ }
+ finally
+ {
+ VisualElement.SkipMeasureInvalidatedPropagation = false;
+ }
}
class LabelInvalidateMeasureCheck : Label
{
+ public int PlatformInvalidateMeasureCount { get; set; }
public int InvalidateMeasureCount { get; set; }
public LabelInvalidateMeasureCheck()
{
+ MeasureInvalidated += (sender, args) =>
+ {
+ InvalidateMeasureCount++;
+ };
+ }
+
+ internal override void InvalidateMeasureInternal(InvalidationTrigger trigger)
+ {
+ base.InvalidateMeasureInternal(trigger);
+ PlatformInvalidateMeasureCount++;
+ }
+ }
+ class ContentViewInvalidationMeasureCheck : ContentView
+ {
+ public int PlatformInvalidateMeasureCount { get; set; }
+ public int InvalidateMeasureCount { get; set; }
+
+ public ContentViewInvalidationMeasureCheck()
+ {
+ MeasureInvalidated += (sender, args) =>
+ {
+ InvalidateMeasureCount++;
+ };
}
- internal override void InvalidateMeasureInternal(InvalidationEventArgs trigger)
+ internal override void InvalidateMeasureInternal(InvalidationTrigger trigger)
{
base.InvalidateMeasureInternal(trigger);
- InvalidateMeasureCount++;
+ PlatformInvalidateMeasureCount++;
}
}
class ScrollViewInvalidationMeasureCheck : ScrollView
{
+ public int PlatformInvalidateMeasureCount { get; set; }
public int InvalidateMeasureCount { get; set; }
public ScrollViewInvalidationMeasureCheck()
{
-
+ MeasureInvalidated += (sender, args) =>
+ {
+ InvalidateMeasureCount++;
+ };
}
- internal override void InvalidateMeasureInternal(InvalidationEventArgs trigger)
+ internal override void InvalidateMeasureInternal(InvalidationTrigger trigger)
{
base.InvalidateMeasureInternal(trigger);
- InvalidateMeasureCount++;
+ PlatformInvalidateMeasureCount++;
}
}
class InvalidatePageInvalidateMeasureCheck : ContentPage
{
+ public int PlatformInvalidateMeasureCount { get; set; }
public int InvalidateMeasureCount { get; set; }
public InvalidatePageInvalidateMeasureCheck()
{
-
+ MeasureInvalidated += (sender, args) =>
+ {
+ InvalidateMeasureCount++;
+ };
}
- internal override void InvalidateMeasureInternal(InvalidationEventArgs trigger)
+ internal override void InvalidateMeasureInternal(InvalidationTrigger trigger)
{
base.InvalidateMeasureInternal(trigger);
- InvalidateMeasureCount++;
+ PlatformInvalidateMeasureCount++;
}
}
}