Skip to content

Commit 7762a08

Browse files
committed
Improve property mapper performance
1 parent f126268 commit 7762a08

File tree

8 files changed

+256
-103
lines changed

8 files changed

+256
-103
lines changed

src/Core/src/Handlers/FlyoutView/FlyoutViewHandler.Android.cs

-10
Original file line numberDiff line numberDiff line change
@@ -118,16 +118,6 @@ void UpdateDetail()
118118
void UpdateFlyout()
119119
{
120120
_ = MauiContext ?? throw new InvalidOperationException($"{nameof(MauiContext)} should have been set by base class.");
121-
122-
// Once this issue has been taken care of
123-
// https://github.com/dotnet/maui/issues/8456
124-
// we can remove this code
125-
if (VirtualView.Flyout.Handler?.MauiContext != null &&
126-
VirtualView.Flyout.Handler.MauiContext != MauiContext)
127-
{
128-
VirtualView.Flyout.Handler.DisconnectHandler();
129-
}
130-
131121
_ = VirtualView.Flyout.ToPlatform(MauiContext);
132122

133123
var newFlyoutView = VirtualView.Flyout.ToPlatform();

src/Core/src/Handlers/FlyoutView/FlyoutViewHandler.cs

+9-1
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,19 @@ namespace Microsoft.Maui.Handlers
1616
{
1717
public partial class FlyoutViewHandler : IFlyoutViewHandler
1818
{
19-
public static IPropertyMapper<IFlyoutView, IFlyoutViewHandler> Mapper = new PropertyMapper<IFlyoutView, IFlyoutViewHandler>(ViewHandler.ViewMapper)
19+
// Like IViewHandler.ContainerView, those properties should be set with priority because other mappers depend on them (like IToolbarElement.Toolbar).
20+
// So we have a separate mapper for them.
21+
private static readonly IPropertyMapper<IFlyoutView, IFlyoutViewHandler> FlyoutLayoutMapper = new PropertyMapper<IFlyoutView, IFlyoutViewHandler>()
2022
{
2123
#if ANDROID || WINDOWS || TIZEN
2224
[nameof(IFlyoutView.Flyout)] = MapFlyout,
2325
[nameof(IFlyoutView.Detail)] = MapDetail,
26+
#endif
27+
};
28+
29+
public static IPropertyMapper<IFlyoutView, IFlyoutViewHandler> Mapper = new PropertyMapper<IFlyoutView, IFlyoutViewHandler>(ViewHandler.ViewMapper, FlyoutLayoutMapper)
30+
{
31+
#if ANDROID || WINDOWS || TIZEN
2432
[nameof(IFlyoutView.IsPresented)] = MapIsPresented,
2533
[nameof(IFlyoutView.FlyoutBehavior)] = MapFlyoutBehavior,
2634
[nameof(IFlyoutView.FlyoutWidth)] = MapFlyoutWidth,

src/Core/src/Handlers/View/AndroidBatchPropertyMapper.cs

-56
This file was deleted.

src/Core/src/Handlers/View/ViewHandler.cs

+3-6
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,11 @@ public abstract partial class ViewHandler : ElementHandler, IViewHandler
2424
/// A dictionary that maps the virtual view properties to their platform view counterparts.
2525
/// </summary>
2626
public static IPropertyMapper<IView, IViewHandler> ViewMapper =
27-
#if ANDROID
28-
// Use a custom mapper for Android which knows how to batch the initial property sets
29-
new AndroidBatchPropertyMapper<IView, IViewHandler>(ElementMapper)
30-
#else
3127
new PropertyMapper<IView, IViewHandler>(ElementHandler.ElementMapper)
32-
#endif
3328
{
29+
// This property is a special one and needs to be set before other properties.
30+
[nameof(IViewHandler.ContainerView)] = MapContainerView,
31+
3432
[nameof(IView.AutomationId)] = MapAutomationId,
3533
[nameof(IView.Clip)] = MapClip,
3634
[nameof(IView.Shadow)] = MapShadow,
@@ -56,7 +54,6 @@ public abstract partial class ViewHandler : ElementHandler, IViewHandler
5654
[nameof(IView.RotationY)] = MapRotationY,
5755
[nameof(IView.AnchorX)] = MapAnchorX,
5856
[nameof(IView.AnchorY)] = MapAnchorY,
59-
[nameof(IViewHandler.ContainerView)] = MapContainerView,
6057
#pragma warning disable CS0618 // Type or member is obsolete
6158
[nameof(IBorder.Border)] = MapBorderView,
6259
#pragma warning restore CS0618 // Type or member is obsolete

src/Core/src/Platform/iOS/ContainerViewController.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ public override void LoadView()
6868

6969
void LoadPlatformView(IElement view)
7070
{
71-
currentPlatformView = _pendingLoadedView ?? CreatePlatformView(view);
71+
var platformView = _pendingLoadedView ?? CreatePlatformView(view);
72+
platformView = platformView.Superview as WrapperView ?? platformView;
73+
74+
currentPlatformView = platformView;
7275
_pendingLoadedView = null;
7376

7477
View!.AddSubview(currentPlatformView);

src/Core/src/PropertyMapper.cs

+119-29
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
34

45
#if IOS || MACCATALYST
56
using PlatformView = UIKit.UIView;
@@ -15,13 +16,37 @@ namespace Microsoft.Maui
1516
{
1617
public abstract class PropertyMapper : IPropertyMapper
1718
{
19+
#if ANDROID
20+
private static readonly HashSet<string> UpdatePropertiesSkipList = new(StringComparer.Ordinal)
21+
{
22+
nameof(IView.Visibility),
23+
nameof(IView.MinimumHeight),
24+
nameof(IView.MinimumWidth),
25+
nameof(IView.IsEnabled),
26+
nameof(IView.Opacity),
27+
nameof(IView.TranslationX),
28+
nameof(IView.TranslationY),
29+
nameof(IView.Scale),
30+
nameof(IView.ScaleX),
31+
nameof(IView.ScaleY),
32+
nameof(IView.Rotation),
33+
nameof(IView.RotationX),
34+
nameof(IView.RotationY),
35+
nameof(IView.AnchorX),
36+
nameof(IView.AnchorY),
37+
};
38+
#endif
39+
40+
// TODO: Make this private in .NET10
1841
protected readonly Dictionary<string, Action<IElementHandler, IElement>> _mapper = new(StringComparer.Ordinal);
1942

2043
IPropertyMapper[]? _chained;
2144

22-
// Keep a distinct list of the keys so we don't run any duplicate (overridden) updates more than once
23-
// when we call UpdateProperties
24-
HashSet<string>? _updateKeys;
45+
IReadOnlyDictionary<string, Action<IElementHandler, IElement>>? _mergedMappers;
46+
private protected IReadOnlyDictionary<string, Action<IElementHandler, IElement>> MergedMappers => _mergedMappers ?? SnapshotMappers().Mappers;
47+
48+
IReadOnlyList<string>? _mergedKeys;
49+
IReadOnlyList<string> MergedKeys => _mergedKeys ?? SnapshotMappers().Keys;
2550

2651
public PropertyMapper()
2752
{
@@ -35,29 +60,48 @@ public PropertyMapper(params IPropertyMapper[]? chained)
3560
protected virtual void SetPropertyCore(string key, Action<IElementHandler, IElement> action)
3661
{
3762
_mapper[key] = action;
63+
3864
ClearKeyCache();
3965
}
4066

67+
// TODO: Remove in .NET10
4168
protected virtual void UpdatePropertyCore(string key, IElementHandler viewHandler, IElement virtualView)
4269
{
4370
if (!viewHandler.CanInvokeMappers())
71+
{
4472
return;
73+
}
74+
75+
TryUpdatePropertyCore(key, viewHandler, virtualView);
76+
}
4577

46-
var action = GetProperty(key);
47-
action?.Invoke(viewHandler, virtualView);
78+
internal bool TryUpdatePropertyCore(string key, IElementHandler viewHandler, IElement virtualView)
79+
{
80+
if (MergedMappers.TryGetValue(key, out var action))
81+
{
82+
action(viewHandler, virtualView);
83+
return true;
84+
}
85+
86+
return false;
4887
}
4988

5089
public virtual Action<IElementHandler, IElement>? GetProperty(string key)
5190
{
5291
if (_mapper.TryGetValue(key, out var action))
92+
{
5393
return action;
54-
else if (Chained is not null)
94+
}
95+
96+
if (Chained is not null)
5597
{
5698
foreach (var ch in Chained)
5799
{
58100
var returnValue = ch.GetProperty(key);
59101
if (returnValue != null)
102+
{
60103
return returnValue;
104+
}
61105
}
62106
}
63107

@@ -66,20 +110,36 @@ protected virtual void UpdatePropertyCore(string key, IElementHandler viewHandle
66110

67111
public void UpdateProperty(IElementHandler viewHandler, IElement? virtualView, string property)
68112
{
69-
if (virtualView == null)
113+
if (virtualView == null || !viewHandler.CanInvokeMappers())
114+
{
70115
return;
116+
}
71117

72-
UpdatePropertyCore(property, viewHandler, virtualView);
118+
if (MergedMappers.TryGetValue(property, out var action))
119+
{
120+
action(viewHandler, virtualView);
121+
}
73122
}
74123

75124
public void UpdateProperties(IElementHandler viewHandler, IElement? virtualView)
76125
{
77-
if (virtualView == null)
126+
if (virtualView == null || !viewHandler.CanInvokeMappers())
127+
{
78128
return;
129+
}
79130

80-
foreach (var key in UpdateKeys)
131+
foreach (var mapper in MergedMappers)
81132
{
82-
UpdatePropertyCore(key, viewHandler, virtualView);
133+
#if ANDROID
134+
// When doing the initial properties batch updates, ignore properties in the skip list.
135+
// These will be handled by ViewHandler.SetVirtualView() instead.
136+
if (UpdatePropertiesSkipList.Contains(mapper.Key))
137+
{
138+
continue;
139+
}
140+
#endif
141+
142+
mapper.Value(viewHandler, virtualView);
83143
}
84144
}
85145

@@ -93,35 +153,55 @@ public IPropertyMapper[]? Chained
93153
}
94154
}
95155

96-
private HashSet<string> PopulateKeys()
97-
{
98-
var keys = new HashSet<string>(StringComparer.Ordinal);
99-
foreach (var key in GetKeys())
100-
{
101-
keys.Add(key);
102-
}
103-
return keys;
104-
}
105-
156+
// TODO: Make private in .NET10 with a new name: ClearMergedMappers
106157
protected virtual void ClearKeyCache()
107158
{
108-
_updateKeys = null;
159+
_mergedMappers = null;
160+
_mergedKeys = null;
109161
}
110162

111-
public virtual IReadOnlyCollection<string> UpdateKeys => _updateKeys ??= PopulateKeys();
163+
// TODO: Remove in .NET10
164+
public virtual IReadOnlyCollection<string> UpdateKeys => MergedKeys;
112165

113166
public virtual IEnumerable<string> GetKeys()
114167
{
115-
foreach (var key in _mapper.Keys)
116-
yield return key;
117-
168+
// We want to retain the initial order of the keys to avoid race conditions
169+
// when a property mapping is overridden by a new instance of property mapper.
170+
// As an example, the container view mapper should always run first.
171+
// Siblings mapper should not have keys intersection.
118172
if (Chained is not null)
119173
{
120-
foreach (var chain in Chained)
121-
foreach (var key in chain.GetKeys())
174+
for (int i = Chained.Length - 1; i >= 0; i--)
175+
{
176+
foreach (var key in Chained[i].GetKeys())
177+
{
122178
yield return key;
179+
}
180+
}
181+
}
182+
183+
// Enqueue keys from this mapper.
184+
foreach (var mapper in _mapper)
185+
{
186+
yield return mapper.Key;
123187
}
124188
}
189+
190+
private (List<string> Keys, Dictionary<string, Action<IElementHandler, IElement>> Mappers) SnapshotMappers()
191+
{
192+
var keys = GetKeys().Distinct().ToList();
193+
194+
var mappers = new Dictionary<string, Action<IElementHandler, IElement>>(keys.Count);
195+
foreach (var key in keys)
196+
{
197+
mappers[key] = GetProperty(key)!;
198+
}
199+
200+
_mergedKeys = keys;
201+
_mergedMappers = mappers;
202+
203+
return (keys, mappers);
204+
}
125205
}
126206

127207
public interface IPropertyMapper
@@ -169,12 +249,22 @@ public void Add(string key, Action<TViewHandler, TVirtualView> action) =>
169249
SetPropertyCore(key, (h, v) =>
170250
{
171251
if (v is TVirtualView vv)
252+
{
172253
action?.Invoke((TViewHandler)h, vv);
254+
}
173255
else if (Chained != null)
174256
{
175257
foreach (var chain in Chained)
176258
{
177-
if (chain.GetProperty(key) != null)
259+
// Try to leverage our internal method which uses merged mappers
260+
if (chain is PropertyMapper propertyMapper)
261+
{
262+
if (propertyMapper.TryUpdatePropertyCore(key, h, v))
263+
{
264+
break;
265+
}
266+
}
267+
else if (chain.GetProperty(key) != null)
178268
{
179269
chain.UpdateProperty(h, v, key);
180270
break;

0 commit comments

Comments
 (0)