Skip to content

Commit 9c4d46c

Browse files
In client-side Blazor, prevent recursive event handler invocations
1 parent 19239a9 commit 9c4d46c

File tree

1 file changed

+75
-3
lines changed

1 file changed

+75
-3
lines changed

src/Components/Browser/src/RendererRegistryEventDispatcher.cs

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
56
using System.Threading.Tasks;
67
using Microsoft.AspNetCore.Components.Rendering;
78
using Microsoft.JSInterop;
@@ -13,16 +14,73 @@ namespace Microsoft.AspNetCore.Components.Browser
1314
/// </summary>
1415
public static class RendererRegistryEventDispatcher
1516
{
17+
private static bool isDispatchingEvent;
18+
private static Queue<IncomingEventInfo> deferredIncomingEvents
19+
= new Queue<IncomingEventInfo>();
20+
1621
/// <summary>
1722
/// For framework use only.
1823
/// </summary>
1924
[JSInvokable(nameof(DispatchEvent))]
2025
public static Task DispatchEvent(
2126
BrowserEventDescriptor eventDescriptor, string eventArgsJson)
2227
{
23-
var eventArgs = ParseEventArgsJson(eventDescriptor.EventArgsType, eventArgsJson);
24-
var renderer = RendererRegistry.Current.Find(eventDescriptor.BrowserRendererId);
25-
return renderer.DispatchEventAsync(eventDescriptor.EventHandlerId, eventArgs);
28+
// Be sure we only run one event handler at once. Although they couldn't run
29+
// simultaneously anyway (there's only one thread), they could run nested on
30+
// the stack if somehow one event handler triggers another event synchronously.
31+
// We need event handlers not to overlap because (a) that's consistent with
32+
// server-side Blazor which uses a sync context, and (b) the rendering logic
33+
// relies completely on the idea that within a given scope it's only building
34+
// or processing one batch at a time.
35+
//
36+
// The only currently known case where this makes a difference is in the E2E
37+
// tests in ReorderingFocusComponent, where we hit what seems like a Chrome bug
38+
// where mutating the DOM cause an element's "change" to fire while its "input"
39+
// handler is still running (i.e., nested on the stack) -- this doesn't happen
40+
// in Firefox. Possibly a future version of Chrome may fix this, but even then,
41+
// it's conceivable that DOM mutation events could trigger this too.
42+
43+
if (isDispatchingEvent)
44+
{
45+
var info = new IncomingEventInfo(eventDescriptor, eventArgsJson);
46+
deferredIncomingEvents.Enqueue(info);
47+
return info.TaskCompletionSource.Task;
48+
}
49+
else
50+
{
51+
isDispatchingEvent = true;
52+
try
53+
{
54+
var eventArgs = ParseEventArgsJson(eventDescriptor.EventArgsType, eventArgsJson);
55+
var renderer = RendererRegistry.Current.Find(eventDescriptor.BrowserRendererId);
56+
return renderer.DispatchEventAsync(eventDescriptor.EventHandlerId, eventArgs);
57+
}
58+
finally
59+
{
60+
isDispatchingEvent = false;
61+
if (deferredIncomingEvents.Count > 0)
62+
{
63+
ProcessNextDeferredEvent();
64+
}
65+
}
66+
}
67+
}
68+
69+
private static void ProcessNextDeferredEvent()
70+
{
71+
var info = deferredIncomingEvents.Dequeue();
72+
var task = DispatchEvent(info.EventDescriptor, info.EventArgsJson);
73+
task.ContinueWith(_ =>
74+
{
75+
if (task.Exception != null)
76+
{
77+
info.TaskCompletionSource.SetException(task.Exception);
78+
}
79+
else
80+
{
81+
info.TaskCompletionSource.SetResult(null);
82+
}
83+
});
2684
}
2785

2886
private static UIEventArgs ParseEventArgsJson(string eventArgsType, string eventArgsJson)
@@ -78,5 +136,19 @@ public class BrowserEventDescriptor
78136
/// </summary>
79137
public string EventArgsType { get; set; }
80138
}
139+
140+
readonly struct IncomingEventInfo
141+
{
142+
public readonly BrowserEventDescriptor EventDescriptor;
143+
public readonly string EventArgsJson;
144+
public readonly TaskCompletionSource<object> TaskCompletionSource;
145+
146+
public IncomingEventInfo(BrowserEventDescriptor eventDescriptor, string eventArgsJson)
147+
{
148+
EventDescriptor = eventDescriptor;
149+
EventArgsJson = eventArgsJson;
150+
TaskCompletionSource = new TaskCompletionSource<object>();
151+
}
152+
}
81153
}
82154
}

0 commit comments

Comments
 (0)