diff --git a/src/Core/src/Handlers/View/ViewHandler.cs b/src/Core/src/Handlers/View/ViewHandler.cs index 43b76c1f695c..31093ff28534 100644 --- a/src/Core/src/Handlers/View/ViewHandler.cs +++ b/src/Core/src/Handlers/View/ViewHandler.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; +using System.Linq; using Microsoft.Maui.Graphics; #if __IOS__ || MACCATALYST using PlatformView = UIKit.UIView; @@ -20,10 +23,228 @@ namespace Microsoft.Maui.Handlers /// Handlers are also responsible for instantiating the underlying platform view, and mapping the cross-platform control API to the platform view API. public abstract partial class ViewHandler : ElementHandler, IViewHandler { + class ViewHandlerPropertyMapper : IPropertyMapper, ICanUpdate + { + static IPropertyMapper Base => ElementHandler.ElementMapper; + + public void Add(string key, Action action) + { + throw new NotImplementedException(); + } + + string[]? _keys; + + public IEnumerable GetKeys() => (_keys ??= new[] + { + // This property is a special one and needs to be set before other properties. + nameof(IViewHandler.ContainerView), + nameof(IView.AutomationId), + nameof(IView.Clip), + nameof(IView.Shadow), + nameof(IView.Visibility), + nameof(IView.Background), + nameof(IView.FlowDirection), + nameof(IView.Width), + nameof(IView.Height), + nameof(IView.MinimumHeight), + nameof(IView.MaximumHeight), + nameof(IView.MinimumWidth), + nameof(IView.MaximumWidth), + nameof(IView.IsEnabled), + nameof(IView.Opacity), + nameof(IView.Semantics), + nameof(IView.TranslationX), + nameof(IView.TranslationY), + nameof(IView.Scale), + nameof(IView.ScaleX), + nameof(IView.ScaleY), + nameof(IView.Rotation), + nameof(IView.RotationX), + nameof(IView.RotationY), + nameof(IView.AnchorX), + nameof(IView.AnchorY), +#pragma warning disable CS0618 // Type or member is obsolete + nameof(IBorder.Border), +#pragma warning restore CS0618 // Type or member is obsolete +#if ANDROID || WINDOWS || TIZEN + nameof(IToolbarElement.Toolbar), +#endif + nameof(IView.InputTransparent), + nameof(IToolTipElement.ToolTip), +#if WINDOWS || MACCATALYST + nameof(IContextFlyoutElement.ContextFlyout) +#endif + }).Union(Base.GetKeys()); + + public Action? GetProperty(string key) => throw new NotImplementedException(); + // { + // Action? property = key switch + // { + + // // 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, + // nameof(IView.Visibility) => MapVisibility, + // nameof(IView.Background) => MapBackground, + // nameof(IView.FlowDirection) => MapFlowDirection, + // nameof(IView.Width) => MapWidth, + // nameof(IView.Height) => MapHeight, + // nameof(IView.MinimumHeight) => MapMinimumHeight, + // nameof(IView.MaximumHeight) => MapMaximumHeight, + // nameof(IView.MinimumWidth) => MapMinimumWidth, + // nameof(IView.MaximumWidth) => MapMaximumWidth, + // nameof(IView.IsEnabled) => MapIsEnabled, + // nameof(IView.Opacity) => MapOpacity, + // nameof(IView.Semantics) => MapSemantics, + // nameof(IView.TranslationX) => MapTranslationX, + // nameof(IView.TranslationY) => MapTranslationY, + // nameof(IView.Scale) => MapScale, + // nameof(IView.ScaleX) => MapScaleX, + // nameof(IView.ScaleY) => MapScaleY, + // nameof(IView.Rotation) => MapRotation, + // nameof(IView.RotationX) => MapRotationX, + // nameof(IView.RotationY) => MapRotationY, + // nameof(IView.AnchorX) => MapAnchorX, + // nameof(IView.AnchorY) => MapAnchorY, + // #pragma warning disable CS0618 // Type or member is obsolete + // nameof(IBorder.Border) => MapBorderView, + // #pragma warning restore CS0618 // Type or member is obsolete + // #if ANDROID || WINDOWS || TIZEN + // nameof(IToolbarElement.Toolbar) => MapToolbar, + // #endif + // nameof(IView.InputTransparent) => MapInputTransparent, + // nameof(IToolTipElement.ToolTip) => MapToolTip, + // #if WINDOWS || MACCATALYST + // nameof(IContextFlyoutElement.ContextFlyout) => MapContextFlyout, + // #endif + // _ => null + // }; + // return property is null ? null : new((a, b) => property((IViewHandler)a, (IView)b)); + // } + + public void UpdateProperties(IElementHandler elementHandler, IElement virtualView) + { + if (elementHandler is not IViewHandler viewHandler || virtualView is not IView view) + return; + + // This property is a special one and needs to be set before other properties. + MapContainerView(viewHandler, view); + MapAutomationId(viewHandler, view); + MapClip(viewHandler, view); + MapShadow(viewHandler, view); + MapVisibility(viewHandler, view); + MapBackground(viewHandler, view); + MapFlowDirection(viewHandler, view); + MapWidth(viewHandler, view); + MapHeight(viewHandler, view); + MapMinimumHeight(viewHandler, view); + MapMaximumHeight(viewHandler, view); + MapMinimumWidth(viewHandler, view); + MapMaximumWidth(viewHandler, view); + MapIsEnabled(viewHandler, view); + MapOpacity(viewHandler, view); + MapSemantics(viewHandler, view); + MapTranslationX(viewHandler, view); + MapTranslationY(viewHandler, view); + MapScale(viewHandler, view); + MapScaleX(viewHandler, view); + MapScaleY(viewHandler, view); + MapRotation(viewHandler, view); + MapRotationX(viewHandler, view); + MapRotationY(viewHandler, view); + MapAnchorX(viewHandler, view); + MapAnchorY(viewHandler, view); +#pragma warning disable CS0618 // Type or member is obsolete + MapBorderView(viewHandler, view); +#pragma warning restore CS0618 // Type or member is obsolete +#if ANDROID || WINDOWS || TIZEN + MapToolbar(viewHandler, view); +#endif + MapInputTransparent(viewHandler, view); + MapToolTip(viewHandler, view); +#if WINDOWS || MACCATALYST + MapContextFlyout(viewHandler, view); +#endif + + Base.UpdateProperties(elementHandler, virtualView); + } + + public void UpdateProperty(IElementHandler elementHandler, IElement virtualView, string property) + { + TryUpdateProperty(elementHandler, virtualView, property); + } + + public bool TryUpdateProperty(IElementHandler elementHandler, IElement virtualView, string property) + { + if (elementHandler is IViewHandler viewHandler && virtualView is IView view) + { + switch (property) + { + // This property is a special one and needs to be set before other properties. + case nameof(IViewHandler.ContainerView): MapContainerView(viewHandler, view); return true; + case nameof(IView.AutomationId): MapAutomationId(viewHandler, view); return true; + case nameof(IView.Clip): MapClip(viewHandler, view); return true; + case nameof(IView.Shadow): MapShadow(viewHandler, view); return true; + case nameof(IView.Visibility): MapVisibility(viewHandler, view); return true; + case nameof(IView.Background): MapBackground(viewHandler, view); return true; + case nameof(IView.FlowDirection): MapFlowDirection(viewHandler, view); return true; + case nameof(IView.Width): MapWidth(viewHandler, view); return true; + case nameof(IView.Height): MapHeight(viewHandler, view); return true; + case nameof(IView.MinimumHeight): MapMinimumHeight(viewHandler, view); return true; + case nameof(IView.MaximumHeight): MapMaximumHeight(viewHandler, view); return true; + case nameof(IView.MinimumWidth): MapMinimumWidth(viewHandler, view); return true; + case nameof(IView.MaximumWidth): MapMaximumWidth(viewHandler, view); return true; + case nameof(IView.IsEnabled): MapIsEnabled(viewHandler, view); return true; + case nameof(IView.Opacity): MapOpacity(viewHandler, view); return true; + case nameof(IView.Semantics): MapSemantics(viewHandler, view); return true; + case nameof(IView.TranslationX): MapTranslationX(viewHandler, view); return true; + case nameof(IView.TranslationY): MapTranslationY(viewHandler, view); return true; + case nameof(IView.Scale): MapScale(viewHandler, view); return true; + case nameof(IView.ScaleX): MapScaleX(viewHandler, view); return true; + case nameof(IView.ScaleY): MapScaleY(viewHandler, view); return true; + case nameof(IView.Rotation): MapRotation(viewHandler, view); return true; + case nameof(IView.RotationX): MapRotationX(viewHandler, view); return true; + case nameof(IView.RotationY): MapRotationY(viewHandler, view); return true; + case nameof(IView.AnchorX): MapAnchorX(viewHandler, view); return true; + case nameof(IView.AnchorY): MapAnchorY(viewHandler, view); return true; + #pragma warning disable CS0618 // Type or member is obsolete + case nameof(IBorder.Border): MapBorderView(viewHandler, view); return true; + #pragma warning restore CS0618 // Type or member is obsolete + #if ANDROID || WINDOWS || TIZEN + case nameof(IToolbarElement.Toolbar): MapToolbar(viewHandler, view); return true; + #endif + case nameof(IView.InputTransparent): MapInputTransparent(viewHandler, view); return true; + case nameof(IToolTipElement.ToolTip): MapToolTip(viewHandler, view); return true; + #if WINDOWS || MACCATALYST + case nameof(IContextFlyoutElement.ContextFlyout): MapContextFlyout(viewHandler, view); return true; + #endif + default: break; + } + } + + if (Base is ICanUpdate can) + { + return can.TryUpdateProperty(elementHandler, virtualView, property); + } + + if (Base.GetProperty(property) is {} prop) + { + prop.Invoke(elementHandler, virtualView); + return true; + } + + return false; + } + } + /// /// A dictionary that maps the virtual view properties to their platform view counterparts. /// - public static IPropertyMapper ViewMapper = + public static IPropertyMapper ViewMapper = + new ViewHandlerPropertyMapper(); +/* #if ANDROID // Use a custom mapper for Android which knows how to batch the initial property sets new AndroidBatchPropertyMapper(ElementHandler.ElementMapper) @@ -74,6 +295,7 @@ public abstract partial class ViewHandler : ElementHandler, IViewHandler [nameof(IContextFlyoutElement.ContextFlyout)] = MapContextFlyout, #endif }; +// */ /// /// A dictionary that maps the virtual view commands to their platform view counterparts. diff --git a/src/Core/src/PropertyMapper.cs b/src/Core/src/PropertyMapper.cs index f084199719ea..69f30889d434 100644 --- a/src/Core/src/PropertyMapper.cs +++ b/src/Core/src/PropertyMapper.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; #if IOS || MACCATALYST using PlatformView = UIKit.UIView; @@ -16,17 +15,13 @@ namespace Microsoft.Maui { public abstract class PropertyMapper : IPropertyMapper { - // TODO: Make this private in .NET10 protected readonly Dictionary> _mapper = new(StringComparer.Ordinal); - IPropertyMapper[]? _chained; - List? _updatePropertiesKeys; - List>? _updatePropertiesMappers; - Dictionary?>? _cachedMappers; + IPropertyMapper[]? _chained; - List UpdatePropertiesKeys => _updatePropertiesKeys ?? SnapshotMappers().UpdatePropertiesKeys; - List> UpdatePropertiesMappers => _updatePropertiesMappers ?? SnapshotMappers().UpdatePropertiesMappers; - Dictionary?> CachedMappers => _cachedMappers ?? SnapshotMappers().CachedMappers; + // Keep a distinct list of the keys so we don't run any duplicate (overridden) updates more than once + // when we call UpdateProperties + HashSet? _updateKeys; public PropertyMapper() { @@ -40,65 +35,49 @@ public PropertyMapper(params IPropertyMapper[]? chained) protected virtual void SetPropertyCore(string key, Action action) { _mapper[key] = action; - ClearKeyCache(); } protected virtual void UpdatePropertyCore(string key, IElementHandler viewHandler, IElement virtualView) { if (!viewHandler.CanInvokeMappers()) + return; + + if (_mapper.TryGetValue(key, out var action)) { + action.Invoke(viewHandler, virtualView); return; } - - TryUpdatePropertyCore(key, viewHandler, virtualView); - } - - internal bool TryUpdatePropertyCore(string key, IElementHandler viewHandler, IElement virtualView) - { - var cachedMappers = CachedMappers; - if (cachedMappers.TryGetValue(key, out var action)) + + if (Chained is not null) { - if (action is not null) + foreach (var ch in Chained) { - action(viewHandler, virtualView); - return true; - } - - return false; - } - - // CachedMappers initially contains only the UpdateProperties keys which may not contain the key we are looking for. - // See AndroidBatchPropertyMapper for an example. - var mapper = GetProperty(key); - cachedMappers[key] = mapper; + if (ch is ICanUpdate can && can.TryUpdateProperty(viewHandler, virtualView, key)) + { + return; + } - if (mapper is not null) - { - mapper(viewHandler, virtualView); - return true; + if (ch.GetProperty(key) is {} prop) + { + prop.Invoke(viewHandler, virtualView); + return; + } + } } - - return false; } public virtual Action? GetProperty(string key) { if (_mapper.TryGetValue(key, out var action)) - { return action; - } - - var chainedPropertyMappers = Chained; - if (chainedPropertyMappers is not null) + else if (Chained is not null) { - foreach (var ch in chainedPropertyMappers) + foreach (var ch in Chained) { var returnValue = ch.GetProperty(key); if (returnValue != null) - { return returnValue; - } } } @@ -107,24 +86,20 @@ internal bool TryUpdatePropertyCore(string key, IElementHandler viewHandler, IEl public void UpdateProperty(IElementHandler viewHandler, IElement? virtualView, string property) { - if (virtualView == null || !viewHandler.CanInvokeMappers()) - { + if (virtualView == null) return; - } - TryUpdatePropertyCore(property, viewHandler, virtualView); + UpdatePropertyCore(property, viewHandler, virtualView); } public void UpdateProperties(IElementHandler viewHandler, IElement? virtualView) { - if (virtualView == null || !viewHandler.CanInvokeMappers()) - { + if (virtualView == null) return; - } - foreach (var mapper in UpdatePropertiesMappers) + foreach (var key in UpdateKeys) { - mapper(viewHandler, virtualView); + UpdatePropertyCore(key, viewHandler, virtualView); } } @@ -138,65 +113,34 @@ public IPropertyMapper[]? Chained } } - // TODO: Make private in .NET10 with a new name: ClearMergedMappers - protected virtual void ClearKeyCache() - { - _updatePropertiesMappers = null; - _updatePropertiesKeys = null; - _cachedMappers = null; - } - - // TODO: Remove in .NET10 - public virtual IReadOnlyCollection UpdateKeys => UpdatePropertiesKeys; - - public virtual IEnumerable GetKeys() + private HashSet PopulateKeys() { - // 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. - var chainedPropertyMappers = Chained; - if (chainedPropertyMappers is not null) + var keys = new HashSet(StringComparer.Ordinal); + foreach (var key in GetKeys()) { - for (int i = chainedPropertyMappers.Length - 1; i >= 0; i--) - { - foreach (var key in chainedPropertyMappers[i].GetKeys()) - { - yield return key; - } - } + keys.Add(key); } + return keys; + } - // Enqueue keys from this mapper. - foreach (var mapper in _mapper) - { - yield return mapper.Key; - } + protected virtual void ClearKeyCache() + { + _updateKeys = null; } - private (List UpdatePropertiesKeys, List> UpdatePropertiesMappers, Dictionary?> CachedMappers) SnapshotMappers() + public virtual IReadOnlyCollection UpdateKeys => _updateKeys ??= PopulateKeys(); + + public virtual IEnumerable GetKeys() { - var updatePropertiesKeys = GetKeys().Distinct().ToList(); - var updatePropertiesMappers = new List>(updatePropertiesKeys.Count); -#if ANDROID - var cacheSize = updatePropertiesKeys.Count + AndroidBatchPropertyMapper.SkipList.Count; -#else - var cacheSize = updatePropertiesKeys.Count; -#endif - var cachedMappers = new Dictionary?>(cacheSize); + foreach (var key in _mapper.Keys) + yield return key; - foreach (var key in updatePropertiesKeys) + if (Chained is not null) { - var mapper = GetProperty(key)!; - updatePropertiesMappers.Add(mapper); - cachedMappers[key] = mapper; + foreach (var chain in Chained) + foreach (var key in chain.GetKeys()) + yield return key; } - - _updatePropertiesKeys = updatePropertiesKeys; - _updatePropertiesMappers = updatePropertiesMappers; - _cachedMappers = cachedMappers; - - return (updatePropertiesKeys, updatePropertiesMappers, cachedMappers); } } @@ -211,6 +155,11 @@ public interface IPropertyMapper void UpdateProperty(IElementHandler elementHandler, IElement virtualView, string property); } + interface ICanUpdate : IPropertyMapper + { + bool TryUpdateProperty(IElementHandler elementHandler, IElement virtualView, string property); + } + public interface IPropertyMapper : IPropertyMapper where TVirtualView : IElement where TViewHandler : IElementHandler @@ -245,22 +194,12 @@ public void Add(string key, Action action) => SetPropertyCore(key, (h, v) => { if (v is TVirtualView vv) - { action?.Invoke((TViewHandler)h, vv); - } else if (Chained != null) { foreach (var chain in Chained) { - // Try to leverage our internal method which uses merged mappers - if (chain is PropertyMapper propertyMapper) - { - if (propertyMapper.TryUpdatePropertyCore(key, h, v)) - { - break; - } - } - else if (chain.GetProperty(key) != null) + if (chain.GetProperty(key) != null) { chain.UpdateProperty(h, v, key); break;