Skip to content

Commit a3c0de4

Browse files
Allow declaring render modes, and emit the corresponding markers into HTML
1 parent c218dff commit a3c0de4

File tree

54 files changed

+1276
-329
lines changed

Some content is hidden

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

54 files changed

+1276
-329
lines changed

AspNetCore.sln

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10754,7 +10754,7 @@ Global
1075410754
{2C022280-340C-43DC-B63D-5A644F0ECBF2} = {5BC5A805-DCA0-41DF-91B8-520B5DAD57DA}
1075510755
{B6081DA9-738E-4088-92DD-7FFA200523C9} = {2C022280-340C-43DC-B63D-5A644F0ECBF2}
1075610756
{BE70E100-E6C4-4686-8592-73E2A04E877F} = {B6081DA9-738E-4088-92DD-7FFA200523C9}
10757-
{60D51C98-2CC0-40DF-B338-44154EFEE2FF} = {017429CC-C5FB-48B4-9C46-034E29EE2F06}
10757+
{60D51C98-2CC0-40DF-B338-44154EFEE2FF} = {380430A1-C02F-4943-8A52-FDFA9F7735F0}
1075810758
{6DE916F5-E78F-446C-89CB-9240AED2A2E2} = {60D51C98-2CC0-40DF-B338-44154EFEE2FF}
1075910759
{7546E2DD-2CF4-4240-8045-2533DF539458} = {6DE916F5-E78F-446C-89CB-9240AED2A2E2}
1076010760
{B55A5DE1-5AF3-4B18-AF04-C1735B071DA6} = {017429CC-C5FB-48B4-9C46-034E29EE2F06}

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(), new RenderModeResolver());
39+
_authorizeRouteViewComponent = (AuthorizeRouteView)componentFactory.InstantiateComponent(services, typeof(AuthorizeRouteView), null);
4040
_authorizeRouteViewComponentId = _renderer.AssignRootComponentId(_authorizeRouteViewComponent);
4141
}
4242

src/Components/Components/src/ComponentFactory.cs

Lines changed: 48 additions & 18 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.Rendering;
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 RenderModeResolver _renderModeResolver;
2022

21-
public ComponentFactory(IComponentActivator componentActivator)
23+
public ComponentFactory(IComponentActivator componentActivator, RenderModeResolver renderModeResolver)
2224
{
2325
_componentActivator = componentActivator ?? throw new ArgumentNullException(nameof(componentActivator));
26+
_renderModeResolver = renderModeResolver ?? throw new ArgumentNullException(nameof(renderModeResolver));
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+
cacheEntry = new ComponentTypeInfoCacheEntry(GetRenderModeFromComponentType(componentType), CreatePropertyInjector(componentType));
39+
_cachedComponentTypeInfo.TryAdd(componentType, cacheEntry);
40+
}
41+
42+
return cacheEntry;
43+
}
44+
45+
public IComponent InstantiateComponent(IServiceProvider serviceProvider, [DynamicallyAccessedMembers(Component)] Type componentType, IComponentRenderMode? callerSpecifiedRenderMode)
46+
{
47+
var componentTypeInfo = GetComponentTypeInfo(componentType);
48+
var component = callerSpecifiedRenderMode is null && componentTypeInfo.RenderMode is null
49+
? _componentActivator.CreateInstance(componentType)
50+
: _renderModeResolver.ResolveComponent(componentType, _componentActivator, componentTypeInfo.RenderMode, callerSpecifiedRenderMode);
51+
3152
if (component is null)
3253
{
3354
// The default activator will never do this, but an externally-supplied one might
3455
throw new InvalidOperationException($"The component activator returned a null value for a component of type {componentType.FullName}.");
3556
}
3657

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

4172
private static void PerformPropertyInjection(IServiceProvider serviceProvider, IComponent instance)
4273
{
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);
74+
var componentTypeInfo = GetComponentTypeInfo(instance.GetType());
75+
componentTypeInfo.PerformPropertyInjection(serviceProvider, instance);
5476
}
5577

56-
private static Action<IServiceProvider, IComponent> CreateInitializer([DynamicallyAccessedMembers(Component)] Type type)
78+
private static IComponentRenderMode? GetRenderModeFromComponentType(Type componentType)
79+
=> componentType.GetCustomAttribute<RenderModeAttribute>()?.Mode;
80+
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? RenderMode,
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: 18 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,14 +9,28 @@ 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+
Microsoft.AspNetCore.Components.Rendering.RenderModeResolver
23+
Microsoft.AspNetCore.Components.Rendering.RenderModeResolver.RenderModeResolver() -> void
24+
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentParameter(int sequence, string! name, Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> void
25+
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentRenderMode(int sequence, Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> void
1826
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.SetEventHandlerName(string! eventHandlerName) -> void
27+
Microsoft.AspNetCore.Components.RenderModeAttribute
28+
Microsoft.AspNetCore.Components.RenderModeAttribute.RenderModeAttribute() -> void
29+
Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags
30+
Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags.HasCallerSpecifiedRenderMode = 1 -> Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags
31+
Microsoft.AspNetCore.Components.RenderTree.Renderer.InstantiateComponent(System.Type! componentType, Microsoft.AspNetCore.Components.IComponentRenderMode? renderMode) -> Microsoft.AspNetCore.Components.IComponent!
32+
Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame.ComponentFrameFlags.get -> Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags
33+
Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType.ComponentRenderMode = 9 -> Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType
1934
Microsoft.AspNetCore.Components.Routing.IScrollToLocationHash
2035
Microsoft.AspNetCore.Components.Routing.IScrollToLocationHash.RefreshScrollPositionForHash(string! locationAbsolute) -> System.Threading.Tasks.Task!
2136
Microsoft.AspNetCore.Components.Rendering.ComponentState
@@ -49,8 +64,11 @@ override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int
4964
override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bool
5065
override Microsoft.AspNetCore.Components.EventCallback<TValue>.GetHashCode() -> int
5166
override Microsoft.AspNetCore.Components.EventCallback<TValue>.Equals(object? obj) -> bool
67+
virtual Microsoft.AspNetCore.Components.Rendering.RenderModeResolver.ResolveComponent(System.Type! componentType, Microsoft.AspNetCore.Components.IComponentActivator! componentActivator, Microsoft.AspNetCore.Components.IComponentRenderMode? componentTypeRenderMode, Microsoft.AspNetCore.Components.IComponentRenderMode? callSiteRenderMode) -> Microsoft.AspNetCore.Components.IComponent!
5268
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.AddPendingTask(Microsoft.AspNetCore.Components.Rendering.ComponentState? componentState, System.Threading.Tasks.Task! task) -> void
5369
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!
5470
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo? fieldInfo, System.EventArgs! eventArgs, bool quiesce) -> System.Threading.Tasks.Task!
71+
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderModeResolver.get -> Microsoft.AspNetCore.Components.Rendering.RenderModeResolver!
5572
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.ShouldTrackNamedEventHandlers() -> bool
5673
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.TrackNamedEventId(ulong eventHandlerId, int componentId, string! eventHandlerName) -> void
74+
~Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame.ComponentRenderMode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode
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+
}
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 & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -951,16 +951,8 @@ private static void InitializeNewSubtree(ref DiffContext diffContext, int frameI
951951
private static void InitializeNewComponentFrame(ref DiffContext diffContext, int frameIndex)
952952
{
953953
var frames = diffContext.NewTree;
954-
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-
961954
var parentComponentId = diffContext.ComponentId;
962-
diffContext.Renderer.InstantiateChildComponentOnFrame(ref frame, parentComponentId);
963-
var childComponentState = frame.ComponentStateField;
955+
var childComponentState = diffContext.Renderer.InstantiateChildComponentOnFrame(frames, frameIndex, parentComponentId);
964956

965957
// Set initial parameters
966958
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 content of the markup frame. 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(RenderTreeFrame.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()

0 commit comments

Comments
 (0)