Skip to content

Commit ed330ff

Browse files
Supply passive content as RenderFragment to interactive components (#14)
1 parent 375f123 commit ed330ff

File tree

9 files changed

+138
-24
lines changed

9 files changed

+138
-24
lines changed

src/Components/Server/src/Circuits/ComponentParameterDeserializer.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ public bool TryDeserializeParameters(IList<ComponentParameter> parametersDefinit
5757
Log.InvalidParameterType(_logger, definition.Name, definition.TypeName, definition.Assembly);
5858
return false;
5959
}
60+
61+
// As a special case, RenderFragment parameters are prerendered on the server and serialized as HTML
62+
// strings. We'll reconstruct a RenderFragment that outputs this HTML as a markup string.
63+
if (parameterType == typeof(RenderFragment))
64+
{
65+
var value = (JsonElement)parameterValues[i];
66+
parametersDictionary.Add(definition.Name,
67+
CreateRenderFragmentFromPrerenderedMarkup(value.GetString()));
68+
continue;
69+
}
70+
6071
try
6172
{
6273
// At this point we know the parameter is not null, as we don't serialize the type name or the assembly name
@@ -81,6 +92,9 @@ public bool TryDeserializeParameters(IList<ComponentParameter> parametersDefinit
8192
return true;
8293
}
8394

95+
private static RenderFragment CreateRenderFragmentFromPrerenderedMarkup(string html)
96+
=> builder => builder.AddContent(0, new MarkupString(html));
97+
8498
private static partial class Log
8599
{
86100
[LoggerMessage(1, LogLevel.Debug, "Parameter values must be an array.", EventName = "ParameterValuesInvalidFormat")]

src/Components/Server/test/Circuits/ServerComponentDeserializerTest.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ private ServerComponentMarker[] CreateMarkers(params Type[] types)
326326
var markers = new ServerComponentMarker[types.Length];
327327
for (var i = 0; i < types.Length; i++)
328328
{
329-
markers[i] = serializer.SerializeInvocation(_invocationSequence, types[i], ParameterView.Empty, false);
329+
markers[i] = serializer.SerializeInvocation(_invocationSequence, types[i], ParameterView.Empty, false, null);
330330
}
331331

332332
return markers;
@@ -343,7 +343,8 @@ private ServerComponentMarker[] CreateMarkers(params (Type, Dictionary<string, o
343343
_invocationSequence,
344344
type,
345345
parameters == null ? ParameterView.Empty : ParameterView.FromDictionary(parameters),
346-
false);
346+
false,
347+
null);
347348
}
348349

349350
return markers;
@@ -355,7 +356,7 @@ private ServerComponentMarker[] CreateMarkers(ServerComponentInvocationSequence
355356
var markers = new ServerComponentMarker[types.Length];
356357
for (var i = 0; i < types.Length; i++)
357358
{
358-
markers[i] = serializer.SerializeInvocation(sequence, types[i], ParameterView.Empty, false);
359+
markers[i] = serializer.SerializeInvocation(sequence, types[i], ParameterView.Empty, false, null);
359360
}
360361

361362
return markers;

src/Components/Web/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ Microsoft.AspNetCore.Components.Web.MouseEventArgs.MovementX.set -> void
1414
Microsoft.AspNetCore.Components.Web.MouseEventArgs.MovementY.get -> double
1515
Microsoft.AspNetCore.Components.Web.MouseEventArgs.MovementY.set -> void
1616
Microsoft.AspNetCore.Components.Web.WebComponentRenderMode
17+
static readonly Microsoft.AspNetCore.Components.Web.WebComponentRenderMode.Auto -> Microsoft.AspNetCore.Components.Web.WebComponentRenderMode!
1718
static readonly Microsoft.AspNetCore.Components.Web.WebComponentRenderMode.Server -> Microsoft.AspNetCore.Components.Web.WebComponentRenderMode!
1819
static readonly Microsoft.AspNetCore.Components.Web.WebComponentRenderMode.WebAssembly -> Microsoft.AspNetCore.Components.Web.WebComponentRenderMode!

src/Components/WebAssembly/WebAssembly/src/Prerendering/ClientComponentParameterDeserializer.cs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,25 @@ public ParameterView DeserializeParameters(IList<ComponentParameter> parametersD
5656
try
5757
{
5858
var value = (JsonElement)parameterValues[i];
59-
var parameterValue = JsonSerializer.Deserialize(
60-
value.GetRawText(),
61-
parameterType,
62-
WebAssemblyComponentSerializationSettings.JsonSerializationOptions);
59+
object parameterValue;
60+
if (parameterType == typeof(RenderFragment))
61+
{
62+
var markup = new MarkupString(value.GetString()!);
63+
parameterValue = (RenderFragment)(builder => builder.AddContent(0, markup));
64+
}
65+
else
66+
{
67+
parameterValue = JsonSerializer.Deserialize(
68+
value.GetRawText(),
69+
parameterType,
70+
WebAssemblyComponentSerializationSettings.JsonSerializationOptions);
71+
}
6372

6473
parametersDictionary[definition.Name] = parameterValue;
6574
}
6675
catch (Exception e)
6776
{
68-
throw new InvalidOperationException("Could not parse the parameter value for parameter '{definition.Name}' of type '{definition.TypeName}' and assembly '{definition.Assembly}'.", e);
77+
throw new InvalidOperationException($"Could not parse the parameter value for parameter '{definition.Name}' of type '{definition.TypeName}' and assembly '{definition.Assembly}'.", e);
6978
}
7079
}
7180
}

src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderer.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ private async Task<IHtmlContent> PrerenderedServerComponentAsync(HttpContext con
138138
invocationId,
139139
type,
140140
parametersCollection,
141-
prerendered: true);
141+
prerendered: true,
142+
null);
142143

143144
var result = await _staticComponentRenderer.PrerenderComponentAsync(
144145
parametersCollection,
@@ -159,7 +160,8 @@ private async ValueTask<IHtmlContent> PrerenderedWebAssemblyComponentAsync(HttpC
159160
var currentInvocation = WebAssemblyComponentSerializer.SerializeInvocation(
160161
type,
161162
parametersCollection,
162-
prerendered: true);
163+
prerendered: true,
164+
null);
163165

164166
var result = await _staticComponentRenderer.PrerenderComponentAsync(
165167
parametersCollection,
@@ -182,7 +184,7 @@ private IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerCo
182184
context.Response.Headers.CacheControl = "no-cache, no-store, max-age=0";
183185
}
184186

185-
var currentInvocation = _serverComponentSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false);
187+
var currentInvocation = _serverComponentSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false, null);
186188

187189
var viewBuffer = new ViewBuffer(_viewBufferScope, nameof(ComponentRenderer), ServerComponentSerializer.PreambleBufferSize);
188190
ServerComponentSerializer.AppendPreamble(viewBuffer, currentInvocation);
@@ -191,7 +193,7 @@ private IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerCo
191193

192194
private IHtmlContent NonPrerenderedWebAssemblyComponent(HttpContext context, Type type, ParameterView parametersCollection)
193195
{
194-
var currentInvocation = WebAssemblyComponentSerializer.SerializeInvocation(type, parametersCollection, prerendered: false);
196+
var currentInvocation = WebAssemblyComponentSerializer.SerializeInvocation(type, parametersCollection, prerendered: false, null);
195197
var viewBuffer = new ViewBuffer(_viewBufferScope, nameof(ComponentRenderer), ServerComponentSerializer.PreambleBufferSize);
196198
WebAssemblyComponentSerializer.AppendPreamble(viewBuffer, currentInvocation);
197199
return viewBuffer;

src/Mvc/Mvc.ViewFeatures/src/RazorComponents/HtmlRenderer.cs

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,17 @@ public Task<ComponentRenderedText> RenderComponentAsync<TComponent>(ParameterVie
106106
protected override void HandleException(Exception exception)
107107
=> ExceptionDispatchInfo.Capture(exception).Throw();
108108

109+
private string RenderTreeToHtmlString(ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
110+
{
111+
var viewBuffer = new ViewBuffer(_viewBufferScope, nameof(HtmlRenderer), ViewBuffer.ViewPageSize);
112+
var context = new HtmlRenderingContext(this, viewBuffer, _serviceProvider);
113+
RenderFrames(context, frames, position, maxElements);
114+
115+
using var sw = new StringWriter();
116+
viewBuffer.WriteTo(sw, HtmlEncoder.Default);
117+
return sw.GetStringBuilder().ToString();
118+
}
119+
109120
private int RenderFrames(HtmlRenderingContext context, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
110121
{
111122
var nextPosition = position;
@@ -306,7 +317,7 @@ private static int RenderAttributes(
306317
private ViewBuffer GetRenderedHtmlContent(int componentId)
307318
{
308319
var viewBuffer = new ViewBuffer(_viewBufferScope, nameof(HtmlRenderer), ViewBuffer.ViewPageSize);
309-
var context = new HtmlRenderingContext(viewBuffer, _serviceProvider);
320+
var context = new HtmlRenderingContext(this, viewBuffer, _serviceProvider);
310321

311322
var frames = GetCurrentRenderTreeFrames(componentId);
312323
var newPosition = RenderFrames(context, frames, 0, frames.Count);
@@ -368,12 +379,14 @@ public void AppendEpilogue(ViewBuffer htmlContentBuilder)
368379
private sealed class HtmlRenderingContext
369380
{
370381
private readonly IServiceProvider _serviceProvider;
382+
private readonly HtmlRenderer _htmlRenderer;
371383
private ServerComponentSerializer _serverComponentSerializer;
372384
private ServerComponentInvocationSequence _serverComponentSequence;
373385

374-
public HtmlRenderingContext(ViewBuffer viewBuffer, IServiceProvider serviceProvider)
386+
public HtmlRenderingContext(HtmlRenderer htmlRenderer, ViewBuffer viewBuffer, IServiceProvider serviceProvider)
375387
{
376388
HtmlContentBuilder = viewBuffer;
389+
_htmlRenderer = htmlRenderer;
377390
_serviceProvider = serviceProvider;
378391
}
379392

@@ -399,13 +412,13 @@ public InteractiveComponentMarker SerializeInvocation(ArrayRange<RenderTreeFrame
399412
_serverComponentSequence = new();
400413
}
401414

402-
serverMarker = _serverComponentSerializer.SerializeInvocation(_serverComponentSequence, componentFrame.ComponentType, parameters, prerendered: true);
415+
serverMarker = _serverComponentSerializer.SerializeInvocation(_serverComponentSequence, componentFrame.ComponentType, parameters, prerendered: true, PrepareRenderFragment);
403416
}
404417

405418
if (renderModeNumericValue == WebComponentRenderMode.WebAssembly.NumericValue
406419
|| renderModeNumericValue == WebComponentRenderMode.Auto.NumericValue)
407420
{
408-
webAssemblyMarker = WebAssemblyComponentSerializer.SerializeInvocation(componentFrame.ComponentType, parameters, prerendered: true);
421+
webAssemblyMarker = WebAssemblyComponentSerializer.SerializeInvocation(componentFrame.ComponentType, parameters, prerendered: true, PrepareRenderFragment);
409422
}
410423

411424
if (!serverMarker.HasValue && !webAssemblyMarker.HasValue)
@@ -415,6 +428,76 @@ public InteractiveComponentMarker SerializeInvocation(ArrayRange<RenderTreeFrame
415428

416429
return new InteractiveComponentMarker(serverMarker, webAssemblyMarker);
417430
}
431+
432+
private string PrepareRenderFragment(RenderFragment fragment)
433+
{
434+
// We can't just execute the RenderFragment delegate directly. We have to run it through the
435+
// renderer so that the renderer can do all the normal things to activate child components
436+
// and run their full lifecycle (disposal, etc.)
437+
var rootComponent = new FragmentRenderer { Fragment = fragment };
438+
string initialHtml = null;
439+
var renderTask = _htmlRenderer.Dispatcher.InvokeAsync(async () =>
440+
{
441+
// WARNING: THIS IS NOT CORRECT AND CAN CAUSE AN INFINITE LOOP
442+
// We should *not* really be creating new root components as a side-effect of
443+
// parameter serialization, because that means the child content within this
444+
// RenderFragment subtree is recreated on each parameter serialization and may
445+
// be repeating work, or worse, keep causing ancestor components to re-render
446+
// and hence an infinite loop.
447+
// Instead, all this work should be done in a completely different place: the
448+
// diffing system. When the diffing system sees that a child component has "interactive"
449+
// rendermode, any RenderFragment parameters should be handled by creating something
450+
// like a FragmentRenderer instance as a new component and referencing it from the
451+
// the parameter's attribute frame. Then on successive diffs, we can reuse the
452+
// FragmentRenderer and hence preserve descendant components. Also this means we would
453+
// no longer be doing the RenderFragment rendering during parameter serialization: we'd
454+
// be doing it during diffing, so it would be integrated into normal the rendering flow.
455+
// The parameter serialization would then only need to know that for this special parameter
456+
// type, it can find the associated FragmentRenderer that already exists, and serialize
457+
// its render frames that already exist. And I think that means we don't have to deal
458+
// with asynchrony during the parameter serialization at all.
459+
var rootComponentId = _htmlRenderer.AssignRootComponentId(rootComponent);
460+
var renderTask = _htmlRenderer.RenderRootComponentAsync(rootComponentId);
461+
462+
var frames = _htmlRenderer.GetCurrentRenderTreeFrames(rootComponentId);
463+
initialHtml = _htmlRenderer.RenderTreeToHtmlString(frames, 0, frames.Count);
464+
});
465+
466+
// TODO: Include renderTask in the set of tasks we'd await if the top-level call
467+
// asked us to await quiescence. And if we're not awaiting quiescence, at least
468+
// merge it into the overall top-level returned task for error handling. The following
469+
// logic is just a cheap stand-in.
470+
if (renderTask.IsFaulted)
471+
{
472+
throw renderTask.Exception;
473+
}
474+
475+
if (!renderTask.IsCompleted)
476+
{
477+
// Fail fast rather than the infinite loop mentioned above.
478+
throw new InvalidOperationException("The current implementation of RenderFragment serialization doesn't support child content that performs async work.");
479+
}
480+
481+
return initialHtml;
482+
}
483+
484+
class FragmentRenderer : IComponent
485+
{
486+
RenderHandle _renderHandle;
487+
488+
public RenderFragment Fragment { get; set; }
489+
490+
public void Attach(RenderHandle renderHandle)
491+
{
492+
_renderHandle = renderHandle;
493+
}
494+
495+
public Task SetParametersAsync(ParameterView parameters)
496+
{
497+
_renderHandle.Render(Fragment);
498+
return Task.CompletedTask;
499+
}
500+
}
418501
}
419502

420503
/// <summary>

src/Mvc/Mvc.ViewFeatures/src/ServerComponentSerializer.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,21 @@ public ServerComponentSerializer(IDataProtectionProvider dataProtectionProvider)
2020
.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
2121
.ToTimeLimitedDataProtector();
2222

23-
public ServerComponentMarker SerializeInvocation(ServerComponentInvocationSequence invocationId, Type type, ParameterView parameters, bool prerendered)
23+
public ServerComponentMarker SerializeInvocation(ServerComponentInvocationSequence invocationId, Type type, ParameterView parameters, bool prerendered, Func<RenderFragment, string> renderFragmentSerializer)
2424
{
25-
var (sequence, serverComponent) = CreateSerializedServerComponent(invocationId, type, parameters);
25+
var (sequence, serverComponent) = CreateSerializedServerComponent(invocationId, type, parameters, renderFragmentSerializer);
2626
return prerendered ? ServerComponentMarker.Prerendered(sequence, serverComponent) : ServerComponentMarker.NonPrerendered(sequence, serverComponent);
2727
}
2828

2929
private (int sequence, string payload) CreateSerializedServerComponent(
3030
ServerComponentInvocationSequence invocationId,
3131
Type rootComponent,
32-
ParameterView parameters)
32+
ParameterView parameters,
33+
Func<RenderFragment, string> renderFragmentSerializer)
3334
{
3435
var sequence = invocationId.Next();
3536

36-
var (definitions, values) = ComponentParameter.FromParameterView(parameters);
37+
var (definitions, values) = ComponentParameter.FromParameterView(parameters, renderFragmentSerializer);
3738

3839
var serverComponent = new ServerComponent(
3940
sequence,

src/Mvc/Mvc.ViewFeatures/src/WebAssemblyComponentSerializer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures;
1010
// See the details of the component serialization protocol in WebAssemblyComponentDeserializer.cs on the Components solution.
1111
internal sealed class WebAssemblyComponentSerializer
1212
{
13-
public static WebAssemblyComponentMarker SerializeInvocation(Type type, ParameterView parameters, bool prerendered)
13+
public static WebAssemblyComponentMarker SerializeInvocation(Type type, ParameterView parameters, bool prerendered, Func<RenderFragment, string> renderFragmentSerializer)
1414
{
1515
var assembly = type.Assembly.GetName().Name;
1616
var typeFullName = type.FullName;
17-
var (definitions, values) = ComponentParameter.FromParameterView(parameters);
17+
var (definitions, values) = ComponentParameter.FromParameterView(parameters, renderFragmentSerializer);
1818

1919
// We need to serialize and Base64 encode parameters separately since they can contain arbitrary data that might
2020
// cause the HTML comment to be invalid (like if you serialize a string that contains two consecutive dashes "--").

src/Shared/Components/ComponentParameter.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ internal struct ComponentParameter
1313
public string? TypeName { get; set; }
1414
public string? Assembly { get; set; }
1515

16-
public static (IList<ComponentParameter> parameterDefinitions, IList<object?> parameterValues) FromParameterView(ParameterView parameters)
16+
public static (IList<ComponentParameter> parameterDefinitions, IList<object?> parameterValues) FromParameterView(ParameterView parameters, Func<RenderFragment, string>? renderFragmentSerializer)
1717
{
1818
var parameterDefinitions = new List<ComponentParameter>();
1919
var parameterValues = new List<object?>();
@@ -27,7 +27,10 @@ public static (IList<ComponentParameter> parameterDefinitions, IList<object?> pa
2727
Assembly = valueType?.Assembly?.GetName()?.Name
2828
});
2929

30-
parameterValues.Add(kvp.Value);
30+
var value = kvp.Value is RenderFragment fragment && renderFragmentSerializer is not null
31+
? renderFragmentSerializer(fragment)
32+
: kvp.Value;
33+
parameterValues.Add(value);
3134
}
3235

3336
return (parameterDefinitions, parameterValues);

0 commit comments

Comments
 (0)