diff --git a/src/Controls/src/Core/Label/Label.Mapper.cs b/src/Controls/src/Core/Label/Label.Mapper.cs index 9759653dcea4..50836bb7a6fd 100644 --- a/src/Controls/src/Core/Label/Label.Mapper.cs +++ b/src/Controls/src/Core/Label/Label.Mapper.cs @@ -21,7 +21,7 @@ public partial class Label // these are really a single property LabelHandler.Mapper.ReplaceMapping(nameof(Text), MapText); - LabelHandler.Mapper.ReplaceMapping(nameof(FormattedText), MapText); + LabelHandler.Mapper.ReplaceMapping(nameof(FormattedText), MapFormattedText); LabelHandler.Mapper.ReplaceMapping(nameof(LineBreakMode), MapLineBreakMode); LabelHandler.Mapper.ReplaceMapping(nameof(MaxLines), MapMaxLines); @@ -54,8 +54,17 @@ public static void MapTextType(ILabelHandler handler, Label label) => MapTextOrFormattedText(handler, label); static void MapTextTransform(ILabelHandler handler, Label label) => MapTextOrFormattedText(handler, label); + static void MapFormattedText(ILabelHandler handler, Label label) + { + if (label.IsConnectingHandler()) return; + + MapText(handler, label); + } + static void MapTextOrFormattedText(ILabelHandler handler, Label label) { + if (label.IsConnectingHandler()) return; + if (label.HasFormattedTextSpans) handler.UpdateValue(nameof(FormattedText)); else diff --git a/src/Core/src/Core/Extensions/InternalElementExtensions.cs b/src/Core/src/Core/Extensions/InternalElementExtensions.cs new file mode 100644 index 000000000000..ed85135ed76e --- /dev/null +++ b/src/Core/src/Core/Extensions/InternalElementExtensions.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Maui +{ + internal static class InternalElementExtensions + { + /// + /// The handler is connecting for the first time to the element and mapping all properties. + /// + /// + /// + internal static bool IsMappingProperties(this IElement element) => + (element.Handler as IElementHandlerStateExhibitor)?.State.HasFlag(ElementHandlerState.MappingProperties) ?? false; + + /// + /// Indicates whether the handler is connecting for the first time to the element and mapping all properties. + /// + /// + /// + internal static bool IsConnectingHandler(this IElement element) => + (element.Handler as IElementHandlerStateExhibitor)?.State.HasFlag(ElementHandlerState.Connecting) ?? false; + + /// + /// Indicates whether the connected handler is now connecting to a new element and updating properties. + /// + /// + /// + internal static bool IsReconnectingHandler(this IElement element) => + (element.Handler as IElementHandlerStateExhibitor)?.State.HasFlag(ElementHandlerState.Reconnecting) ?? false; + } +} diff --git a/src/Core/src/Handlers/Border/BorderHandler.cs b/src/Core/src/Handlers/Border/BorderHandler.cs index 2ef2fd0a26fa..9f8e6b339a0b 100644 --- a/src/Core/src/Handlers/Border/BorderHandler.cs +++ b/src/Core/src/Handlers/Border/BorderHandler.cs @@ -86,6 +86,16 @@ public static void MapBackground(IBorderHandler handler, IBorderView border) ((PlatformView?)handler.PlatformView)?.UpdateBackground(border); } + private static bool ShouldSkipStrokeMappings(IBorderHandler handler) { +#if __IOS__ || MACCATALYST || ANDROID + // During the initial connection, the `MapBackground` takes care of updating the stroke properties + // so we can skip the stroke mappings to avoid repetitive and useless updates. + return handler.IsConnectingHandler(); +#else + return false; +#endif + } + /// /// Maps the abstract property to the platform-specific implementations. /// @@ -93,6 +103,8 @@ public static void MapBackground(IBorderHandler handler, IBorderView border) /// The associated instance. public static void MapStrokeShape(IBorderHandler handler, IBorderView border) { + if (ShouldSkipStrokeMappings(handler)) return; + ((PlatformView?)handler.PlatformView)?.UpdateStrokeShape(border); MapBackground(handler, border); } @@ -104,6 +116,8 @@ public static void MapStrokeShape(IBorderHandler handler, IBorderView border) /// The associated instance. public static void MapStroke(IBorderHandler handler, IBorderView border) { + if (ShouldSkipStrokeMappings(handler)) return; + ((PlatformView?)handler.PlatformView)?.UpdateStroke(border); MapBackground(handler, border); } @@ -115,6 +129,8 @@ public static void MapStroke(IBorderHandler handler, IBorderView border) /// The associated instance. public static void MapStrokeThickness(IBorderHandler handler, IBorderView border) { + if (ShouldSkipStrokeMappings(handler)) return; + ((PlatformView?)handler.PlatformView)?.UpdateStrokeThickness(border); MapBackground(handler, border); } @@ -126,6 +142,8 @@ public static void MapStrokeThickness(IBorderHandler handler, IBorderView border /// The associated instance. public static void MapStrokeLineCap(IBorderHandler handler, IBorderView border) { + if (ShouldSkipStrokeMappings(handler)) return; + ((PlatformView?)handler.PlatformView)?.UpdateStrokeLineCap(border); } @@ -136,6 +154,8 @@ public static void MapStrokeLineCap(IBorderHandler handler, IBorderView border) /// The associated instance. public static void MapStrokeLineJoin(IBorderHandler handler, IBorderView border) { + if (ShouldSkipStrokeMappings(handler)) return; + ((PlatformView?)handler.PlatformView)?.UpdateStrokeLineJoin(border); } @@ -146,6 +166,8 @@ public static void MapStrokeLineJoin(IBorderHandler handler, IBorderView border) /// The associated instance. public static void MapStrokeDashPattern(IBorderHandler handler, IBorderView border) { + if (ShouldSkipStrokeMappings(handler)) return; + ((PlatformView?)handler.PlatformView)?.UpdateStrokeDashPattern(border); } @@ -156,6 +178,8 @@ public static void MapStrokeDashPattern(IBorderHandler handler, IBorderView bord /// The associated instance. public static void MapStrokeDashOffset(IBorderHandler handler, IBorderView border) { + if (ShouldSkipStrokeMappings(handler)) return; + ((PlatformView?)handler.PlatformView)?.UpdateStrokeDashOffset(border); } @@ -166,6 +190,8 @@ public static void MapStrokeDashOffset(IBorderHandler handler, IBorderView borde /// The associated instance. public static void MapStrokeMiterLimit(IBorderHandler handler, IBorderView border) { + if (ShouldSkipStrokeMappings(handler)) return; + ((PlatformView?)handler.PlatformView)?.UpdateStrokeMiterLimit(border); } diff --git a/src/Core/src/Handlers/Element/ElementHandler.cs b/src/Core/src/Handlers/Element/ElementHandler.cs index 47ef3fcd97a1..aa56596039cd 100644 --- a/src/Core/src/Handlers/Element/ElementHandler.cs +++ b/src/Core/src/Handlers/Element/ElementHandler.cs @@ -2,7 +2,7 @@ namespace Microsoft.Maui.Handlers { - public abstract partial class ElementHandler : IElementHandler + public abstract partial class ElementHandler : IElementHandler, IElementHandlerStateExhibitor { public static IPropertyMapper ElementMapper = new PropertyMapper() { @@ -15,6 +15,9 @@ public abstract partial class ElementHandler : IElementHandler internal readonly IPropertyMapper _defaultMapper; internal readonly CommandMapper? _commandMapper; internal IPropertyMapper _mapper; + ElementHandlerState _handlerState; + + ElementHandlerState IElementHandlerStateExhibitor.State => _handlerState; protected ElementHandler(IPropertyMapper mapper, CommandMapper? commandMapper = null) { @@ -40,24 +43,38 @@ public virtual void SetVirtualView(IElement view) _ = view ?? throw new ArgumentNullException(nameof(view)); if (VirtualView == view) + { return; + } var oldVirtualView = VirtualView; bool setupPlatformView = oldVirtualView == null; VirtualView = view; - PlatformView ??= CreatePlatformElement(); + if (PlatformView is null) + { + _handlerState = ElementHandlerState.Connecting; + PlatformView = CreatePlatformElement(); + } + else + { + _handlerState = ElementHandlerState.Reconnecting; + } if (VirtualView.Handler != this) + { VirtualView.Handler = this; + } // We set the previous virtual view to null after setting it on the incoming virtual view. // This makes it easier for the incoming virtual view to have influence // on how the exchange of handlers happens. // We will just set the handler to null ourselves as a last resort cleanup if (oldVirtualView?.Handler != null) + { oldVirtualView.Handler = null; + } if (setupPlatformView) { @@ -77,6 +94,8 @@ public virtual void SetVirtualView(IElement view) } _mapper.UpdateProperties(this, VirtualView); + + _handlerState = ElementHandlerState.Connected; } public virtual void UpdateValue(string property) @@ -129,6 +148,8 @@ void IElementHandler.DisconnectHandler() PlatformView = null; DisconnectHandler(oldPlatformView); } + + _handlerState = ElementHandlerState.Disconnected; } } } diff --git a/src/Core/src/Handlers/ElementHandlerState.cs b/src/Core/src/Handlers/ElementHandlerState.cs new file mode 100644 index 000000000000..d213f7afac73 --- /dev/null +++ b/src/Core/src/Handlers/ElementHandlerState.cs @@ -0,0 +1,32 @@ +using System; + +namespace Microsoft.Maui +{ + /// + /// Exposes the state of an element handler. + /// + [Flags] + internal enum ElementHandlerState : byte + { + /// + /// The handler is not connected to an element. + /// + Disconnected = 0x0, + /// + /// The handler is mapping all properties to the element. + /// + MappingProperties = 0x1, + /// + /// The handler is connecting for the first time to the element and mapping all properties. + /// + Connecting = MappingProperties | 0x2, + /// + /// The connected handler is now connecting to a new element and updating properties. + /// + Reconnecting = MappingProperties | 0x4, + /// + /// The handler is connected to an element. + /// + Connected = 0x8 + } +} diff --git a/src/Core/src/Handlers/IElementHandlerStateExhibitor.cs b/src/Core/src/Handlers/IElementHandlerStateExhibitor.cs new file mode 100644 index 000000000000..665cb5dfbd64 --- /dev/null +++ b/src/Core/src/Handlers/IElementHandlerStateExhibitor.cs @@ -0,0 +1,16 @@ +namespace Microsoft.Maui +{ + /// + /// Exposes the state of an element handler. + /// + /// + /// To be migrated to a public API. + /// + internal interface IElementHandlerStateExhibitor + { + /// + /// Gets the state of the element handler. + /// + ElementHandlerState State { get; } + } +} \ No newline at end of file diff --git a/src/Core/src/Handlers/InternalElementHandlerExtensions.cs b/src/Core/src/Handlers/InternalElementHandlerExtensions.cs new file mode 100644 index 000000000000..11548714e3f5 --- /dev/null +++ b/src/Core/src/Handlers/InternalElementHandlerExtensions.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Maui +{ + internal static class InternalElementHandlerExtensions + { + /// + /// The handler is connecting for the first time to the element and mapping all properties. + /// + /// + /// + internal static bool IsMappingProperties(this IElementHandler handler) => + (handler as IElementHandlerStateExhibitor)?.State.HasFlag(ElementHandlerState.MappingProperties) ?? false; + + /// + /// Indicates whether the handler is connecting for the first time to the element and mapping all properties. + /// + /// + /// + internal static bool IsConnectingHandler(this IElementHandler handler) => + (handler as IElementHandlerStateExhibitor)?.State.HasFlag(ElementHandlerState.Connecting) ?? false; + + /// + /// Indicates whether the connected handler is now connecting to a new element and updating properties. + /// + /// + /// + internal static bool IsReconnectingHandler(this IElementHandler handler) => + (handler as IElementHandlerStateExhibitor)?.State.HasFlag(ElementHandlerState.Reconnecting) ?? false; + } +} diff --git a/src/Core/src/Handlers/View/ViewHandler.Windows.cs b/src/Core/src/Handlers/View/ViewHandler.Windows.cs index 159757c930d1..7b5d29e65bfc 100644 --- a/src/Core/src/Handlers/View/ViewHandler.Windows.cs +++ b/src/Core/src/Handlers/View/ViewHandler.Windows.cs @@ -112,6 +112,8 @@ public static void MapContextFlyout(IViewHandler handler, IView view) { if (view is IContextFlyoutElement contextFlyoutContainer) { + if (handler.IsConnectingHandler() && contextFlyoutContainer.ContextFlyout is null) return; + MapContextFlyout(handler, contextFlyoutContainer); } } diff --git a/src/Core/src/Handlers/View/ViewHandler.cs b/src/Core/src/Handlers/View/ViewHandler.cs index c2dbc56e153c..01e644235a19 100644 --- a/src/Core/src/Handlers/View/ViewHandler.cs +++ b/src/Core/src/Handlers/View/ViewHandler.cs @@ -31,6 +31,9 @@ public abstract partial class ViewHandler : ElementHandler, IViewHandler new PropertyMapper(ElementHandler.ElementMapper) #endif { + // This property is a special one and needs to be set before other properties. + [nameof(IViewHandler.ContainerView)] = MapContainerView, + [nameof(IView.AutomationId)] = MapAutomationId, [nameof(IView.Clip)] = MapClip, [nameof(IView.Shadow)] = MapShadow, @@ -56,7 +59,6 @@ public abstract partial class ViewHandler : ElementHandler, IViewHandler [nameof(IView.RotationY)] = MapRotationY, [nameof(IView.AnchorX)] = MapAnchorX, [nameof(IView.AnchorY)] = MapAnchorY, - [nameof(IViewHandler.ContainerView)] = MapContainerView, #pragma warning disable CS0618 // Type or member is obsolete [nameof(IBorder.Border)] = MapBorderView, #pragma warning restore CS0618 // Type or member is obsolete @@ -244,6 +246,8 @@ public static void MapHeight(IViewHandler handler, IView view) /// The associated instance. public static void MapMinimumHeight(IViewHandler handler, IView view) { + if (handler.IsConnectingHandler() && double.IsNaN(view.MinimumHeight)) return; + ((PlatformView?)handler.PlatformView)?.UpdateMinimumHeight(view); } @@ -254,6 +258,8 @@ public static void MapMinimumHeight(IViewHandler handler, IView view) /// The associated instance. public static void MapMaximumHeight(IViewHandler handler, IView view) { + if (handler.IsConnectingHandler() && double.IsNaN(view.MaximumHeight)) return; + ((PlatformView?)handler.PlatformView)?.UpdateMaximumHeight(view); } @@ -264,6 +270,8 @@ public static void MapMaximumHeight(IViewHandler handler, IView view) /// The associated instance. public static void MapMinimumWidth(IViewHandler handler, IView view) { + if (handler.IsConnectingHandler() && double.IsNaN(view.MinimumWidth)) return; + ((PlatformView?)handler.PlatformView)?.UpdateMinimumWidth(view); } @@ -274,6 +282,8 @@ public static void MapMinimumWidth(IViewHandler handler, IView view) /// The associated instance. public static void MapMaximumWidth(IViewHandler handler, IView view) { + if (handler.IsConnectingHandler() && double.IsNaN(view.MaximumWidth)) return; + ((PlatformView?)handler.PlatformView)?.UpdateMaximumWidth(view); } @@ -294,6 +304,8 @@ public static void MapIsEnabled(IViewHandler handler, IView view) /// The associated instance. public static void MapVisibility(IViewHandler handler, IView view) { + if (handler.IsConnectingHandler() && view.Visibility == Visibility.Visible) return; + if (handler.HasContainer) ((PlatformView?)handler.ContainerView)?.UpdateVisibility(view); @@ -330,6 +342,8 @@ public static void MapBackground(IViewHandler handler, IView view) /// The associated instance. public static void MapFlowDirection(IViewHandler handler, IView view) { + if (handler.IsConnectingHandler() && view.FlowDirection == FlowDirection.MatchParent) return; + ((PlatformView?)handler.PlatformView)?.UpdateFlowDirection(view); } @@ -340,6 +354,8 @@ public static void MapFlowDirection(IViewHandler handler, IView view) /// The associated instance. public static void MapOpacity(IViewHandler handler, IView view) { + if (handler.IsConnectingHandler() && view.Opacity == 1) return; + if (handler.HasContainer) { ((PlatformView?)handler.ContainerView)?.UpdateOpacity(view); @@ -357,6 +373,8 @@ public static void MapOpacity(IViewHandler handler, IView view) /// The associated instance. public static void MapAutomationId(IViewHandler handler, IView view) { + if (handler.IsConnectingHandler() && view.AutomationId is null) return; + ((PlatformView?)handler.PlatformView)?.UpdateAutomationId(view); } @@ -367,7 +385,13 @@ public static void MapAutomationId(IViewHandler handler, IView view) /// The associated instance. public static void MapClip(IViewHandler handler, IView view) { - handler.UpdateValue(nameof(IViewHandler.ContainerView)); + if (handler.IsConnectingHandler() && view.Clip is null) return; + + if (!handler.IsMappingProperties()) + { + // ContainerView is already being mapped + handler.UpdateValue(nameof(IViewHandler.ContainerView)); + } ((PlatformView?)handler.ContainerView)?.UpdateClip(view); } @@ -379,7 +403,13 @@ public static void MapClip(IViewHandler handler, IView view) /// The associated instance. public static void MapShadow(IViewHandler handler, IView view) { - handler.UpdateValue(nameof(IViewHandler.ContainerView)); + if (handler.IsConnectingHandler() && view.Shadow is null) return; + + if (!handler.IsMappingProperties()) + { + // ContainerView is already being mapped + handler.UpdateValue(nameof(IViewHandler.ContainerView)); + } ((PlatformView?)handler.ContainerView)?.UpdateShadow(view); } diff --git a/src/Core/src/Handlers/View/ViewHandler.iOS.cs b/src/Core/src/Handlers/View/ViewHandler.iOS.cs index 496ff4431d24..501404384f76 100644 --- a/src/Core/src/Handlers/View/ViewHandler.iOS.cs +++ b/src/Core/src/Handlers/View/ViewHandler.iOS.cs @@ -59,51 +59,81 @@ static partial void MappingFrame(IViewHandler handler, IView view) public static void MapTranslationX(IViewHandler handler, IView view) { + // During the initial setup, MappingFrame will take care of everything + if (handler.IsConnectingHandler()) return; + UpdateTransformation(handler, view); } public static void MapTranslationY(IViewHandler handler, IView view) { + // During the initial setup, MappingFrame will take care of everything + if (handler.IsConnectingHandler()) return; + UpdateTransformation(handler, view); } public static void MapScale(IViewHandler handler, IView view) { + // During the initial setup, MappingFrame will take care of everything + if (handler.IsConnectingHandler()) return; + UpdateTransformation(handler, view); } public static void MapScaleX(IViewHandler handler, IView view) { + // During the initial setup, MappingFrame will take care of everything + if (handler.IsConnectingHandler()) return; + UpdateTransformation(handler, view); } public static void MapScaleY(IViewHandler handler, IView view) { + // During the initial setup, MappingFrame will take care of everything + if (handler.IsConnectingHandler()) return; + UpdateTransformation(handler, view); } public static void MapRotation(IViewHandler handler, IView view) { + // During the initial setup, MappingFrame will take care of everything + if (handler.IsConnectingHandler()) return; + UpdateTransformation(handler, view); } public static void MapRotationX(IViewHandler handler, IView view) { + // During the initial setup, MappingFrame will take care of everything + if (handler.IsConnectingHandler()) return; + UpdateTransformation(handler, view); } public static void MapRotationY(IViewHandler handler, IView view) { + // During the initial setup, MappingFrame will take care of everything + if (handler.IsConnectingHandler()) return; + UpdateTransformation(handler, view); } public static void MapAnchorX(IViewHandler handler, IView view) { + // During the initial setup, MappingFrame will take care of everything + if (handler.IsConnectingHandler()) return; + UpdateTransformation(handler, view); } public static void MapAnchorY(IViewHandler handler, IView view) { + // During the initial setup, MappingFrame will take care of everything + if (handler.IsConnectingHandler()) return; + UpdateTransformation(handler, view); } diff --git a/src/Core/src/Platform/iOS/ContainerViewController.cs b/src/Core/src/Platform/iOS/ContainerViewController.cs index 3b85e030c268..1ff5a1fd2504 100644 --- a/src/Core/src/Platform/iOS/ContainerViewController.cs +++ b/src/Core/src/Platform/iOS/ContainerViewController.cs @@ -68,7 +68,10 @@ public override void LoadView() void LoadPlatformView(IElement view) { - currentPlatformView = _pendingLoadedView ?? CreatePlatformView(view); + var platformView = _pendingLoadedView ?? CreatePlatformView(view); + platformView = platformView.Superview as WrapperView ?? platformView; + + currentPlatformView = platformView; _pendingLoadedView = null; View!.AddSubview(currentPlatformView); diff --git a/src/Core/src/PropertyMapper.cs b/src/Core/src/PropertyMapper.cs index 61bf26b5c92b..e14b519b47c5 100644 --- a/src/Core/src/PropertyMapper.cs +++ b/src/Core/src/PropertyMapper.cs @@ -112,15 +112,20 @@ protected virtual void ClearKeyCache() public virtual IEnumerable GetKeys() { - foreach (var key in _mapper.Keys) - yield return key; - + // We want to retain the initial order of the keys to avoid race conditions + // when a property mapping is overridden by a new instance of property mapper. + // As an example, the container view mapper should always run first. + // Siblings mapper should not have keys intersection. if (Chained is not null) { foreach (var chain in Chained) foreach (var key in chain.GetKeys()) yield return key; } + + // Enqueue any additional keys + foreach (var key in _mapper.Keys) + yield return key; } }