Skip to content

Commit 5b3ba86

Browse files
Fix "no event handler" in simultaneous blur+removal case (#31612)
1 parent 0377410 commit 5b3ba86

File tree

3 files changed

+92
-15
lines changed

3 files changed

+92
-15
lines changed

src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ internal class WebAssemblyRenderer : Renderer
2020
{
2121
private readonly ILogger _logger;
2222
private readonly int _webAssemblyRendererId;
23+
private readonly QueueWithLast<IncomingEventInfo> deferredIncomingEvents = new();
2324

2425
private bool isDispatchingEvent;
25-
private Queue<IncomingEventInfo> deferredIncomingEvents = new Queue<IncomingEventInfo>();
2626

2727
/// <summary>
2828
/// Constructs an instance of <see cref="WebAssemblyRenderer"/>.
@@ -103,7 +103,23 @@ protected override Task UpdateDisplayAsync(in RenderBatch batch)
103103
_webAssemblyRendererId,
104104
batch);
105105

106-
return Task.CompletedTask;
106+
if (deferredIncomingEvents.Count == 0)
107+
{
108+
// In the vast majority of cases, since the call to update the UI is synchronous,
109+
// we just return a pre-completed task from here.
110+
return Task.CompletedTask;
111+
}
112+
else
113+
{
114+
// However, in the rare case where JS sent us any event notifications that we had to
115+
// defer until later, we behave as if the renderbatch isn't acknowledged until we have at
116+
// least dispatched those event calls. This is to make the WebAssembly behavior more
117+
// consistent with the Server behavior, which receives batch acknowledgements asynchronously
118+
// and they are queued up with any other calls from JS such as event calls. If we didn't
119+
// do this, then the order of execution could be inconsistent with Server, and in fact
120+
// leads to a specific bug: https://github.com/dotnet/aspnetcore/issues/26838
121+
return deferredIncomingEvents.Last.StartHandlerCompletionSource.Task;
122+
}
107123
}
108124

109125
/// <inheritdoc />
@@ -144,7 +160,7 @@ public override Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? ev
144160
{
145161
var info = new IncomingEventInfo(eventHandlerId, eventFieldInfo, eventArgs);
146162
deferredIncomingEvents.Enqueue(info);
147-
return info.TaskCompletionSource.Task;
163+
return info.FinishHandlerCompletionSource.Task;
148164
}
149165
else
150166
{
@@ -171,16 +187,20 @@ public override Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? ev
171187
private async Task ProcessNextDeferredEventAsync()
172188
{
173189
var info = deferredIncomingEvents.Dequeue();
174-
var taskCompletionSource = info.TaskCompletionSource;
175190

176191
try
177192
{
178-
await DispatchEventAsync(info.EventHandlerId, info.EventFieldInfo, info.EventArgs);
179-
taskCompletionSource.SetResult();
193+
var handlerTask = DispatchEventAsync(info.EventHandlerId, info.EventFieldInfo, info.EventArgs);
194+
info.StartHandlerCompletionSource.SetResult();
195+
await handlerTask;
196+
info.FinishHandlerCompletionSource.SetResult();
180197
}
181198
catch (Exception ex)
182199
{
183-
taskCompletionSource.SetException(ex);
200+
// Even if the handler threw synchronously, we at least started processing, so always complete successfully
201+
info.StartHandlerCompletionSource.TrySetResult();
202+
203+
info.FinishHandlerCompletionSource.SetException(ex);
184204
}
185205
}
186206

@@ -189,14 +209,16 @@ readonly struct IncomingEventInfo
189209
public readonly ulong EventHandlerId;
190210
public readonly EventFieldInfo? EventFieldInfo;
191211
public readonly EventArgs EventArgs;
192-
public readonly TaskCompletionSource TaskCompletionSource;
212+
public readonly TaskCompletionSource StartHandlerCompletionSource;
213+
public readonly TaskCompletionSource FinishHandlerCompletionSource;
193214

194215
public IncomingEventInfo(ulong eventHandlerId, EventFieldInfo? eventFieldInfo, EventArgs eventArgs)
195216
{
196217
EventHandlerId = eventHandlerId;
197218
EventFieldInfo = eventFieldInfo;
198219
EventArgs = eventArgs;
199-
TaskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
220+
StartHandlerCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
221+
FinishHandlerCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
200222
}
201223
}
202224

@@ -225,5 +247,30 @@ public static void UnhandledExceptionRenderingComponent(ILogger logger, Exceptio
225247
exception);
226248
}
227249
}
250+
251+
private class QueueWithLast<T>
252+
{
253+
private readonly Queue<T> _items = new();
254+
255+
public int Count => _items.Count;
256+
257+
public T? Last { get; private set; }
258+
259+
public T Dequeue()
260+
{
261+
if (_items.Count == 1)
262+
{
263+
Last = default;
264+
}
265+
266+
return _items.Dequeue();
267+
}
268+
269+
public void Enqueue(T item)
270+
{
271+
Last = item;
272+
_items.Enqueue(item);
273+
}
274+
}
228275
}
229276
}

src/Components/test/E2ETest/Tests/EventTest.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@ public void FocusEvents_CanTrigger()
5050
Browser.Equal("onfocus,onfocusin,onblur,onfocusout,", () => output.Text);
5151
}
5252

53+
[Fact]
54+
public void FocusEvents_CanReceiveBlurCausedByElementRemoval()
55+
{
56+
// Represents https://github.com/dotnet/aspnetcore/issues/26838
57+
58+
Browser.MountTestComponent<FocusEventComponent>();
59+
60+
Browser.FindElement(By.Id("button-that-disappears")).Click();
61+
Browser.Equal("True", () => Browser.FindElement(By.Id("button-received-focus-out")).Text);
62+
}
63+
5364
[Fact]
5465
public void MouseOverAndMouseOut_CanTrigger()
5566
{

src/Components/test/testassets/BasicTestApp/FocusEventComponent.razor

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<h2>Focus and activation</h2>
44

55
<p @onfocusin="OnFocusIn" @onfocusout="OnFocusOut">
6-
Input: <input id="input" type="text" @onfocus="OnFocus" @onblur="OnBlur"/>
6+
Input: <input id="input" type="text" @onfocus="OnFocus" @onblur="OnBlur" />
77
</p>
88
<p>
99
Output: <span id="output">@message</span>
@@ -12,40 +12,59 @@
1212
<button @onclick="Clear">Clear</button>
1313
</p>
1414

15+
<p>
16+
A button that disappears when clicked:
17+
@if (showButtonThatDisappearsWhenClicked)
18+
{
19+
<button id="button-that-disappears" @onfocusout="DisappearingButtonFocusOut" @onclick="MakeButtonDisappear">
20+
Click me
21+
</button>
22+
}
23+
24+
Received focus out: <strong id="button-received-focus-out">@buttonReceivedFocusOut</strong>
25+
</p>
26+
1527
<p>
1628
Another input (to distract you) <input id="other" />
1729
</p>
1830

1931
@code {
20-
32+
bool showButtonThatDisappearsWhenClicked = true;
33+
bool buttonReceivedFocusOut;
2134
string message;
2235

2336
void OnFocus(FocusEventArgs e)
2437
{
2538
message += "onfocus,";
26-
StateHasChanged();
2739
}
2840

2941
void OnBlur(FocusEventArgs e)
3042
{
3143
message += "onblur,";
32-
StateHasChanged();
3344
}
3445

3546
void OnFocusIn(FocusEventArgs e)
3647
{
3748
message += "onfocusin,";
38-
StateHasChanged();
3949
}
4050

4151
void OnFocusOut(FocusEventArgs e)
4252
{
4353
message += "onfocusout,";
44-
StateHasChanged();
4554
}
4655

4756
void Clear()
4857
{
4958
message = string.Empty;
5059
}
60+
61+
void MakeButtonDisappear()
62+
{
63+
showButtonThatDisappearsWhenClicked = false;
64+
}
65+
66+
void DisappearingButtonFocusOut()
67+
{
68+
buttonReceivedFocusOut = true;
69+
}
5170
}

0 commit comments

Comments
 (0)