Skip to content

Commit ec9a2d8

Browse files
Allow declaring render modes, and emit the corresponding markers into HTML (#48190)
1 parent 2bb4f08 commit ec9a2d8

File tree

45 files changed

+1050
-315
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1050
-315
lines changed

src/Components/Authorization/test/AuthorizeRouteViewTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ public AuthorizeRouteViewTest()
3535

3636
var services = serviceCollection.BuildServiceProvider();
3737
_renderer = new TestRenderer(services);
38-
var componentFactory = new ComponentFactory(new DefaultComponentActivator());
39-
_authorizeRouteViewComponent = (AuthorizeRouteView)componentFactory.InstantiateComponent(services, typeof(AuthorizeRouteView));
38+
var componentFactory = new ComponentFactory(new DefaultComponentActivator(), _renderer);
39+
_authorizeRouteViewComponent = (AuthorizeRouteView)componentFactory.InstantiateComponent(services, typeof(AuthorizeRouteView), null);
4040
_authorizeRouteViewComponentId = _renderer.AssignRootComponentId(_authorizeRouteViewComponent);
4141
}
4242

src/Components/Components/src/ComponentFactory.cs

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Diagnostics.CodeAnalysis;
66
using System.Reflection;
77
using Microsoft.AspNetCore.Components.Reflection;
8+
using Microsoft.AspNetCore.Components.RenderTree;
89
using static Microsoft.AspNetCore.Internal.LinkerFlags;
910

1011
namespace Microsoft.AspNetCore.Components;
@@ -14,46 +15,70 @@ internal sealed class ComponentFactory
1415
private const BindingFlags _injectablePropertyBindingFlags
1516
= BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
1617

17-
private static readonly ConcurrentDictionary<Type, Action<IServiceProvider, IComponent>> _cachedInitializers = new();
18+
private static readonly ConcurrentDictionary<Type, ComponentTypeInfoCacheEntry> _cachedComponentTypeInfo = new();
1819

1920
private readonly IComponentActivator _componentActivator;
21+
private readonly Renderer _renderer;
2022

21-
public ComponentFactory(IComponentActivator componentActivator)
23+
public ComponentFactory(IComponentActivator componentActivator, Renderer renderer)
2224
{
2325
_componentActivator = componentActivator ?? throw new ArgumentNullException(nameof(componentActivator));
26+
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
2427
}
2528

26-
public static void ClearCache() => _cachedInitializers.Clear();
29+
public static void ClearCache() => _cachedComponentTypeInfo.Clear();
2730

28-
public IComponent InstantiateComponent(IServiceProvider serviceProvider, [DynamicallyAccessedMembers(Component)] Type componentType)
31+
private static ComponentTypeInfoCacheEntry GetComponentTypeInfo([DynamicallyAccessedMembers(Component)] Type componentType)
2932
{
30-
var component = _componentActivator.CreateInstance(componentType);
33+
// Unfortunately we can't use 'GetOrAdd' here because the DynamicallyAccessedMembers annotation doesn't flow through to the
34+
// callback, so it becomes an IL2111 warning. The following is equivalent and thread-safe because it's a ConcurrentDictionary
35+
// and it doesn't matter if we build a cache entry more than once.
36+
if (!_cachedComponentTypeInfo.TryGetValue(componentType, out var cacheEntry))
37+
{
38+
var componentTypeRenderMode = componentType.GetCustomAttribute<RenderModeAttribute>()?.Mode;
39+
cacheEntry = new ComponentTypeInfoCacheEntry(
40+
componentTypeRenderMode,
41+
CreatePropertyInjector(componentType));
42+
_cachedComponentTypeInfo.TryAdd(componentType, cacheEntry);
43+
}
44+
45+
return cacheEntry;
46+
}
47+
48+
public IComponent InstantiateComponent(IServiceProvider serviceProvider, [DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId)
49+
{
50+
var componentTypeInfo = GetComponentTypeInfo(componentType);
51+
var component = componentTypeInfo.ComponentTypeRenderMode is null
52+
? _componentActivator.CreateInstance(componentType)
53+
: _renderer.ResolveComponentForRenderMode(componentType, parentComponentId, _componentActivator, componentTypeInfo.ComponentTypeRenderMode);
54+
3155
if (component is null)
3256
{
33-
// The default activator will never do this, but an externally-supplied one might
57+
// The default activator/resolver will never do this, but an externally-supplied one might
3458
throw new InvalidOperationException($"The component activator returned a null value for a component of type {componentType.FullName}.");
3559
}
3660

37-
PerformPropertyInjection(serviceProvider, component);
61+
if (component.GetType() == componentType)
62+
{
63+
// Fast, common case: use the cached data we already looked up
64+
componentTypeInfo.PerformPropertyInjection(serviceProvider, component);
65+
}
66+
else
67+
{
68+
// Uncommon case where the activator/resolver returned a different type. Needs an extra cache lookup.
69+
PerformPropertyInjection(serviceProvider, component);
70+
}
71+
3872
return component;
3973
}
4074

4175
private static void PerformPropertyInjection(IServiceProvider serviceProvider, IComponent instance)
4276
{
43-
// This is thread-safe because _cachedInitializers is a ConcurrentDictionary.
44-
// We might generate the initializer more than once for a given type, but would
45-
// still produce the correct result.
46-
var instanceType = instance.GetType();
47-
if (!_cachedInitializers.TryGetValue(instanceType, out var initializer))
48-
{
49-
initializer = CreateInitializer(instanceType);
50-
_cachedInitializers.TryAdd(instanceType, initializer);
51-
}
52-
53-
initializer(serviceProvider, instance);
77+
var componentTypeInfo = GetComponentTypeInfo(instance.GetType());
78+
componentTypeInfo.PerformPropertyInjection(serviceProvider, instance);
5479
}
5580

56-
private static Action<IServiceProvider, IComponent> CreateInitializer([DynamicallyAccessedMembers(Component)] Type type)
81+
private static Action<IServiceProvider, IComponent> CreatePropertyInjector([DynamicallyAccessedMembers(Component)] Type type)
5782
{
5883
// Do all the reflection up front
5984
List<(string name, Type propertyType, PropertySetter setter)>? injectables = null;
@@ -93,4 +118,9 @@ void Initialize(IServiceProvider serviceProvider, IComponent component)
93118
}
94119
}
95120
}
121+
122+
// Tracks information about a specific component type that ComponentFactory uses
123+
private record class ComponentTypeInfoCacheEntry(
124+
IComponentRenderMode? ComponentTypeRenderMode,
125+
Action<IServiceProvider, IComponent> PerformPropertyInjection);
96126
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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;
5+
6+
/// <summary>
7+
/// Represents a render mode for a component.
8+
/// </summary>
9+
public interface IComponentRenderMode
10+
{
11+
}

src/Components/Components/src/ParameterView.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,9 @@ public TValue GetValueOrDefault<TValue>(string parameterName, TValue defaultValu
103103
/// Returns a dictionary populated with the contents of the <see cref="ParameterView"/>.
104104
/// </summary>
105105
/// <returns>A dictionary populated with the contents of the <see cref="ParameterView"/>.</returns>
106-
public IReadOnlyDictionary<string, object> ToDictionary()
106+
public IReadOnlyDictionary<string, object?> ToDictionary()
107107
{
108-
var result = new Dictionary<string, object>();
108+
var result = new Dictionary<string, object?>();
109109
foreach (var entry in this)
110110
{
111111
result[entry.Name] = entry.Value;

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#nullable enable
2+
abstract Microsoft.AspNetCore.Components.RenderModeAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode!
23
Microsoft.AspNetCore.Components.CascadingModelBinder
34
Microsoft.AspNetCore.Components.CascadingModelBinder.CascadingModelBinder() -> void
45
Microsoft.AspNetCore.Components.CascadingModelBinder.ChildContent.get -> Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.ModelBindingContext!>!
@@ -8,10 +9,13 @@ Microsoft.AspNetCore.Components.CascadingModelBinder.IsFixed.set -> void
89
Microsoft.AspNetCore.Components.CascadingModelBinder.Name.get -> string!
910
Microsoft.AspNetCore.Components.CascadingModelBinder.Name.set -> void
1011
Microsoft.AspNetCore.Components.ComponentBase.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task!
12+
Microsoft.AspNetCore.Components.IComponentRenderMode
1113
Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.PersistStateAsync(Microsoft.AspNetCore.Components.IPersistentComponentStateStore! store, Microsoft.AspNetCore.Components.Dispatcher! dispatcher) -> System.Threading.Tasks.Task!
1214
Microsoft.AspNetCore.Components.ModelBindingContext
1315
Microsoft.AspNetCore.Components.ModelBindingContext.BindingContextId.get -> string!
1416
Microsoft.AspNetCore.Components.ModelBindingContext.Name.get -> string!
17+
Microsoft.AspNetCore.Components.ParameterView.ToDictionary() -> System.Collections.Generic.IReadOnlyDictionary<string!, object?>!
18+
*REMOVED*Microsoft.AspNetCore.Components.ParameterView.ToDictionary() -> System.Collections.Generic.IReadOnlyDictionary<string!, object!>!
1519
Microsoft.AspNetCore.Components.RenderHandle.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task!
1620
*REMOVED*Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string! relativeUri) -> System.Uri!
1721
Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string? relativeUri) -> System.Uri!
@@ -22,6 +26,8 @@ Microsoft.AspNetCore.Components.RouteData.RouteData(System.Type! pageType, Syste
2226
Microsoft.AspNetCore.Components.RouteData.RouteValues.get -> System.Collections.Generic.IReadOnlyDictionary<string!, object?>!
2327
Microsoft.AspNetCore.Components.Routing.IRoutingStateProvider
2428
Microsoft.AspNetCore.Components.Routing.IRoutingStateProvider.RouteData.get -> Microsoft.AspNetCore.Components.RouteData?
29+
Microsoft.AspNetCore.Components.RenderModeAttribute
30+
Microsoft.AspNetCore.Components.RenderModeAttribute.RenderModeAttribute() -> void
2531
Microsoft.AspNetCore.Components.Routing.IScrollToLocationHash
2632
Microsoft.AspNetCore.Components.Routing.IScrollToLocationHash.RefreshScrollPositionForHash(string! locationAbsolute) -> System.Threading.Tasks.Task!
2733
Microsoft.AspNetCore.Components.Rendering.ComponentState
@@ -58,5 +64,6 @@ override Microsoft.AspNetCore.Components.EventCallback<TValue>.Equals(object? ob
5864
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.AddPendingTask(Microsoft.AspNetCore.Components.Rendering.ComponentState? componentState, System.Threading.Tasks.Task! task) -> void
5965
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!
6066
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo? fieldInfo, System.EventArgs! eventArgs, bool quiesce) -> System.Threading.Tasks.Task!
67+
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!
6168
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.ShouldTrackNamedEventHandlers() -> bool
6269
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.TrackNamedEventId(ulong eventHandlerId, int componentId, string! eventHandlerName) -> void
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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;
5+
6+
/// <summary>
7+
/// Specifies a fixed rendering mode for a component type.
8+
///
9+
/// Where possible, components should not specify any render mode this way, and should
10+
/// be implemented to work across all render modes. Component authors should only specify
11+
/// a fixed rendering mode when the component is incapable of running in other modes.
12+
/// </summary>
13+
[AttributeUsage(AttributeTargets.Class)]
14+
public abstract class RenderModeAttribute : Attribute
15+
{
16+
/// <summary>
17+
/// Gets the fixed rendering mode for a component type.
18+
/// </summary>
19+
public abstract IComponentRenderMode Mode { get; }
20+
}

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -952,15 +952,8 @@ private static void InitializeNewComponentFrame(ref DiffContext diffContext, int
952952
{
953953
var frames = diffContext.NewTree;
954954
ref var frame = ref frames[frameIndex];
955-
956-
if (frame.ComponentStateField != null)
957-
{
958-
throw new InvalidOperationException($"Child component already exists during {nameof(InitializeNewComponentFrame)}");
959-
}
960-
961955
var parentComponentId = diffContext.ComponentId;
962-
diffContext.Renderer.InstantiateChildComponentOnFrame(ref frame, parentComponentId);
963-
var childComponentState = frame.ComponentStateField;
956+
var childComponentState = diffContext.Renderer.InstantiateChildComponentOnFrame(ref frame, parentComponentId);
964957

965958
// Set initial parameters
966959
var initialParametersLifetime = new ParameterViewLifetime(diffContext.BatchBuilder);

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,3 @@ public enum RenderTreeFrameType : short
5959
/// </summary>
6060
Markup = 8,
6161
}
62-

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

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory,
8686
// has always taken ILoggerFactory so to avoid the per-instance string allocation of the logger name we just pass the
8787
// logger name in here as a string literal.
8888
_logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Components.RenderTree.Renderer");
89-
_componentFactory = new ComponentFactory(componentActivator);
89+
_componentFactory = new ComponentFactory(componentActivator, this);
9090
}
9191

9292
internal HotReloadManager HotReloadManager { get; set; } = HotReloadManager.Default;
@@ -162,9 +162,7 @@ await Dispatcher.InvokeAsync(() =>
162162
/// <param name="componentType">The type of the component to instantiate.</param>
163163
/// <returns>The component instance.</returns>
164164
protected IComponent InstantiateComponent([DynamicallyAccessedMembers(Component)] Type componentType)
165-
{
166-
return _componentFactory.InstantiateComponent(_serviceProvider, componentType);
167-
}
165+
=> _componentFactory.InstantiateComponent(_serviceProvider, componentType, null);
168166

169167
/// <summary>
170168
/// Associates the <see cref="IComponent"/> with the <see cref="Renderer"/>, assigning
@@ -475,7 +473,7 @@ public Type GetEventArgsType(ulong eventHandlerId)
475473
: EventArgsTypeCache.GetEventArgsType(methodInfo);
476474
}
477475

478-
internal void InstantiateChildComponentOnFrame(ref RenderTreeFrame frame, int parentComponentId)
476+
internal ComponentState InstantiateChildComponentOnFrame(ref RenderTreeFrame frame, int parentComponentId)
479477
{
480478
if (frame.FrameTypeField != RenderTreeFrameType.Component)
481479
{
@@ -487,10 +485,12 @@ internal void InstantiateChildComponentOnFrame(ref RenderTreeFrame frame, int pa
487485
throw new ArgumentException($"The frame already has a non-null component instance", nameof(frame));
488486
}
489487

490-
var newComponent = InstantiateComponent(frame.ComponentTypeField);
488+
var newComponent = _componentFactory.InstantiateComponent(_serviceProvider, frame.ComponentTypeField, parentComponentId);
491489
var newComponentState = AttachAndInitComponent(newComponent, parentComponentId);
492490
frame.ComponentStateField = newComponentState;
493491
frame.ComponentIdField = newComponentState.ComponentId;
492+
493+
return newComponentState;
494494
}
495495

496496
internal void AddToPendingTasksWithErrorHandling(Task task, ComponentState? owningComponentState)
@@ -1159,6 +1159,27 @@ void NotifyExceptions(List<Exception> exceptions)
11591159
}
11601160
}
11611161

1162+
/// <summary>
1163+
/// Determines how to handle an <see cref="IComponentRenderMode"/> when obtaining a component instance.
1164+
/// This is only called for components that have specified a render mode. Subclasses may override this
1165+
/// method to return a component of a different type, or throw, depending on whether the renderer
1166+
/// supports the render mode and how it implements that support.
1167+
/// </summary>
1168+
/// <param name="componentType">The type of component that was requested.</param>
1169+
/// <param name="parentComponentId">The parent component ID, or null if it is a root component.</param>
1170+
/// <param name="componentActivator">An <see cref="IComponentActivator"/> that should be used when instantiating component objects.</param>
1171+
/// <param name="componentTypeRenderMode">The <see cref="IComponentRenderMode"/> declared on <paramref name="componentType"/>.</param>
1172+
/// <returns>An <see cref="IComponent"/> instance.</returns>
1173+
protected internal virtual IComponent ResolveComponentForRenderMode(
1174+
[DynamicallyAccessedMembers(Component)] Type componentType,
1175+
int? parentComponentId,
1176+
IComponentActivator componentActivator,
1177+
IComponentRenderMode componentTypeRenderMode)
1178+
{
1179+
// Nothing is supported by default. Subclasses must override this to opt into supporting specific render modes.
1180+
throw new NotSupportedException($"Cannot supply a component of type '{componentType}' because the current platform does not support the render mode '{componentTypeRenderMode}'.");
1181+
}
1182+
11621183
/// <summary>
11631184
/// Releases all resources currently used by this <see cref="Renderer"/> instance.
11641185
/// </summary>

0 commit comments

Comments
 (0)