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