diff --git a/src/Controls/src/Core/BindableObject.cs b/src/Controls/src/Core/BindableObject.cs
index 7bdb15610463..b612a4440fbb 100644
--- a/src/Controls/src/Core/BindableObject.cs
+++ b/src/Controls/src/Core/BindableObject.cs
@@ -361,11 +361,16 @@ public static void SetInheritedBindingContext(BindableObject bindable, object va
else
{
bindable._inheritedContext = new WeakReference(value);
- bindable.ApplyBindings(fromBindingContextChanged: true);
- bindable.OnBindingContextChanged();
+ bindable.ApplyBindingsFromBindingContextChanged();
}
}
+ private protected virtual void ApplyBindingsFromBindingContextChanged()
+ {
+ ApplyBindings(fromBindingContextChanged: true);
+ OnBindingContextChanged();
+ }
+
///
/// Applies all the current bindings to .
///
@@ -717,8 +722,7 @@ static void BindingContextPropertyBindingChanging(BindableObject bindable, Bindi
static void BindingContextPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
{
bindable._inheritedContext = null;
- bindable.ApplyBindings(fromBindingContextChanged: true);
- bindable.OnBindingContextChanged();
+ bindable.ApplyBindingsFromBindingContextChanged();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
diff --git a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/ListViewRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/ListViewRenderer.cs
index ae7fd8cce6a7..598186cad372 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/ListViewRenderer.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/ListViewRenderer.cs
@@ -20,7 +20,7 @@
namespace Microsoft.Maui.Controls.Handlers.Compatibility
{
- public class ListViewRenderer : ViewRenderer
+ public class ListViewRenderer : ViewRenderer, IPropagatesSetNeedsLayout
{
public static PropertyMapper Mapper =
new PropertyMapper(VisualElementRendererMapper);
@@ -62,6 +62,33 @@ public ListViewRenderer() : base(Mapper, CommandMapper)
AutoPackage = false;
}
+ bool _pendingSuperViewSetNeedsLayout;
+
+ public override void SetNeedsLayout()
+ {
+ base.SetNeedsLayout();
+
+ if (Window is not null)
+ {
+ _pendingSuperViewSetNeedsLayout = false;
+ Superview?.SetNeedsLayout();
+ }
+ else {
+ _pendingSuperViewSetNeedsLayout = true;
+ }
+ }
+
+ public override void MovedToWindow()
+ {
+ base.MovedToWindow();
+ if (_pendingSuperViewSetNeedsLayout)
+ {
+ Superview?.SetNeedsLayout();
+ }
+
+ _pendingSuperViewSetNeedsLayout = false;
+ }
+
public override void LayoutSubviews()
{
base.LayoutSubviews();
diff --git a/src/Controls/src/Core/Compatibility/Handlers/TableView/iOS/TableViewRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/TableView/iOS/TableViewRenderer.cs
index b859af8cd439..8c9805d85c1f 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/TableView/iOS/TableViewRenderer.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/TableView/iOS/TableViewRenderer.cs
@@ -9,7 +9,7 @@
namespace Microsoft.Maui.Controls.Handlers.Compatibility
{
- public class TableViewRenderer : ViewRenderer
+ public class TableViewRenderer : ViewRenderer, IPropagatesSetNeedsLayout
{
const int DefaultRowHeight = 44;
UIView _originalBackgroundView;
@@ -33,6 +33,33 @@ public override void LayoutSubviews()
_previousFrame = Frame;
}
+ bool _pendingSuperViewSetNeedsLayout;
+
+ public override void SetNeedsLayout()
+ {
+ base.SetNeedsLayout();
+
+ if (Window is not null)
+ {
+ _pendingSuperViewSetNeedsLayout = false;
+ Superview?.SetNeedsLayout();
+ }
+ else {
+ _pendingSuperViewSetNeedsLayout = true;
+ }
+ }
+
+ public override void MovedToWindow()
+ {
+ base.MovedToWindow();
+ if (_pendingSuperViewSetNeedsLayout)
+ {
+ Superview?.SetNeedsLayout();
+ }
+
+ _pendingSuperViewSetNeedsLayout = false;
+ }
+
protected override void Dispose(bool disposing)
{
if (disposing)
diff --git a/src/Controls/src/Core/Compatibility/Handlers/iOS/FrameRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/iOS/FrameRenderer.cs
index 76b7a3d43c4d..887d91e76cf1 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/iOS/FrameRenderer.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/iOS/FrameRenderer.cs
@@ -7,7 +7,7 @@
namespace Microsoft.Maui.Controls.Handlers.Compatibility
{
- public class FrameRenderer : VisualElementRenderer
+ public class FrameRenderer : VisualElementRenderer, IPropagatesSetNeedsLayout
{
public static IPropertyMapper Mapper
= new PropertyMapper(VisualElementRendererMapper);
diff --git a/src/Controls/src/Core/Internals/InvalidationTriggerFlags.cs b/src/Controls/src/Core/Internals/InvalidationTriggerFlags.cs
new file mode 100644
index 000000000000..f09843ff806e
--- /dev/null
+++ b/src/Controls/src/Core/Internals/InvalidationTriggerFlags.cs
@@ -0,0 +1,43 @@
+using System;
+
+namespace Microsoft.Maui.Controls.Internals;
+
+[Flags]
+internal enum InvalidationTriggerFlags : ushort
+{
+ None = 0,
+ ApplyingBindingContext = 1 << 0,
+ WillNotifyParentMeasureInvalidated = 1 << 1,
+ WillTriggerHorizontalOptionsChanged = 1 << 2,
+ WillTriggerVerticalOptionsChanged = 1 << 3,
+ WillTriggerMarginChanged = 1 << 4,
+ WillTriggerSizeRequestChanged = 1 << 5,
+ WillTriggerMeasureChanged = 1 << 6,
+ WillTriggerRendererReady = 1 << 7,
+ WillTriggerUndefined = 1 << 8,
+}
+
+internal static class InvalidationTriggerFlagsExtensions {
+ public static InvalidationTriggerFlags ToInvalidationTriggerFlags(this InvalidationTrigger trigger) {
+ return trigger switch {
+ InvalidationTrigger.MeasureChanged => InvalidationTriggerFlags.WillTriggerMeasureChanged,
+ InvalidationTrigger.HorizontalOptionsChanged => InvalidationTriggerFlags.WillTriggerHorizontalOptionsChanged,
+ InvalidationTrigger.VerticalOptionsChanged => InvalidationTriggerFlags.WillTriggerVerticalOptionsChanged,
+ InvalidationTrigger.SizeRequestChanged => InvalidationTriggerFlags.WillTriggerSizeRequestChanged,
+ InvalidationTrigger.RendererReady => InvalidationTriggerFlags.WillTriggerRendererReady,
+ InvalidationTrigger.MarginChanged => InvalidationTriggerFlags.WillTriggerMarginChanged,
+ _ => InvalidationTriggerFlags.WillTriggerUndefined,
+ };
+ }
+
+ public static InvalidationTrigger ToInvalidationTrigger(this InvalidationTriggerFlags flags) {
+ if ((flags & InvalidationTriggerFlags.WillTriggerUndefined) != 0) return InvalidationTrigger.Undefined;
+ if ((flags & InvalidationTriggerFlags.WillTriggerRendererReady) != 0) return InvalidationTrigger.RendererReady;
+ if ((flags & InvalidationTriggerFlags.WillTriggerMeasureChanged) != 0) return InvalidationTrigger.MeasureChanged;
+ if ((flags & InvalidationTriggerFlags.WillTriggerSizeRequestChanged) != 0) return InvalidationTrigger.SizeRequestChanged;
+ if ((flags & InvalidationTriggerFlags.WillTriggerMarginChanged) != 0) return InvalidationTrigger.MarginChanged;
+ if ((flags & InvalidationTriggerFlags.WillTriggerVerticalOptionsChanged) != 0) return InvalidationTrigger.VerticalOptionsChanged;
+ if ((flags & InvalidationTriggerFlags.WillTriggerHorizontalOptionsChanged) != 0) return InvalidationTrigger.HorizontalOptionsChanged;
+ return InvalidationTrigger.Undefined;
+ }
+}
\ 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 0f3b6d0b0d7f..c254976db9df 100644
--- a/src/Controls/src/Core/LegacyLayouts/Layout.cs
+++ b/src/Controls/src/Core/LegacyLayouts/Layout.cs
@@ -192,8 +192,12 @@ private protected override IList LogicalChildrenInternalBackingStore
public override SizeRequest Measure(double widthConstraint, double heightConstraint, MeasureFlags flags = MeasureFlags.None)
{
SizeRequest size = base.Measure(widthConstraint - Padding.HorizontalThickness, heightConstraint - Padding.VerticalThickness, flags);
- 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);
}
///
@@ -294,13 +298,19 @@ public void RaiseChild(View view)
OnChildrenReordered();
}
+ internal virtual void InvalidateLayoutInternal()
+ {
+ _hasDoneLayout = false;
+ InvalidateMeasureCacheInternal();
+ }
+
///
/// Invalidates the current layout.
///
/// Calling this method will invalidate the measure and triggers a new layout cycle.
protected virtual void InvalidateLayout()
{
- _hasDoneLayout = false;
+ InvalidateLayoutInternal();
InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
if (!_hasDoneLayout)
{
@@ -319,10 +329,15 @@ protected virtual void InvalidateLayout()
/// It is suggested to still call the base method and modify its calculated results.
protected abstract void LayoutChildren(double x, double y, double width, double height);
- internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger)
+ internal override bool OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger)
{
- // TODO: once we remove old Xamarin public signatures we can invoke `OnChildMeasureInvalidated(VisualElement, InvalidationTrigger)` directly
- OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger));
+ if (base.OnChildMeasureInvalidatedInternal(child, trigger))
+ {
+ OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger));
+ return true;
+ }
+
+ return false;
}
///
@@ -334,8 +349,7 @@ 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)
{
- InvalidationTrigger trigger = (e as InvalidationEventArgs)?.Trigger ?? InvalidationTrigger.Undefined;
- OnChildMeasureInvalidated((VisualElement)sender, trigger);
+ InvalidateLayoutInternal();
OnChildMeasureInvalidated();
}
@@ -497,42 +511,6 @@ internal static void LayoutChildIntoBoundingRegion(View child, Rect region, Size
child.Layout(region);
}
- internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger)
- {
- 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);
- }
- }
-
- if (trigger == InvalidationTrigger.RendererReady)
- {
- InvalidateMeasureInternal(InvalidationTrigger.RendererReady);
- }
- else
- {
- InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
- }
- }
-
internal override void OnIsVisibleChanged(bool oldValue, bool newValue)
{
base.OnIsVisibleChanged(oldValue, newValue);
@@ -645,15 +623,8 @@ bool ShouldLayoutChildren()
protected override void InvalidateMeasureOverride()
{
+ InvalidateLayoutInternal();
base.InvalidateMeasureOverride();
-
- foreach (var child in ((IElementController)this).LogicalChildren)
- {
- if (child is IView fe)
- {
- fe.InvalidateMeasure();
- }
- }
}
protected override Size ArrangeOverride(Rect bounds)
diff --git a/src/Controls/src/Core/LegacyLayouts/StackLayout.cs b/src/Controls/src/Core/LegacyLayouts/StackLayout.cs
index db341b2df4d4..c2aa1e9f94fe 100644
--- a/src/Controls/src/Core/LegacyLayouts/StackLayout.cs
+++ b/src/Controls/src/Core/LegacyLayouts/StackLayout.cs
@@ -92,10 +92,16 @@ internal override void ComputeConstraintForView(View view)
ComputeConstraintForView(view, false);
}
- internal override void InvalidateMeasureInternal(InvalidationTrigger trigger)
+ internal override void InvalidateLayoutInternal()
{
+ base.InvalidateLayoutInternal();
_layoutInformation = new LayoutInformation();
- base.InvalidateMeasureInternal(trigger);
+ }
+
+ internal override void InvalidateMeasureInternal(InvalidationTriggerFlags flags)
+ {
+ InvalidateLayoutInternal();
+ base.InvalidateMeasureInternal(flags);
}
void AlignOffAxis(LayoutInformation layout, StackOrientation orientation, double widthConstraint, double heightConstraint)
diff --git a/src/Controls/src/Core/Page/Page.cs b/src/Controls/src/Core/Page/Page.cs
index 53b083d64504..2339a15fd27c 100644
--- a/src/Controls/src/Core/Page/Page.cs
+++ b/src/Controls/src/Core/Page/Page.cs
@@ -497,10 +497,44 @@ protected override void OnBindingContextChanged()
SetInheritedBindingContext(TitleView, BindingContext);
}
- internal override void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger)
+ internal override bool OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger)
{
- // TODO: once we remove old Xamarin public signatures we can invoke `OnChildMeasureInvalidated(VisualElement, InvalidationTrigger)` directly
+ // Behave like `VisualElement` except for propagation to parent
+ switch (trigger)
+ {
+ case InvalidationTrigger.Undefined:
+ if (IsApplyingBindings)
+ {
+ // If we're applying bindings, we need to wait until it's done to invalidate the measure
+ MeasureInvalidationStatus |= InvalidationTriggerFlags.WillNotifyParentMeasureInvalidated;
+ return false;
+ }
+
+ InvokeMeasureInvalidated(InvalidationTrigger.MeasureChanged);
+ break;
+
+ default:
+ // When visibility changes `InvalidationTrigger.Undefined` is used,
+ // so here we're sure that visibility didn't change
+ if (child.IsVisible)
+ {
+ if (IsApplyingBindings)
+ {
+ // If we're applying bindings, we need to wait until it's done to invalidate the measure
+ MeasureInvalidationStatus |= InvalidationTriggerFlags.WillNotifyParentMeasureInvalidated;
+ return false;
+ }
+
+ // We need to invalidate measures only if child is actually visible
+ InvokeMeasureInvalidated(InvalidationTrigger.MeasureChanged);
+ }
+ break;
+ }
+
+ // We still need to call the legacy OnChildMeasureInvalidated to keep the compatibility.
OnChildMeasureInvalidated(child, new InvalidationEventArgs(trigger));
+
+ return true;
}
///
@@ -510,8 +544,7 @@ internal override void OnChildMeasureInvalidatedInternal(VisualElement child, In
/// The event arguments.
protected virtual void OnChildMeasureInvalidated(object sender, EventArgs e)
{
- InvalidationTrigger trigger = (e as InvalidationEventArgs)?.Trigger ?? InvalidationTrigger.Undefined;
- OnChildMeasureInvalidated((VisualElement)sender, trigger);
+ // Nothing to do here: platform will take care of arranging the children if needed on the next layout pass
}
///
@@ -583,29 +616,6 @@ protected void UpdateChildrenLayout()
}
}
- internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger)
- {
- 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;
- }
- }
-
- InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
- }
-
internal void OnAppearing(Action action)
{
if (_hasAppeared)
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 f8ec2fad5146..466ec2bb0686 100644
--- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt
@@ -1,2 +1,6 @@
#nullable enable
override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.MovedToWindow() -> void
+override Microsoft.Maui.Controls.Handlers.Compatibility.ListViewRenderer.MovedToWindow() -> void
+override Microsoft.Maui.Controls.Handlers.Compatibility.ListViewRenderer.SetNeedsLayout() -> void
+override Microsoft.Maui.Controls.Handlers.Compatibility.TableViewRenderer.MovedToWindow() -> void
+override Microsoft.Maui.Controls.Handlers.Compatibility.TableViewRenderer.SetNeedsLayout() -> 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 182b9e67b45a..910585c9d0e9 100644
--- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
@@ -1,2 +1,6 @@
#nullable enable
-override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.MovedToWindow() -> void
\ No newline at end of file
+override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.MovedToWindow() -> void
+override Microsoft.Maui.Controls.Handlers.Compatibility.ListViewRenderer.MovedToWindow() -> void
+override Microsoft.Maui.Controls.Handlers.Compatibility.ListViewRenderer.SetNeedsLayout() -> void
+override Microsoft.Maui.Controls.Handlers.Compatibility.TableViewRenderer.MovedToWindow() -> void
+override Microsoft.Maui.Controls.Handlers.Compatibility.TableViewRenderer.SetNeedsLayout() -> void
\ No newline at end of file
diff --git a/src/Controls/src/Core/VisualElement/VisualElement.cs b/src/Controls/src/Core/VisualElement/VisualElement.cs
index 24946eb7c3d5..56a3ad70adad 100644
--- a/src/Controls/src/Core/VisualElement/VisualElement.cs
+++ b/src/Controls/src/Core/VisualElement/VisualElement.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
+using System.Runtime.CompilerServices;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Controls.Shapes;
using Microsoft.Maui.Graphics;
@@ -919,6 +920,12 @@ public Geometry Clip
[EditorBrowsable(EditorBrowsableState.Never)]
public bool Batched => _batched > 0;
+ ///
+ /// Legacy s can force flags on the element.
+ ///
+ ///
+ /// This should be removed once we drop the legacy layout system, unless we decide to implement a similar functionality in the new layout system.
+ ///
internal LayoutConstraint ComputedConstraint
{
get { return _computedConstraint; }
@@ -935,6 +942,9 @@ internal LayoutConstraint ComputedConstraint
}
}
+ ///
+ /// This flag is substantially used by to avoid measure invalidation when the view is .
+ ///
internal LayoutConstraint Constraint => ComputedConstraint | SelfConstraint;
///
@@ -991,7 +1001,7 @@ public bool IsPlatformStateConsistent
internal event EventHandler PlatformEnabledChanged;
///
- /// Gets or sets a value that indicates whether this elements's platform equivalent element is enabled.
+ /// Gets or sets a value that indicates whether this element's platform equivalent element is enabled.
///
/// For internal use only. This API can be changed or removed without notice at any time.
[EditorBrowsable(EditorBrowsableState.Never)]
@@ -1354,56 +1364,146 @@ public void InvalidateMeasureNonVirtual(InvalidationTrigger trigger)
{
InvalidateMeasureInternal(trigger);
}
+
+ internal void InvokeMeasureInvalidated(InvalidationTrigger trigger)
+ {
+ MeasureInvalidated?.Invoke(this, new InvalidationEventArgs(trigger));
+ }
- internal virtual void InvalidateMeasureInternal(InvalidationTrigger trigger)
+ private protected InvalidationTriggerFlags MeasureInvalidationStatus;
+ private protected bool IsApplyingBindings => (MeasureInvalidationStatus & InvalidationTriggerFlags.ApplyingBindingContext) != 0;
+
+ private protected override void ApplyBindingsFromBindingContextChanged()
{
- _measureCache.Clear();
+ try
+ {
+ MeasureInvalidationStatus |= InvalidationTriggerFlags.ApplyingBindingContext;
+ base.ApplyBindingsFromBindingContextChanged();
+ }
+ finally
+ {
+ var status = MeasureInvalidationStatus;
+ MeasureInvalidationStatus = InvalidationTriggerFlags.None;
+ if (status > InvalidationTriggerFlags.ApplyingBindingContext)
+ {
+ InvalidateMeasureInternal(status);
+ }
+ }
+ }
- // TODO ezhart Once we get InvalidateArrange sorted, HorizontalOptionsChanged and
- // VerticalOptionsChanged will need to call ParentView.InvalidateArrange() instead
+ internal virtual void InvalidateMeasureInternal(InvalidationTriggerFlags flags)
+ {
+ var parentVisualElement = Parent as VisualElement;
- switch (trigger)
+ const InvalidationTriggerFlags notifyOnly = InvalidationTriggerFlags.WillNotifyParentMeasureInvalidated |
+ InvalidationTriggerFlags.ApplyingBindingContext;
+
+ // While applying bindings we only received `OnChildMeasureInvalidatedInternal` calls, so just propagate one of them
+ if (flags <= notifyOnly)
+ {
+ InvokeMeasureInvalidated(InvalidationTrigger.MeasureChanged);
+ // Notify parent chain that a child's measure has been invalidated
+ parentVisualElement?.OnChildMeasureInvalidatedInternal(this, InvalidationTrigger.MeasureChanged);
+ return;
+ }
+
+ const InvalidationTriggerFlags positionOptionsChanged = InvalidationTriggerFlags.WillTriggerHorizontalOptionsChanged | InvalidationTriggerFlags.WillTriggerVerticalOptionsChanged;
+
+ // If position options changed, we need to recompute constraints
+ if ((flags & positionOptionsChanged) != 0 && parentVisualElement != null && this is View thisView)
+ {
+ parentVisualElement.ComputeConstraintForView(thisView);
+ }
+
+ var invalidationTrigger = flags.ToInvalidationTrigger();
+
+ // If this view's measure has not been invalidated, we can trigger the invalidation on parent only
+ if (flags < InvalidationTriggerFlags.WillTriggerSizeRequestChanged)
{
- case InvalidationTrigger.MarginChanged:
- case InvalidationTrigger.HorizontalOptionsChanged:
- case InvalidationTrigger.VerticalOptionsChanged:
+ InvokeMeasureInvalidated(invalidationTrigger);
+
+ // Trigger parent measure invalidation
+ if (parentVisualElement is not null)
+ {
+ parentVisualElement.InvalidateMeasure();
+ }
+ else
+ {
+ // This should never happen, but just in case, the only thing we can do is invalidate the platform view
ParentView?.InvalidateMeasure();
- break;
- default:
- (this as IView)?.InvalidateMeasure();
- break;
+ }
+ return;
}
- MeasureInvalidated?.Invoke(this, new InvalidationEventArgs(trigger));
- (Parent as VisualElement)?.OnChildMeasureInvalidatedInternal(this, trigger);
+ InvalidateMeasureCacheInternal();
+ ((IView)this).InvalidateMeasure();
+ InvokeMeasureInvalidated(invalidationTrigger);
+ // Notify parent chain that a child's measure has been invalidated
+ (Parent as VisualElement)?.OnChildMeasureInvalidatedInternal(this, invalidationTrigger);
}
-
- internal virtual void OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger)
+
+ internal void InvalidateMeasureInternal(InvalidationTrigger trigger)
+ {
+ if (!IsPlatformEnabled)
+ {
+ // No need to invalidate measure if there's no platform view
+ return;
+ }
+
+ if (IsApplyingBindings)
+ {
+ // If we're applying bindings, we need to wait until it's done to invalidate the measure
+ MeasureInvalidationStatus |= trigger.ToInvalidationTriggerFlags();
+ return;
+ }
+
+ InvalidateMeasureInternal(trigger.ToInvalidationTriggerFlags());
+ }
+
+ ///
+ /// Clears the measure cache of a visual element.
+ ///
+ internal void InvalidateMeasureCacheInternal()
+ {
+ _measureCache.Clear();
+ }
+
+ internal virtual bool OnChildMeasureInvalidatedInternal(VisualElement child, InvalidationTrigger trigger)
{
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:
- MeasureInvalidated?.Invoke(this, new InvalidationEventArgs(trigger));
- (Parent as VisualElement)?.OnChildMeasureInvalidatedInternal(this, trigger);
- return;
+ if (IsApplyingBindings)
+ {
+ // If we're applying bindings, we need to wait until it's done to invalidate the measure
+ MeasureInvalidationStatus |= InvalidationTriggerFlags.WillNotifyParentMeasureInvalidated;
+ return false;
+ }
+
+ InvokeMeasureInvalidated(InvalidationTrigger.MeasureChanged);
+ (Parent as VisualElement)?.OnChildMeasureInvalidatedInternal(this, InvalidationTrigger.MeasureChanged);
+ break;
+
default:
// When visibility changes `InvalidationTrigger.Undefined` is used,
// so here we're sure that visibility didn't change
if (child.IsVisible)
{
// We need to invalidate measures only if child is actually visible
- MeasureInvalidated?.Invoke(this, new InvalidationEventArgs(InvalidationTrigger.MeasureChanged));
+ if (IsApplyingBindings)
+ {
+ // If we're applying bindings, we need to wait until it's done to invalidate the measure
+ MeasureInvalidationStatus |= InvalidationTriggerFlags.WillNotifyParentMeasureInvalidated;
+ return false;
+ }
+
+ InvokeMeasureInvalidated(InvalidationTrigger.MeasureChanged);
(Parent as VisualElement)?.OnChildMeasureInvalidatedInternal(this, InvalidationTrigger.MeasureChanged);
}
- return;
+ break;
}
+
+ return true;
}
///
diff --git a/src/Controls/tests/Core.UnitTests/ButtonUnitTest.cs b/src/Controls/tests/Core.UnitTests/ButtonUnitTest.cs
index f5ffd00a3d8d..154bd8508078 100644
--- a/src/Controls/tests/Core.UnitTests/ButtonUnitTest.cs
+++ b/src/Controls/tests/Core.UnitTests/ButtonUnitTest.cs
@@ -10,7 +10,7 @@ public class ButtonUnitTest : VisualElementCommandSourceTests