Skip to content

Commit 0e48ee0

Browse files
Callsite rendermodes (#48967)
* builder.AddComponentRenderMode API and storage on RenderTree * Extend ResolveComponentForRenderMode to receive callerSpecifiedRenderMode, and supply it from ComponentFactory * Simplify API: collapse the two rendermode sources into a single rendermode before calling ResolveComponentForRenderMode * Corresponding renames * Actually supply callsite rendermode from RenderTree * Give clearer errors if misusing this feature * Build fix * Actually use the parameter name @rendermode for forward-compatibility with the final syntax * Add E2E tests
1 parent a36067d commit 0e48ee0

File tree

23 files changed

+638
-60
lines changed

23 files changed

+638
-60
lines changed

src/Components/Authorization/test/AuthorizeRouteViewTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public AuthorizeRouteViewTest()
3838
var services = serviceCollection.BuildServiceProvider();
3939
_renderer = new TestRenderer(services);
4040
var componentFactory = new ComponentFactory(new DefaultComponentActivator(), _renderer);
41-
_authorizeRouteViewComponent = (AuthorizeRouteView)componentFactory.InstantiateComponent(services, typeof(AuthorizeRouteView), null);
41+
_authorizeRouteViewComponent = (AuthorizeRouteView)componentFactory.InstantiateComponent(services, typeof(AuthorizeRouteView), null, null);
4242
_authorizeRouteViewComponentId = _renderer.AssignRootComponentId(_authorizeRouteViewComponent);
4343
}
4444

src/Components/Components/src/ComponentFactory.cs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,26 @@ private static ComponentTypeInfoCacheEntry GetComponentTypeInfo([DynamicallyAcce
4545
return cacheEntry;
4646
}
4747

48-
public IComponent InstantiateComponent(IServiceProvider serviceProvider, [DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId)
48+
public IComponent InstantiateComponent(IServiceProvider serviceProvider, [DynamicallyAccessedMembers(Component)] Type componentType, IComponentRenderMode? callerSpecifiedRenderMode, int? parentComponentId)
4949
{
50-
var componentTypeInfo = GetComponentTypeInfo(componentType);
51-
var component = componentTypeInfo.ComponentTypeRenderMode is null
52-
? _componentActivator.CreateInstance(componentType)
53-
: _renderer.ResolveComponentForRenderMode(componentType, parentComponentId, _componentActivator, componentTypeInfo.ComponentTypeRenderMode);
50+
var (componentTypeRenderMode, propertyInjector) = GetComponentTypeInfo(componentType);
51+
IComponent component;
52+
53+
if (componentTypeRenderMode is null && callerSpecifiedRenderMode is null)
54+
{
55+
// Typical case where no rendermode is specified in either location. We don't call ResolveComponentForRenderMode in this case.
56+
component = _componentActivator.CreateInstance(componentType);
57+
}
58+
else
59+
{
60+
// At least one rendermode is specified. We require that it's exactly one, and use ResolveComponentForRenderMode with it.
61+
var effectiveRenderMode = callerSpecifiedRenderMode is null
62+
? componentTypeRenderMode!
63+
: componentTypeRenderMode is null
64+
? callerSpecifiedRenderMode
65+
: throw new InvalidOperationException($"The component type '{componentType}' has a fixed rendermode of '{componentTypeRenderMode}', so it is not valid to specify any rendermode when using this component.");
66+
component = _renderer.ResolveComponentForRenderMode(componentType, parentComponentId, _componentActivator, effectiveRenderMode);
67+
}
5468

5569
if (component is null)
5670
{
@@ -61,7 +75,7 @@ public IComponent InstantiateComponent(IServiceProvider serviceProvider, [Dynami
6175
if (component.GetType() == componentType)
6276
{
6377
// Fast, common case: use the cached data we already looked up
64-
componentTypeInfo.PerformPropertyInjection(serviceProvider, component);
78+
propertyInjector(serviceProvider, component);
6579
}
6680
else
6781
{

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,15 @@ Microsoft.AspNetCore.Components.RenderHandle.DispatchExceptionAsync(System.Excep
4242
*REMOVED*Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string! relativeUri) -> System.Uri!
4343
Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string? relativeUri) -> System.Uri!
4444
Microsoft.AspNetCore.Components.Rendering.ComponentState.LogicalParentComponentState.get -> Microsoft.AspNetCore.Components.Rendering.ComponentState?
45+
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentParameter(int sequence, string! name, Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> void
46+
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentRenderMode(int sequence, Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> void
4547
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.SetEventHandlerName(string! eventHandlerName) -> void
4648
*REMOVED*Microsoft.AspNetCore.Components.RouteData.RouteData(System.Type! pageType, System.Collections.Generic.IReadOnlyDictionary<string!, object!>! routeValues) -> void
4749
*REMOVED*Microsoft.AspNetCore.Components.RouteData.RouteValues.get -> System.Collections.Generic.IReadOnlyDictionary<string!, object!>!
50+
Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags
51+
Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags.HasCallerSpecifiedRenderMode = 1 -> Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags
52+
Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame.ComponentFrameFlags.get -> Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags
53+
Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType.ComponentRenderMode = 9 -> Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType
4854
Microsoft.AspNetCore.Components.RouteData.RouteData(System.Type! pageType, System.Collections.Generic.IReadOnlyDictionary<string!, object?>! routeValues) -> void
4955
Microsoft.AspNetCore.Components.RouteData.RouteValues.get -> System.Collections.Generic.IReadOnlyDictionary<string!, object?>!
5056
Microsoft.AspNetCore.Components.Routing.IRoutingStateProvider
@@ -97,6 +103,7 @@ virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.DisposeAsync()
97103
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.AddPendingTask(Microsoft.AspNetCore.Components.Rendering.ComponentState? componentState, System.Threading.Tasks.Task! task) -> void
98104
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.CreateComponentState(int componentId, Microsoft.AspNetCore.Components.IComponent! component, Microsoft.AspNetCore.Components.Rendering.ComponentState? parentComponentState) -> Microsoft.AspNetCore.Components.Rendering.ComponentState!
99105
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo? fieldInfo, System.EventArgs! eventArgs, bool quiesce) -> System.Threading.Tasks.Task!
100-
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.ResolveComponentForRenderMode(System.Type! componentType, int? parentComponentId, Microsoft.AspNetCore.Components.IComponentActivator! componentActivator, Microsoft.AspNetCore.Components.IComponentRenderMode! componentTypeRenderMode) -> Microsoft.AspNetCore.Components.IComponent!
106+
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.ResolveComponentForRenderMode(System.Type! componentType, int? parentComponentId, Microsoft.AspNetCore.Components.IComponentActivator! componentActivator, Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> Microsoft.AspNetCore.Components.IComponent!
101107
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.ShouldTrackNamedEventHandlers() -> bool
102108
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.TrackNamedEventId(ulong eventHandlerId, int componentId, string! eventHandlerName) -> void
109+
~Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame.ComponentRenderMode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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+
namespace Microsoft.AspNetCore.Components.RenderTree;
5+
6+
/// <summary>
7+
/// Types in the Microsoft.AspNetCore.Components.RenderTree namespace are not recommended for use outside
8+
/// of the Blazor framework. These types will change in future release.
9+
/// </summary>
10+
[Flags]
11+
public enum ComponentFrameFlags : byte
12+
{
13+
/// <summary>
14+
/// Indicates that the caller has specified a render mode.
15+
/// </summary>
16+
HasCallerSpecifiedRenderMode = 1,
17+
}

src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -953,7 +953,7 @@ private static void InitializeNewComponentFrame(ref DiffContext diffContext, int
953953
var frames = diffContext.NewTree;
954954
ref var frame = ref frames[frameIndex];
955955
var parentComponentId = diffContext.ComponentId;
956-
var childComponentState = diffContext.Renderer.InstantiateChildComponentOnFrame(ref frame, parentComponentId);
956+
var childComponentState = diffContext.Renderer.InstantiateChildComponentOnFrame(frames, frameIndex, parentComponentId);
957957

958958
// Set initial parameters
959959
var initialParametersLifetime = new ParameterViewLifetime(diffContext.BatchBuilder);

src/Components/Components/src/RenderTree/RenderTreeFrame.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ public struct RenderTreeFrame
136136
// RenderTreeFrameType.Component
137137
// --------------------------------------------------------------------------------
138138

139+
[FieldOffset(6)] internal ComponentFrameFlags ComponentFrameFlagsField;
139140
[FieldOffset(8)] internal int ComponentSubtreeLengthField;
140141
[FieldOffset(12)] internal int ComponentIdField;
141142
[FieldOffset(16)]
@@ -144,6 +145,12 @@ public struct RenderTreeFrame
144145
[FieldOffset(24)] internal ComponentState ComponentStateField;
145146
[FieldOffset(32)] internal object ComponentKeyField;
146147

148+
/// <summary>
149+
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.Component"/>
150+
/// gets the <see cref="ComponentFrameFlags"/> for the component frame.
151+
/// </summary>
152+
public ComponentFrameFlags ComponentFrameFlags => ComponentFrameFlagsField;
153+
147154
/// <summary>
148155
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.Component"/>
149156
/// gets the number of frames in the subtree for which this frame is the root.
@@ -250,6 +257,31 @@ public struct RenderTreeFrame
250257
/// </summary>
251258
public string MarkupContent => MarkupContentField;
252259

260+
// --------------------------------------------------------------------------------
261+
// RenderTreeFrameType.ComponentRenderMode
262+
// --------------------------------------------------------------------------------
263+
264+
[FieldOffset(16)] internal IComponentRenderMode ComponentRenderModeField;
265+
266+
/// <summary>
267+
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.ComponentRenderMode"/>,
268+
/// gets the specified <see cref="IComponentRenderMode"/>. Otherwise, the value is undefined.
269+
/// </summary>
270+
public IComponentRenderMode ComponentRenderMode
271+
{
272+
get
273+
{
274+
// Normally we don't check the frame type matches, and leave it to the caller to be responsible for only evaluating the correct properties.
275+
// However the name "ComponentRenderMode" sounds so much like it would be a field on Component frames that we'll explicitly check to avoid mistakes.
276+
if (FrameType != RenderTreeFrameType.ComponentRenderMode)
277+
{
278+
throw new InvalidOperationException($"The {nameof(ComponentRenderMode)} field only exists on frames of type {nameof(RenderTreeFrameType.ComponentRenderMode)}.");
279+
}
280+
281+
return ComponentRenderModeField;
282+
}
283+
}
284+
253285
// Element constructor
254286
private RenderTreeFrame(int sequence, int elementSubtreeLength, string elementName, object elementKey)
255287
: this()

src/Components/Components/src/RenderTree/RenderTreeFrameArrayBuilder.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,19 @@ public void AppendRegion(int sequence)
134134
FrameTypeField = RenderTreeFrameType.Region,
135135
};
136136
}
137+
138+
public void AppendComponentRenderMode(int sequence, IComponentRenderMode renderMode)
139+
{
140+
if (_itemsInUse == _items.Length)
141+
{
142+
GrowBuffer(_items.Length * 2);
143+
}
144+
145+
_items[_itemsInUse++] = new RenderTreeFrame
146+
{
147+
SequenceField = sequence,
148+
FrameTypeField = RenderTreeFrameType.ComponentRenderMode,
149+
ComponentRenderModeField = renderMode,
150+
};
151+
}
137152
}

src/Components/Components/src/RenderTree/RenderTreeFrameType.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,9 @@ public enum RenderTreeFrameType : short
5858
/// Represents a block of markup content.
5959
/// </summary>
6060
Markup = 8,
61+
62+
/// <summary>
63+
/// Represents an instruction to use a specified render mode for the component.
64+
/// </summary>
65+
ComponentRenderMode = 9,
6166
}

src/Components/Components/src/RenderTree/Renderer.cs

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ await Dispatcher.InvokeAsync(() =>
164164
/// <param name="componentType">The type of the component to instantiate.</param>
165165
/// <returns>The component instance.</returns>
166166
protected IComponent InstantiateComponent([DynamicallyAccessedMembers(Component)] Type componentType)
167-
=> _componentFactory.InstantiateComponent(_serviceProvider, componentType, null);
167+
=> _componentFactory.InstantiateComponent(_serviceProvider, componentType, null, null);
168168

169169
/// <summary>
170170
/// Associates the <see cref="IComponent"/> with the <see cref="Renderer"/>, assigning
@@ -475,26 +475,55 @@ public Type GetEventArgsType(ulong eventHandlerId)
475475
: EventArgsTypeCache.GetEventArgsType(methodInfo);
476476
}
477477

478-
internal ComponentState InstantiateChildComponentOnFrame(ref RenderTreeFrame frame, int parentComponentId)
478+
internal ComponentState InstantiateChildComponentOnFrame(RenderTreeFrame[] frames, int frameIndex, int parentComponentId)
479479
{
480+
ref var frame = ref frames[frameIndex];
480481
if (frame.FrameTypeField != RenderTreeFrameType.Component)
481482
{
482-
throw new ArgumentException($"The frame's {nameof(RenderTreeFrame.FrameType)} property must equal {RenderTreeFrameType.Component}", nameof(frame));
483+
throw new ArgumentException($"The frame's {nameof(RenderTreeFrame.FrameType)} property must equal {RenderTreeFrameType.Component}", nameof(frameIndex));
483484
}
484485

485486
if (frame.ComponentStateField != null)
486487
{
487-
throw new ArgumentException($"The frame already has a non-null component instance", nameof(frame));
488+
throw new ArgumentException($"The frame already has a non-null component instance", nameof(frameIndex));
488489
}
489490

490-
var newComponent = _componentFactory.InstantiateComponent(_serviceProvider, frame.ComponentTypeField, parentComponentId);
491+
var callerSpecifiedRenderMode = frame.ComponentFrameFlags.HasFlag(ComponentFrameFlags.HasCallerSpecifiedRenderMode)
492+
? FindCallerSpecifiedRenderMode(frames, frameIndex)
493+
: null;
494+
495+
var newComponent = _componentFactory.InstantiateComponent(_serviceProvider, frame.ComponentTypeField, callerSpecifiedRenderMode, parentComponentId);
491496
var newComponentState = AttachAndInitComponent(newComponent, parentComponentId);
492497
frame.ComponentStateField = newComponentState;
493498
frame.ComponentIdField = newComponentState.ComponentId;
494499

495500
return newComponentState;
496501
}
497502

503+
private static IComponentRenderMode? FindCallerSpecifiedRenderMode(RenderTreeFrame[] frames, int componentFrameIndex)
504+
{
505+
// ComponentRenderMode frames are immediate children of Component frames. So, they have to appear after any parameter
506+
// attributes (since attributes must always immediately follow Component frames), but before anything that would
507+
// represent a different child node, such as text/element or another component. It's OK to do this linear scan
508+
// because we consider it uncommon to specify a rendermode, and none of this happens if you don't.
509+
var endIndex = componentFrameIndex + frames[componentFrameIndex].ComponentSubtreeLengthField;
510+
for (var index = componentFrameIndex + 1; index <= endIndex; index++)
511+
{
512+
ref var frame = ref frames[index];
513+
switch (frame.FrameType)
514+
{
515+
case RenderTreeFrameType.Attribute:
516+
continue;
517+
case RenderTreeFrameType.ComponentRenderMode:
518+
return frame.ComponentRenderMode;
519+
default:
520+
break;
521+
}
522+
}
523+
524+
return null;
525+
}
526+
498527
internal void AddToPendingTasksWithErrorHandling(Task task, ComponentState? owningComponentState)
499528
{
500529
switch (task == null ? TaskStatus.RanToCompletion : task.Status)
@@ -1146,23 +1175,24 @@ void NotifyExceptions(List<Exception> exceptions)
11461175

11471176
/// <summary>
11481177
/// Determines how to handle an <see cref="IComponentRenderMode"/> when obtaining a component instance.
1149-
/// This is only called for components that have specified a render mode. Subclasses may override this
1150-
/// method to return a component of a different type, or throw, depending on whether the renderer
1178+
/// This is only called when a render mode is specified either at the call site or on the component type.
1179+
///
1180+
/// Subclasses may override this method to return a component of a different type, or throw, depending on whether the renderer
11511181
/// supports the render mode and how it implements that support.
11521182
/// </summary>
11531183
/// <param name="componentType">The type of component that was requested.</param>
11541184
/// <param name="parentComponentId">The parent component ID, or null if it is a root component.</param>
11551185
/// <param name="componentActivator">An <see cref="IComponentActivator"/> that should be used when instantiating component objects.</param>
1156-
/// <param name="componentTypeRenderMode">The <see cref="IComponentRenderMode"/> declared on <paramref name="componentType"/>.</param>
1186+
/// <param name="renderMode">The <see cref="IComponentRenderMode"/> declared on <paramref name="componentType"/> or at the call site (for example, by the parent component).</param>
11571187
/// <returns>An <see cref="IComponent"/> instance.</returns>
11581188
protected internal virtual IComponent ResolveComponentForRenderMode(
11591189
[DynamicallyAccessedMembers(Component)] Type componentType,
11601190
int? parentComponentId,
11611191
IComponentActivator componentActivator,
1162-
IComponentRenderMode componentTypeRenderMode)
1192+
IComponentRenderMode renderMode)
11631193
{
11641194
// Nothing is supported by default. Subclasses must override this to opt into supporting specific render modes.
1165-
throw new NotSupportedException($"Cannot supply a component of type '{componentType}' because the current platform does not support the render mode '{componentTypeRenderMode}'.");
1195+
throw new NotSupportedException($"Cannot supply a component of type '{componentType}' because the current platform does not support the render mode '{renderMode}'.");
11661196
}
11671197

11681198
/// <summary>

0 commit comments

Comments
 (0)