Skip to content

Commit 67aa9d5

Browse files
Support layouts and anonymously-typed parameter objects
1 parent 5ce5ca6 commit 67aa9d5

File tree

4 files changed

+102
-15
lines changed

4 files changed

+102
-15
lines changed

src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ Microsoft.AspNetCore.Mvc.Diagnostics.BeforeRazorComponentEventData
55
Microsoft.AspNetCore.Mvc.Diagnostics.BeforeRazorComponentEventData.RenderMode.get -> Microsoft.AspNetCore.Mvc.Rendering.RenderMode
66
Microsoft.AspNetCore.Mvc.Rendering.FormMethod.Dialog = 2 -> Microsoft.AspNetCore.Mvc.Rendering.FormMethod
77
Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult
8-
Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.Parameters.get -> Microsoft.AspNetCore.Components.ParameterView
9-
Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.Parameters.set -> void
8+
~Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.Parameters.get -> object
9+
~Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.Parameters.set -> void
1010
Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.RenderMode.get -> Microsoft.AspNetCore.Mvc.Rendering.RenderMode
1111
Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.RenderMode.set -> void
1212
Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.StatusCode.get -> int?

src/Mvc/Mvc.ViewFeatures/src/RazorComponents/RazorComponentResult.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public RazorComponentResult(Type componentType)
4343
/// <summary>
4444
/// Gets or sets the parameters for the component.
4545
/// </summary>
46-
public ParameterView Parameters { get; set; } = ParameterView.Empty;
46+
public object Parameters { get; set; }
4747

4848
/// <summary>
4949
/// Gets or sets the rendering mode.

src/Mvc/Mvc.ViewFeatures/src/RazorComponents/RazorComponentResultExecutor.cs

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Diagnostics;
55
using System.Text;
6+
using Microsoft.AspNetCore.Components;
67
using Microsoft.AspNetCore.Internal;
78
using Microsoft.AspNetCore.Mvc.Formatters;
89
using Microsoft.AspNetCore.Mvc.Infrastructure;
@@ -70,25 +71,34 @@ public virtual async Task ExecuteAsync(ActionContext actionContext, RazorCompone
7071
}
7172

7273
await using var writer = WriterFactory.CreateWriter(response.Body, resolvedContentTypeEncoding);
74+
DiagnosticListener.BeforeRazorComponent(result.ComponentType, result.RenderMode, actionContext);
75+
await RenderComponentToResponse(actionContext, result, writer);
76+
DiagnosticListener.AfterRazorComponent(result.ComponentType, result.RenderMode, actionContext);
7377

74-
var componentPrerenderer = actionContext.HttpContext.RequestServices.GetRequiredService<ComponentPrerenderer>();
78+
// Perf: Invoke FlushAsync to ensure any buffered content is asynchronously written to the underlying
79+
// response asynchronously. In the absence of this line, the buffer gets synchronously written to the
80+
// response as part of the Dispose which has a perf impact.
81+
await writer.FlushAsync();
82+
}
7583

76-
DiagnosticListener.BeforeRazorComponent(result.ComponentType, result.RenderMode, actionContext);
77-
await componentPrerenderer.Dispatcher.InvokeAsync(async () =>
84+
private static Task RenderComponentToResponse(ActionContext actionContext, RazorComponentResult result, TextWriter writer)
85+
{
86+
var componentPrerenderer = actionContext.HttpContext.RequestServices.GetRequiredService<ComponentPrerenderer>();
87+
return componentPrerenderer.Dispatcher.InvokeAsync(async () =>
7888
{
89+
// We could pool these dictionary instances if we wanted. We could even skip the dictionary
90+
// phase entirely if we added some new ParameterView.FromSingleValue(name, value) API.
91+
var hostParameters = ParameterView.FromDictionary(new Dictionary<string, object>
92+
{
93+
{ nameof(RazorComponentResultHost.RazorComponentResult), result },
94+
});
95+
7996
var htmlContent = await componentPrerenderer.PrerenderComponentAsync(
8097
actionContext,
81-
result.ComponentType,
98+
typeof(RazorComponentResultHost),
8299
result.RenderMode,
83-
result.Parameters);
100+
hostParameters);
84101
await htmlContent.WriteToAsync(writer);
85102
});
86-
87-
DiagnosticListener.AfterRazorComponent(result.ComponentType, result.RenderMode, actionContext);
88-
89-
// Perf: Invoke FlushAsync to ensure any buffered content is asynchronously written to the underlying
90-
// response asynchronously. In the absence of this line, the buffer gets synchronously written to the
91-
// response as part of the Dispose which has a perf impact.
92-
await writer.FlushAsync();
93103
}
94104
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Reflection;
5+
using Microsoft.AspNetCore.Components;
6+
using Microsoft.AspNetCore.Components.Rendering;
7+
using Microsoft.Extensions.Internal;
8+
9+
namespace Microsoft.AspNetCore.Mvc.ViewFeatures;
10+
11+
/// <summary>
12+
/// Internal component type that acts as a root component when executing a RazorComponentResult. It takes
13+
/// care of rendering the component inside its hierarchy of layouts (via LayoutView) as well as converting
14+
/// any information we want from RazorComponentResult into component parameters. We could also use this to
15+
/// supply other data from the original HttpContext as component parameters, e.g., for model binding.
16+
///
17+
/// It happens to be almost the same as RouteView except it doesn't supply any query parameters. We can
18+
/// resolve that at the same time we implement support for form posts.
19+
/// </summary>
20+
internal class RazorComponentResultHost : IComponent
21+
{
22+
private RenderHandle _renderHandle;
23+
24+
public RazorComponentResult RazorComponentResult { get; private set; }
25+
26+
public void Attach(RenderHandle renderHandle)
27+
=> _renderHandle = renderHandle;
28+
29+
public Task SetParametersAsync(ParameterView parameters)
30+
{
31+
foreach (var kvp in parameters)
32+
{
33+
switch (kvp.Name)
34+
{
35+
case nameof(RazorComponentResult):
36+
RazorComponentResult = (RazorComponentResult)kvp.Value;
37+
break;
38+
default:
39+
throw new ArgumentException($"Unknown parameter '{kvp.Name}'");
40+
}
41+
}
42+
43+
if (RazorComponentResult is null)
44+
{
45+
throw new InvalidOperationException($"Failed to supply nonnull value for required parameter '{nameof(RazorComponentResult)}'");
46+
}
47+
48+
_renderHandle.Render(BuildRenderTree);
49+
return Task.CompletedTask;
50+
}
51+
52+
private void BuildRenderTree(RenderTreeBuilder builder)
53+
{
54+
var pageLayoutType = RazorComponentResult.ComponentType.GetCustomAttribute<LayoutAttribute>()?.LayoutType;
55+
56+
builder.OpenComponent<LayoutView>(0);
57+
builder.AddComponentParameter(1, nameof(LayoutView.Layout), pageLayoutType);
58+
builder.AddComponentParameter(2, nameof(LayoutView.ChildContent), (RenderFragment)RenderPageWithParameters);
59+
builder.CloseComponent();
60+
}
61+
62+
private void RenderPageWithParameters(RenderTreeBuilder builder)
63+
{
64+
builder.OpenComponent(0, RazorComponentResult.ComponentType);
65+
66+
if (RazorComponentResult.Parameters is not null)
67+
{
68+
var dict = PropertyHelper.ObjectToDictionary(RazorComponentResult.Parameters);
69+
foreach (var kvp in dict)
70+
{
71+
builder.AddComponentParameter(1, kvp.Key, kvp.Value);
72+
}
73+
}
74+
75+
builder.CloseComponent();
76+
}
77+
}

0 commit comments

Comments
 (0)