2
2
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3
3
4
4
using System ;
5
- using System . Collections . Generic ;
6
5
using System . Diagnostics . CodeAnalysis ;
7
6
using System . Threading . Tasks ;
8
7
using Microsoft . AspNetCore . Components . RenderTree ;
8
+ using Microsoft . AspNetCore . Components . WebAssembly . Hosting ;
9
9
using Microsoft . AspNetCore . Components . WebAssembly . Services ;
10
10
using Microsoft . Extensions . Logging ;
11
11
using static Microsoft . AspNetCore . Internal . LinkerFlags ;
@@ -20,9 +20,6 @@ internal class WebAssemblyRenderer : Renderer
20
20
{
21
21
private readonly ILogger _logger ;
22
22
private readonly int _webAssemblyRendererId ;
23
- private readonly QueueWithLast < IncomingEventInfo > deferredIncomingEvents = new ( ) ;
24
-
25
- private bool isDispatchingEvent ;
26
23
27
24
/// <summary>
28
25
/// Constructs an instance of <see cref="WebAssemblyRenderer"/>.
@@ -95,6 +92,32 @@ protected override void Dispose(bool disposing)
95
92
RendererRegistry . TryRemove ( _webAssemblyRendererId ) ;
96
93
}
97
94
95
+ /// <inheritdoc />
96
+ protected override void ProcessPendingRender ( )
97
+ {
98
+ // For historical reasons, Blazor WebAssembly doesn't enforce that you use InvokeAsync
99
+ // to dispatch calls that originated from outside the system. Changing that now would be
100
+ // too breaking, at least until we can make it a prerequisite for multithreading.
101
+ // So, we don't have a way to guarantee that calls to here are already on our work queue.
102
+ //
103
+ // We do need rendering to happen on the work queue so that incoming events can be deferred
104
+ // until we've finished this rendering process (and other similar cases where we want
105
+ // execution order to be consistent with Blazor Server, which queues all JS->.NET calls).
106
+ //
107
+ // So, if we find that we're here and are not yet on the work queue, get onto it. Either
108
+ // way, rendering must continue synchronously here and is not deferred until later.
109
+ if ( WebAssemblyCallQueue . IsInProgress )
110
+ {
111
+ base . ProcessPendingRender ( ) ;
112
+ }
113
+ else
114
+ {
115
+ WebAssemblyCallQueue . Schedule ( this , static @this => @this . CallBaseProcessPendingRender ( ) ) ;
116
+ }
117
+ }
118
+
119
+ private void CallBaseProcessPendingRender ( ) => base . ProcessPendingRender ( ) ;
120
+
98
121
/// <inheritdoc />
99
122
protected override Task UpdateDisplayAsync ( in RenderBatch batch )
100
123
{
@@ -103,22 +126,21 @@ protected override Task UpdateDisplayAsync(in RenderBatch batch)
103
126
_webAssemblyRendererId ,
104
127
batch ) ;
105
128
106
- if ( deferredIncomingEvents . Count == 0 )
129
+ if ( WebAssemblyCallQueue . HasUnstartedWork )
107
130
{
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 ;
131
+ // Because further incoming calls from JS to .NET are already queued (e.g., event notifications),
132
+ // we have to delay the renderbatch acknowledgement until it gets to the front of that queue.
133
+ // This is for consistency with Blazor Server which queues all JS-to-.NET calls relative to each
134
+ // other, and because various bits of cleanup logic rely on this ordering.
135
+ var tcs = new TaskCompletionSource ( ) ;
136
+ WebAssemblyCallQueue . Schedule ( tcs , static tcs => tcs . SetResult ( ) ) ;
137
+ return tcs . Task ;
111
138
}
112
139
else
113
140
{
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 ;
141
+ // Nothing else is pending, so we can treat the renderbatch as acknowledged synchronously.
142
+ // This lets upstream code skip an expensive code path and avoids some allocations.
143
+ return Task . CompletedTask ;
122
144
}
123
145
}
124
146
@@ -138,90 +160,6 @@ protected override void HandleException(Exception exception)
138
160
}
139
161
}
140
162
141
- /// <inheritdoc />
142
- public override Task DispatchEventAsync ( ulong eventHandlerId , EventFieldInfo ? eventFieldInfo , EventArgs eventArgs )
143
- {
144
- // Be sure we only run one event handler at once. Although they couldn't run
145
- // simultaneously anyway (there's only one thread), they could run nested on
146
- // the stack if somehow one event handler triggers another event synchronously.
147
- // We need event handlers not to overlap because (a) that's consistent with
148
- // server-side Blazor which uses a sync context, and (b) the rendering logic
149
- // relies completely on the idea that within a given scope it's only building
150
- // or processing one batch at a time.
151
- //
152
- // The only currently known case where this makes a difference is in the E2E
153
- // tests in ReorderingFocusComponent, where we hit what seems like a Chrome bug
154
- // where mutating the DOM cause an element's "change" to fire while its "input"
155
- // handler is still running (i.e., nested on the stack) -- this doesn't happen
156
- // in Firefox. Possibly a future version of Chrome may fix this, but even then,
157
- // it's conceivable that DOM mutation events could trigger this too.
158
-
159
- if ( isDispatchingEvent )
160
- {
161
- var info = new IncomingEventInfo ( eventHandlerId , eventFieldInfo , eventArgs ) ;
162
- deferredIncomingEvents . Enqueue ( info ) ;
163
- return info . FinishHandlerCompletionSource . Task ;
164
- }
165
- else
166
- {
167
- try
168
- {
169
- isDispatchingEvent = true ;
170
- return base . DispatchEventAsync ( eventHandlerId , eventFieldInfo , eventArgs ) ;
171
- }
172
- finally
173
- {
174
- isDispatchingEvent = false ;
175
-
176
- if ( deferredIncomingEvents . Count > 0 )
177
- {
178
- // Fire-and-forget because the task we return from this method should only reflect the
179
- // completion of its own event dispatch, not that of any others that happen to be queued.
180
- // Also, ProcessNextDeferredEventAsync deals with its own async errors.
181
- _ = ProcessNextDeferredEventAsync ( ) ;
182
- }
183
- }
184
- }
185
- }
186
-
187
- private async Task ProcessNextDeferredEventAsync ( )
188
- {
189
- var info = deferredIncomingEvents . Dequeue ( ) ;
190
-
191
- try
192
- {
193
- var handlerTask = DispatchEventAsync ( info . EventHandlerId , info . EventFieldInfo , info . EventArgs ) ;
194
- info . StartHandlerCompletionSource . SetResult ( ) ;
195
- await handlerTask ;
196
- info . FinishHandlerCompletionSource . SetResult ( ) ;
197
- }
198
- catch ( Exception ex )
199
- {
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 ) ;
204
- }
205
- }
206
-
207
- readonly struct IncomingEventInfo
208
- {
209
- public readonly ulong EventHandlerId ;
210
- public readonly EventFieldInfo ? EventFieldInfo ;
211
- public readonly EventArgs EventArgs ;
212
- public readonly TaskCompletionSource StartHandlerCompletionSource ;
213
- public readonly TaskCompletionSource FinishHandlerCompletionSource ;
214
-
215
- public IncomingEventInfo ( ulong eventHandlerId , EventFieldInfo ? eventFieldInfo , EventArgs eventArgs )
216
- {
217
- EventHandlerId = eventHandlerId ;
218
- EventFieldInfo = eventFieldInfo ;
219
- EventArgs = eventArgs ;
220
- StartHandlerCompletionSource = new TaskCompletionSource ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
221
- FinishHandlerCompletionSource = new TaskCompletionSource ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
222
- }
223
- }
224
-
225
163
private static class Log
226
164
{
227
165
private static readonly Action < ILogger , string , Exception > _unhandledExceptionRenderingComponent ;
@@ -247,30 +185,5 @@ public static void UnhandledExceptionRenderingComponent(ILogger logger, Exceptio
247
185
exception ) ;
248
186
}
249
187
}
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
- }
275
188
}
276
189
}
0 commit comments