-
Notifications
You must be signed in to change notification settings - Fork 9.8k
/
WebAssemblyRenderer.cs
226 lines (200 loc) · 9.93 KB
/
WebAssemblyRenderer.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.WebAssembly.Services;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop.WebAssembly;
namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering
{
/// <summary>
/// Provides mechanisms for rendering <see cref="IComponent"/> instances in a
/// web browser, dispatching events to them, and refreshing the UI as required.
/// </summary>
internal class WebAssemblyRenderer : Renderer
{
private readonly ILogger _logger;
private readonly int _webAssemblyRendererId;
private bool isDispatchingEvent;
private Queue<IncomingEventInfo> deferredIncomingEvents = new Queue<IncomingEventInfo>();
/// <summary>
/// Constructs an instance of <see cref="WebAssemblyRenderer"/>.
/// </summary>
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> to use when initializing components.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public WebAssemblyRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory)
: base(serviceProvider, loggerFactory)
{
// The WebAssembly renderer registers and unregisters itself with the static registry
_webAssemblyRendererId = RendererRegistry.Add(this);
_logger = loggerFactory.CreateLogger<WebAssemblyRenderer>();
ElementReferenceContext = DefaultWebAssemblyJSRuntime.Instance.ElementReferenceContext;
}
public override Dispatcher Dispatcher => NullDispatcher.Instance;
/// <summary>
/// Attaches a new root component to the renderer,
/// causing it to be displayed in the specified DOM element.
/// </summary>
/// <typeparam name="TComponent">The type of the component.</typeparam>
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous rendering of the added component.</returns>
/// <remarks>
/// Callers of this method may choose to ignore the returned <see cref="Task"/> if they do not
/// want to await the rendering of the added component.
/// </remarks>
public Task AddComponentAsync<TComponent>(string domElementSelector) where TComponent : IComponent
=> AddComponentAsync(typeof(TComponent), domElementSelector);
/// <summary>
/// Associates the <see cref="IComponent"/> with the <see cref="WebAssemblyRenderer"/>,
/// causing it to be displayed in the specified DOM element.
/// </summary>
/// <param name="componentType">The type of the component.</param>
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous rendering of the added component.</returns>
/// <remarks>
/// Callers of this method may choose to ignore the returned <see cref="Task"/> if they do not
/// want to await the rendering of the added component.
/// </remarks>
public Task AddComponentAsync(Type componentType, string domElementSelector)
{
var component = InstantiateComponent(componentType);
var componentId = AssignRootComponentId(component);
// The only reason we're calling this synchronously is so that, if it throws,
// we get the exception back *before* attempting the first UpdateDisplayAsync
// (otherwise the logged exception will come from UpdateDisplayAsync instead of here)
// When implementing support for out-of-process runtimes, we'll need to call this
// asynchronously and ensure we surface any exceptions correctly.
DefaultWebAssemblyJSRuntime.Instance.Invoke<object>(
"Blazor._internal.attachRootComponentToElement",
domElementSelector,
componentId,
_webAssemblyRendererId);
return RenderRootComponentAsync(componentId);
}
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
RendererRegistry.TryRemove(_webAssemblyRendererId);
}
/// <inheritdoc />
protected override Task UpdateDisplayAsync(in RenderBatch batch)
{
DefaultWebAssemblyJSRuntime.Instance.InvokeUnmarshalled<int, RenderBatch, object>(
"Blazor._internal.renderBatch",
_webAssemblyRendererId,
batch);
return Task.CompletedTask;
}
/// <inheritdoc />
protected override void HandleException(Exception exception)
{
if (exception is AggregateException aggregateException)
{
foreach (var innerException in aggregateException.Flatten().InnerExceptions)
{
Log.UnhandledExceptionRenderingComponent(_logger, innerException);
}
}
else
{
Log.UnhandledExceptionRenderingComponent(_logger, exception);
}
}
/// <inheritdoc />
public override Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo eventFieldInfo, EventArgs eventArgs)
{
// Be sure we only run one event handler at once. Although they couldn't run
// simultaneously anyway (there's only one thread), they could run nested on
// the stack if somehow one event handler triggers another event synchronously.
// We need event handlers not to overlap because (a) that's consistent with
// server-side Blazor which uses a sync context, and (b) the rendering logic
// relies completely on the idea that within a given scope it's only building
// or processing one batch at a time.
//
// The only currently known case where this makes a difference is in the E2E
// tests in ReorderingFocusComponent, where we hit what seems like a Chrome bug
// where mutating the DOM cause an element's "change" to fire while its "input"
// handler is still running (i.e., nested on the stack) -- this doesn't happen
// in Firefox. Possibly a future version of Chrome may fix this, but even then,
// it's conceivable that DOM mutation events could trigger this too.
if (isDispatchingEvent)
{
var info = new IncomingEventInfo(eventHandlerId, eventFieldInfo, eventArgs);
deferredIncomingEvents.Enqueue(info);
return info.TaskCompletionSource.Task;
}
else
{
try
{
isDispatchingEvent = true;
return base.DispatchEventAsync(eventHandlerId, eventFieldInfo, eventArgs);
}
finally
{
isDispatchingEvent = false;
if (deferredIncomingEvents.Count > 0)
{
// Fire-and-forget because the task we return from this method should only reflect the
// completion of its own event dispatch, not that of any others that happen to be queued.
// Also, ProcessNextDeferredEventAsync deals with its own async errors.
_ = ProcessNextDeferredEventAsync();
}
}
}
}
private async Task ProcessNextDeferredEventAsync()
{
var info = deferredIncomingEvents.Dequeue();
var taskCompletionSource = info.TaskCompletionSource;
try
{
await DispatchEventAsync(info.EventHandlerId, info.EventFieldInfo, info.EventArgs);
taskCompletionSource.SetResult(null);
}
catch (Exception ex)
{
taskCompletionSource.SetException(ex);
}
}
readonly struct IncomingEventInfo
{
public readonly ulong EventHandlerId;
public readonly EventFieldInfo EventFieldInfo;
public readonly EventArgs EventArgs;
public readonly TaskCompletionSource<object> TaskCompletionSource;
public IncomingEventInfo(ulong eventHandlerId, EventFieldInfo eventFieldInfo, EventArgs eventArgs)
{
EventHandlerId = eventHandlerId;
EventFieldInfo = eventFieldInfo;
EventArgs = eventArgs;
TaskCompletionSource = new TaskCompletionSource<object>();
}
}
private static class Log
{
private static readonly Action<ILogger, string, Exception> _unhandledExceptionRenderingComponent;
private static class EventIds
{
public static readonly EventId UnhandledExceptionRenderingComponent = new EventId(100, "ExceptionRenderingComponent");
}
static Log()
{
_unhandledExceptionRenderingComponent = LoggerMessage.Define<string>(
LogLevel.Critical,
EventIds.UnhandledExceptionRenderingComponent,
"Unhandled exception rendering component: {Message}");
}
public static void UnhandledExceptionRenderingComponent(ILogger logger, Exception exception)
{
_unhandledExceptionRenderingComponent(
logger,
exception.Message,
exception);
}
}
}
}