Skip to content

Commit 5bc413a

Browse files
committed
fix: Fix possible collection changed while enumerating in EVP propagation
1 parent 8b8abd4 commit 5bc413a

File tree

2 files changed

+98
-4
lines changed

2 files changed

+98
-4
lines changed

src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_FrameworkElement_EffectiveViewport.cs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,80 @@ await RetryAssert(() =>
10561056
}
10571057
#endif
10581058

1059+
[TestMethod]
1060+
[RunsOnUIThread]
1061+
public async Task EVP_When_RemoveHandlerWhileRaisingEvent()
1062+
{
1063+
const bool canHorizontallyScroll = true, canVerticallyScroll = true;
1064+
Border sut;
1065+
ScrollViewer sv;
1066+
var root = new Border
1067+
{
1068+
HorizontalAlignment = HorizontalAlignment.Left,
1069+
VerticalAlignment = VerticalAlignment.Top,
1070+
Child = sv = new ScrollViewer
1071+
{
1072+
Width = 512,
1073+
Height = 512,
1074+
HorizontalAlignment = HorizontalAlignment.Left,
1075+
VerticalAlignment = VerticalAlignment.Top,
1076+
HorizontalScrollBarVisibility = canHorizontallyScroll ? ScrollBarVisibility.Auto : ScrollBarVisibility.Disabled,
1077+
VerticalScrollBarVisibility = canVerticallyScroll ? ScrollBarVisibility.Auto : ScrollBarVisibility.Disabled,
1078+
HorizontalScrollMode = canHorizontallyScroll ? ScrollMode.Enabled : ScrollMode.Disabled,
1079+
VerticalScrollMode = canVerticallyScroll ? ScrollMode.Enabled : ScrollMode.Disabled,
1080+
Content = new Grid
1081+
{
1082+
Height = 1024,
1083+
Children =
1084+
{
1085+
(sut = new Border
1086+
{
1087+
HorizontalAlignment = HorizontalAlignment.Left,
1088+
VerticalAlignment = VerticalAlignment.Top,
1089+
Background = new SolidColorBrush(Color.FromArgb(0xFF, 0x00, 0x00, 0xF9)),
1090+
Width = 100,
1091+
Height = 100,
1092+
RenderTransform = new CompositeTransform { TranslateX = 50, TranslateY = 25, ScaleY = 2 }
1093+
})
1094+
}
1095+
}
1096+
}
1097+
};
1098+
1099+
var result = new TaskCompletionSource<object?>();
1100+
var allowDetach = false;
1101+
sut.EffectiveViewportChanged += OnSutEVPChanged;
1102+
1103+
WindowContent = root;
1104+
1105+
// First make sure to be loaded (and actually ignore them)
1106+
await WaitForIdle();
1107+
allowDetach = true;
1108+
1109+
// Then cause a layout update
1110+
sv.ChangeView(null, 512, null, disableAnimation: true);
1111+
1112+
await result.Task;
1113+
1114+
void OnSutEVPChanged(FrameworkElement sender, EffectiveViewportChangedEventArgs args)
1115+
{
1116+
try
1117+
{
1118+
if (!allowDetach)
1119+
{
1120+
return;
1121+
}
1122+
1123+
sut.EffectiveViewportChanged -= OnSutEVPChanged;
1124+
result.TrySetResult(default);
1125+
}
1126+
catch (Exception e)
1127+
{
1128+
result.TrySetException(e);
1129+
}
1130+
}
1131+
}
1132+
10591133
private async Task RetryAssert(Action assertion)
10601134
{
10611135
var attempt = 0;

src/Uno.UI/UI/Xaml/FrameworkElement.EffectiveViewport.cs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ partial class FrameworkElement : IFrameworkElement_EffectiveViewport
4343
private event TypedEventHandler<_This, EffectiveViewportChangedEventArgs>? _effectiveViewportChanged;
4444
private bool _hasNewHandler;
4545
private List<IFrameworkElement_EffectiveViewport>? _childrenInterestedInViewportUpdates;
46+
private bool _isEnumeratingChildrenInterestedInViewportUpdates;
4647
private IDisposable? _parentViewportUpdatesSubscription;
4748
private ViewportInfo _parentViewport = ViewportInfo.Empty; // WARNING: Stored in parent's coordinates space, use GetParentViewport()
4849
private ViewportInfo _lastEffectiveViewport;
@@ -153,12 +154,21 @@ IDisposable IFrameworkElement_EffectiveViewport.RequestViewportUpdates(bool isIn
153154
Uno.UI.Extensions.DependencyObjectExtensions.GetChildren(this).OfType<IFrameworkElement_EffectiveViewport>().Contains(child)
154155
|| (child as _View)?.FindFirstAncestor<IFrameworkElement_EffectiveViewport>() == this);
155156

156-
(_childrenInterestedInViewportUpdates ??= new()).Add(child);
157+
var childrenInterestedInViewportUpdates = _childrenInterestedInViewportUpdates switch
158+
{
159+
null => (_childrenInterestedInViewportUpdates = new()),
160+
_ when _isEnumeratingChildrenInterestedInViewportUpdates => (_childrenInterestedInViewportUpdates = new(_childrenInterestedInViewportUpdates)),
161+
_ => _childrenInterestedInViewportUpdates,
162+
};
163+
childrenInterestedInViewportUpdates.Add(child);
157164
ReconfigureViewportPropagation(isInternalUpdate, child);
158165

159166
return Disposable.Create(() =>
160167
{
161-
_childrenInterestedInViewportUpdates!.Remove(child);
168+
var childrenInterestedInViewportUpdates = _isEnumeratingChildrenInterestedInViewportUpdates
169+
? (_childrenInterestedInViewportUpdates = new(_childrenInterestedInViewportUpdates))
170+
: _childrenInterestedInViewportUpdates!;
171+
childrenInterestedInViewportUpdates.Remove(child);
162172
ReconfigureViewportPropagation();
163173
});
164174
}
@@ -351,9 +361,19 @@ private void PropagateEffectiveViewportChange(
351361

352362
if (_childrenInterestedInViewportUpdates is { Count: > 0 } && (isInitial || viewportUpdated))
353363
{
354-
foreach (var child in _childrenInterestedInViewportUpdates)
364+
_isEnumeratingChildrenInterestedInViewportUpdates = true;
365+
var enumerator = _childrenInterestedInViewportUpdates.GetEnumerator();
366+
try
367+
{
368+
while (enumerator.MoveNext())
369+
{
370+
enumerator.Current!.OnParentViewportChanged(isInitial, isInternal, this, viewport);
371+
}
372+
}
373+
finally
355374
{
356-
child.OnParentViewportChanged(isInitial, isInternal, this, viewport);
375+
_isEnumeratingChildrenInterestedInViewportUpdates = false;
376+
enumerator.Dispose();
357377
}
358378
}
359379
}

0 commit comments

Comments
 (0)