Skip to content

Commit 4395e1f

Browse files
committed
Improve property mapper performance
1 parent f126268 commit 4395e1f

File tree

8 files changed

+271
-87
lines changed

8 files changed

+271
-87
lines changed

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

Lines changed: 0 additions & 10 deletions
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

Lines changed: 9 additions & 1 deletion
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,
Lines changed: 40 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,51 @@
1-
using System;
1+
#if ANDROID
2+
3+
using System;
24
using System.Collections.Generic;
35

4-
namespace Microsoft.Maui.Handlers
5-
{
6-
#if ANDROID
7-
class AndroidBatchPropertyMapper<TVirtualView, TViewHandler> : PropertyMapper<TVirtualView, TViewHandler>
8-
where TVirtualView : IElement
9-
where TViewHandler : IElementHandler
10-
{
11-
// During mass property updates, this list of properties will be skipped
12-
public static HashSet<string> SkipList = new(StringComparer.Ordinal)
13-
{
14-
nameof(IView.Visibility),
15-
nameof(IView.MinimumHeight),
16-
nameof(IView.MinimumWidth),
17-
nameof(IView.IsEnabled),
18-
nameof(IView.Opacity),
19-
nameof(IView.TranslationX),
20-
nameof(IView.TranslationY),
21-
nameof(IView.Scale),
22-
nameof(IView.ScaleX),
23-
nameof(IView.ScaleY),
24-
nameof(IView.Rotation),
25-
nameof(IView.RotationX),
26-
nameof(IView.RotationY),
27-
nameof(IView.AnchorX),
28-
nameof(IView.AnchorY),
29-
};
6+
namespace Microsoft.Maui.Handlers;
307

31-
public AndroidBatchPropertyMapper(params IPropertyMapper[] chained) : base(chained) { }
8+
file class AndroidPropertyMapperInfo
9+
{
10+
public static readonly ISet<string> UpdatePropertiesSkipList = new HashSet<string>(StringComparer.Ordinal)
11+
{
12+
nameof(IView.Visibility),
13+
nameof(IView.MinimumHeight),
14+
nameof(IView.MinimumWidth),
15+
nameof(IView.IsEnabled),
16+
nameof(IView.Opacity),
17+
nameof(IView.TranslationX),
18+
nameof(IView.TranslationY),
19+
nameof(IView.Scale),
20+
nameof(IView.ScaleX),
21+
nameof(IView.ScaleY),
22+
nameof(IView.Rotation),
23+
nameof(IView.RotationX),
24+
nameof(IView.RotationY),
25+
nameof(IView.AnchorX),
26+
nameof(IView.AnchorY),
27+
};
28+
}
3229

33-
public override IEnumerable<string> GetKeys()
30+
class AndroidBatchPropertyMapper<TVirtualView, TViewHandler>(params IPropertyMapper[] chained)
31+
: PropertyMapper<TVirtualView, TViewHandler>(chained)
32+
where TVirtualView : IElement
33+
where TViewHandler : IElementHandler
34+
{
35+
private protected override void UpdatePropertiesCore(IElementHandler viewHandler, IElement virtualView)
36+
{
37+
foreach (var mapper in MergedMappers)
3438
{
35-
foreach (var key in _mapper.Keys)
39+
// When doing the initial properties batch updates, ignore properties in the skip list.
40+
// These will be handled by ViewHandler.SetVirtualView() instead.
41+
if (AndroidPropertyMapperInfo.UpdatePropertiesSkipList.Contains(mapper.Key))
3642
{
37-
// When reporting the key list for mass updates up the chain, ignore properties in SkipList.
38-
// These will be handled by ViewHandler.SetVirtualView() instead.
39-
if (SkipList.Contains(key))
40-
{
41-
continue;
42-
}
43-
44-
yield return key;
43+
continue;
4544
}
4645

47-
if (Chained is not null)
48-
{
49-
foreach (var chain in Chained)
50-
foreach (var key in chain.GetKeys())
51-
yield return key;
52-
}
46+
mapper.Value(viewHandler, virtualView);
5347
}
5448
}
55-
#endif
5649
}
50+
51+
#endif

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ public abstract partial class ViewHandler : ElementHandler, IViewHandler
3131
new PropertyMapper<IView, IViewHandler>(ElementHandler.ElementMapper)
3232
#endif
3333
{
34+
// This property is a special one and needs to be set before other properties.
35+
[nameof(IViewHandler.ContainerView)] = MapContainerView,
36+
3437
[nameof(IView.AutomationId)] = MapAutomationId,
3538
[nameof(IView.Clip)] = MapClip,
3639
[nameof(IView.Shadow)] = MapShadow,
@@ -56,7 +59,6 @@ public abstract partial class ViewHandler : ElementHandler, IViewHandler
5659
[nameof(IView.RotationY)] = MapRotationY,
5760
[nameof(IView.AnchorX)] = MapAnchorX,
5861
[nameof(IView.AnchorY)] = MapAnchorY,
59-
[nameof(IViewHandler.ContainerView)] = MapContainerView,
6062
#pragma warning disable CS0618 // Type or member is obsolete
6163
[nameof(IBorder.Border)] = MapBorderView,
6264
#pragma warning restore CS0618 // Type or member is obsolete

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

Lines changed: 4 additions & 1 deletion
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

Lines changed: 94 additions & 29 deletions
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,16 @@ namespace Microsoft.Maui
1516
{
1617
public abstract class PropertyMapper : IPropertyMapper
1718
{
19+
// TODO: Make this private in .NET10
1820
protected readonly Dictionary<string, Action<IElementHandler, IElement>> _mapper = new(StringComparer.Ordinal);
1921

2022
IPropertyMapper[]? _chained;
2123

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;
24+
IReadOnlyDictionary<string, Action<IElementHandler, IElement>>? _mergedMappers;
25+
private protected IReadOnlyDictionary<string, Action<IElementHandler, IElement>> MergedMappers => _mergedMappers ?? SnapshotMappers().Mappers;
26+
27+
IReadOnlyList<string>? _mergedKeys;
28+
IReadOnlyList<string> MergedKeys => _mergedKeys ?? SnapshotMappers().Keys;
2529

2630
public PropertyMapper()
2731
{
@@ -35,29 +39,48 @@ public PropertyMapper(params IPropertyMapper[]? chained)
3539
protected virtual void SetPropertyCore(string key, Action<IElementHandler, IElement> action)
3640
{
3741
_mapper[key] = action;
42+
3843
ClearKeyCache();
3944
}
4045

46+
// TODO: Remove in .NET10
4147
protected virtual void UpdatePropertyCore(string key, IElementHandler viewHandler, IElement virtualView)
4248
{
4349
if (!viewHandler.CanInvokeMappers())
50+
{
4451
return;
52+
}
4553

46-
var action = GetProperty(key);
47-
action?.Invoke(viewHandler, virtualView);
54+
TryUpdatePropertyCore(key, viewHandler, virtualView);
55+
}
56+
57+
internal bool TryUpdatePropertyCore(string key, IElementHandler viewHandler, IElement virtualView)
58+
{
59+
if (MergedMappers.TryGetValue(key, out var action))
60+
{
61+
action(viewHandler, virtualView);
62+
return true;
63+
}
64+
65+
return false;
4866
}
4967

5068
public virtual Action<IElementHandler, IElement>? GetProperty(string key)
5169
{
5270
if (_mapper.TryGetValue(key, out var action))
71+
{
5372
return action;
54-
else if (Chained is not null)
73+
}
74+
75+
if (Chained is not null)
5576
{
5677
foreach (var ch in Chained)
5778
{
5879
var returnValue = ch.GetProperty(key);
5980
if (returnValue != null)
81+
{
6082
return returnValue;
83+
}
6184
}
6285
}
6386

@@ -66,20 +89,32 @@ protected virtual void UpdatePropertyCore(string key, IElementHandler viewHandle
6689

6790
public void UpdateProperty(IElementHandler viewHandler, IElement? virtualView, string property)
6891
{
69-
if (virtualView == null)
92+
if (virtualView == null || !viewHandler.CanInvokeMappers())
93+
{
7094
return;
95+
}
7196

72-
UpdatePropertyCore(property, viewHandler, virtualView);
97+
if (MergedMappers.TryGetValue(property, out var action))
98+
{
99+
action(viewHandler, virtualView);
100+
}
73101
}
74102

75103
public void UpdateProperties(IElementHandler viewHandler, IElement? virtualView)
76104
{
77-
if (virtualView == null)
105+
if (virtualView == null || !viewHandler.CanInvokeMappers())
106+
{
78107
return;
108+
}
79109

80-
foreach (var key in UpdateKeys)
110+
UpdatePropertiesCore(viewHandler, virtualView);
111+
}
112+
113+
private protected virtual void UpdatePropertiesCore(IElementHandler viewHandler, IElement virtualView)
114+
{
115+
foreach (var mapper in MergedMappers)
81116
{
82-
UpdatePropertyCore(key, viewHandler, virtualView);
117+
mapper.Value(viewHandler, virtualView);
83118
}
84119
}
85120

@@ -93,35 +128,55 @@ public IPropertyMapper[]? Chained
93128
}
94129
}
95130

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-
131+
// TODO: Make private in .NET10 with a new name: ClearMergedMappers
106132
protected virtual void ClearKeyCache()
107133
{
108-
_updateKeys = null;
134+
_mergedMappers = null;
135+
_mergedKeys = null;
109136
}
110137

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

113141
public virtual IEnumerable<string> GetKeys()
114142
{
115-
foreach (var key in _mapper.Keys)
116-
yield return key;
117-
143+
// We want to retain the initial order of the keys to avoid race conditions
144+
// when a property mapping is overridden by a new instance of property mapper.
145+
// As an example, the container view mapper should always run first.
146+
// Siblings mapper should not have keys intersection.
118147
if (Chained is not null)
119148
{
120-
foreach (var chain in Chained)
121-
foreach (var key in chain.GetKeys())
149+
for (int i = Chained.Length - 1; i >= 0; i--)
150+
{
151+
foreach (var key in Chained[i].GetKeys())
152+
{
122153
yield return key;
154+
}
155+
}
156+
}
157+
158+
// Enqueue keys from this mapper.
159+
foreach (var mapper in _mapper)
160+
{
161+
yield return mapper.Key;
123162
}
124163
}
164+
165+
private (List<string> Keys, Dictionary<string, Action<IElementHandler, IElement>> Mappers) SnapshotMappers()
166+
{
167+
var keys = GetKeys().Distinct().ToList();
168+
169+
var mappers = new Dictionary<string, Action<IElementHandler, IElement>>(keys.Count);
170+
foreach (var key in keys)
171+
{
172+
mappers[key] = GetProperty(key)!;
173+
}
174+
175+
_mergedKeys = keys;
176+
_mergedMappers = mappers;
177+
178+
return (keys, mappers);
179+
}
125180
}
126181

127182
public interface IPropertyMapper
@@ -169,12 +224,22 @@ public void Add(string key, Action<TViewHandler, TVirtualView> action) =>
169224
SetPropertyCore(key, (h, v) =>
170225
{
171226
if (v is TVirtualView vv)
227+
{
172228
action?.Invoke((TViewHandler)h, vv);
229+
}
173230
else if (Chained != null)
174231
{
175232
foreach (var chain in Chained)
176233
{
177-
if (chain.GetProperty(key) != null)
234+
// Try to leverage our internal method which uses merged mappers
235+
if (chain is PropertyMapper propertyMapper)
236+
{
237+
if (propertyMapper.TryUpdatePropertyCore(key, h, v))
238+
{
239+
break;
240+
}
241+
}
242+
else if (chain.GetProperty(key) != null)
178243
{
179244
chain.UpdateProperty(h, v, key);
180245
break;

0 commit comments

Comments
 (0)