Skip to content

Commit

Permalink
Allow declaring render modes, and emit the corresponding markers into…
Browse files Browse the repository at this point in the history
… HTML (#48190)
  • Loading branch information
SteveSandersonMS authored May 16, 2023
1 parent 2bb4f08 commit ec9a2d8
Show file tree
Hide file tree
Showing 45 changed files with 1,050 additions and 315 deletions.
4 changes: 2 additions & 2 deletions src/Components/Authorization/test/AuthorizeRouteViewTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ public AuthorizeRouteViewTest()

var services = serviceCollection.BuildServiceProvider();
_renderer = new TestRenderer(services);
var componentFactory = new ComponentFactory(new DefaultComponentActivator());
_authorizeRouteViewComponent = (AuthorizeRouteView)componentFactory.InstantiateComponent(services, typeof(AuthorizeRouteView));
var componentFactory = new ComponentFactory(new DefaultComponentActivator(), _renderer);
_authorizeRouteViewComponent = (AuthorizeRouteView)componentFactory.InstantiateComponent(services, typeof(AuthorizeRouteView), null);
_authorizeRouteViewComponentId = _renderer.AssignRootComponentId(_authorizeRouteViewComponent);
}

Expand Down
68 changes: 49 additions & 19 deletions src/Components/Components/src/ComponentFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Microsoft.AspNetCore.Components.Reflection;
using Microsoft.AspNetCore.Components.RenderTree;
using static Microsoft.AspNetCore.Internal.LinkerFlags;

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

private static readonly ConcurrentDictionary<Type, Action<IServiceProvider, IComponent>> _cachedInitializers = new();
private static readonly ConcurrentDictionary<Type, ComponentTypeInfoCacheEntry> _cachedComponentTypeInfo = new();

private readonly IComponentActivator _componentActivator;
private readonly Renderer _renderer;

public ComponentFactory(IComponentActivator componentActivator)
public ComponentFactory(IComponentActivator componentActivator, Renderer renderer)
{
_componentActivator = componentActivator ?? throw new ArgumentNullException(nameof(componentActivator));
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
}

public static void ClearCache() => _cachedInitializers.Clear();
public static void ClearCache() => _cachedComponentTypeInfo.Clear();

public IComponent InstantiateComponent(IServiceProvider serviceProvider, [DynamicallyAccessedMembers(Component)] Type componentType)
private static ComponentTypeInfoCacheEntry GetComponentTypeInfo([DynamicallyAccessedMembers(Component)] Type componentType)
{
var component = _componentActivator.CreateInstance(componentType);
// Unfortunately we can't use 'GetOrAdd' here because the DynamicallyAccessedMembers annotation doesn't flow through to the
// callback, so it becomes an IL2111 warning. The following is equivalent and thread-safe because it's a ConcurrentDictionary
// and it doesn't matter if we build a cache entry more than once.
if (!_cachedComponentTypeInfo.TryGetValue(componentType, out var cacheEntry))
{
var componentTypeRenderMode = componentType.GetCustomAttribute<RenderModeAttribute>()?.Mode;
cacheEntry = new ComponentTypeInfoCacheEntry(
componentTypeRenderMode,
CreatePropertyInjector(componentType));
_cachedComponentTypeInfo.TryAdd(componentType, cacheEntry);
}

return cacheEntry;
}

public IComponent InstantiateComponent(IServiceProvider serviceProvider, [DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId)
{
var componentTypeInfo = GetComponentTypeInfo(componentType);
var component = componentTypeInfo.ComponentTypeRenderMode is null
? _componentActivator.CreateInstance(componentType)
: _renderer.ResolveComponentForRenderMode(componentType, parentComponentId, _componentActivator, componentTypeInfo.ComponentTypeRenderMode);

if (component is null)
{
// The default activator will never do this, but an externally-supplied one might
// The default activator/resolver will never do this, but an externally-supplied one might
throw new InvalidOperationException($"The component activator returned a null value for a component of type {componentType.FullName}.");
}

PerformPropertyInjection(serviceProvider, component);
if (component.GetType() == componentType)
{
// Fast, common case: use the cached data we already looked up
componentTypeInfo.PerformPropertyInjection(serviceProvider, component);
}
else
{
// Uncommon case where the activator/resolver returned a different type. Needs an extra cache lookup.
PerformPropertyInjection(serviceProvider, component);
}

return component;
}

private static void PerformPropertyInjection(IServiceProvider serviceProvider, IComponent instance)
{
// This is thread-safe because _cachedInitializers is a ConcurrentDictionary.
// We might generate the initializer more than once for a given type, but would
// still produce the correct result.
var instanceType = instance.GetType();
if (!_cachedInitializers.TryGetValue(instanceType, out var initializer))
{
initializer = CreateInitializer(instanceType);
_cachedInitializers.TryAdd(instanceType, initializer);
}

initializer(serviceProvider, instance);
var componentTypeInfo = GetComponentTypeInfo(instance.GetType());
componentTypeInfo.PerformPropertyInjection(serviceProvider, instance);
}

private static Action<IServiceProvider, IComponent> CreateInitializer([DynamicallyAccessedMembers(Component)] Type type)
private static Action<IServiceProvider, IComponent> CreatePropertyInjector([DynamicallyAccessedMembers(Component)] Type type)
{
// Do all the reflection up front
List<(string name, Type propertyType, PropertySetter setter)>? injectables = null;
Expand Down Expand Up @@ -93,4 +118,9 @@ void Initialize(IServiceProvider serviceProvider, IComponent component)
}
}
}

// Tracks information about a specific component type that ComponentFactory uses
private record class ComponentTypeInfoCacheEntry(
IComponentRenderMode? ComponentTypeRenderMode,
Action<IServiceProvider, IComponent> PerformPropertyInjection);
}
11 changes: 11 additions & 0 deletions src/Components/Components/src/IComponentRenderMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Components;

/// <summary>
/// Represents a render mode for a component.
/// </summary>
public interface IComponentRenderMode
{
}
4 changes: 2 additions & 2 deletions src/Components/Components/src/ParameterView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ public TValue GetValueOrDefault<TValue>(string parameterName, TValue defaultValu
/// Returns a dictionary populated with the contents of the <see cref="ParameterView"/>.
/// </summary>
/// <returns>A dictionary populated with the contents of the <see cref="ParameterView"/>.</returns>
public IReadOnlyDictionary<string, object> ToDictionary()
public IReadOnlyDictionary<string, object?> ToDictionary()
{
var result = new Dictionary<string, object>();
var result = new Dictionary<string, object?>();
foreach (var entry in this)
{
result[entry.Name] = entry.Value;
Expand Down
7 changes: 7 additions & 0 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#nullable enable
abstract Microsoft.AspNetCore.Components.RenderModeAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode!
Microsoft.AspNetCore.Components.CascadingModelBinder
Microsoft.AspNetCore.Components.CascadingModelBinder.CascadingModelBinder() -> void
Microsoft.AspNetCore.Components.CascadingModelBinder.ChildContent.get -> Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.ModelBindingContext!>!
Expand All @@ -8,10 +9,13 @@ Microsoft.AspNetCore.Components.CascadingModelBinder.IsFixed.set -> void
Microsoft.AspNetCore.Components.CascadingModelBinder.Name.get -> string!
Microsoft.AspNetCore.Components.CascadingModelBinder.Name.set -> void
Microsoft.AspNetCore.Components.ComponentBase.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Components.IComponentRenderMode
Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.PersistStateAsync(Microsoft.AspNetCore.Components.IPersistentComponentStateStore! store, Microsoft.AspNetCore.Components.Dispatcher! dispatcher) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Components.ModelBindingContext
Microsoft.AspNetCore.Components.ModelBindingContext.BindingContextId.get -> string!
Microsoft.AspNetCore.Components.ModelBindingContext.Name.get -> string!
Microsoft.AspNetCore.Components.ParameterView.ToDictionary() -> System.Collections.Generic.IReadOnlyDictionary<string!, object?>!
*REMOVED*Microsoft.AspNetCore.Components.ParameterView.ToDictionary() -> System.Collections.Generic.IReadOnlyDictionary<string!, object!>!
Microsoft.AspNetCore.Components.RenderHandle.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task!
*REMOVED*Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string! relativeUri) -> System.Uri!
Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string? relativeUri) -> System.Uri!
Expand All @@ -22,6 +26,8 @@ Microsoft.AspNetCore.Components.RouteData.RouteData(System.Type! pageType, Syste
Microsoft.AspNetCore.Components.RouteData.RouteValues.get -> System.Collections.Generic.IReadOnlyDictionary<string!, object?>!
Microsoft.AspNetCore.Components.Routing.IRoutingStateProvider
Microsoft.AspNetCore.Components.Routing.IRoutingStateProvider.RouteData.get -> Microsoft.AspNetCore.Components.RouteData?
Microsoft.AspNetCore.Components.RenderModeAttribute
Microsoft.AspNetCore.Components.RenderModeAttribute.RenderModeAttribute() -> void
Microsoft.AspNetCore.Components.Routing.IScrollToLocationHash
Microsoft.AspNetCore.Components.Routing.IScrollToLocationHash.RefreshScrollPositionForHash(string! locationAbsolute) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Components.Rendering.ComponentState
Expand Down Expand Up @@ -58,5 +64,6 @@ override Microsoft.AspNetCore.Components.EventCallback<TValue>.Equals(object? ob
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.AddPendingTask(Microsoft.AspNetCore.Components.Rendering.ComponentState? componentState, System.Threading.Tasks.Task! task) -> void
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!
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo? fieldInfo, System.EventArgs! eventArgs, bool quiesce) -> System.Threading.Tasks.Task!
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!
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.ShouldTrackNamedEventHandlers() -> bool
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.TrackNamedEventId(ulong eventHandlerId, int componentId, string! eventHandlerName) -> void
20 changes: 20 additions & 0 deletions src/Components/Components/src/RenderModeAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Components;

/// <summary>
/// Specifies a fixed rendering mode for a component type.
///
/// Where possible, components should not specify any render mode this way, and should
/// be implemented to work across all render modes. Component authors should only specify
/// a fixed rendering mode when the component is incapable of running in other modes.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public abstract class RenderModeAttribute : Attribute
{
/// <summary>
/// Gets the fixed rendering mode for a component type.
/// </summary>
public abstract IComponentRenderMode Mode { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -952,15 +952,8 @@ private static void InitializeNewComponentFrame(ref DiffContext diffContext, int
{
var frames = diffContext.NewTree;
ref var frame = ref frames[frameIndex];

if (frame.ComponentStateField != null)
{
throw new InvalidOperationException($"Child component already exists during {nameof(InitializeNewComponentFrame)}");
}

var parentComponentId = diffContext.ComponentId;
diffContext.Renderer.InstantiateChildComponentOnFrame(ref frame, parentComponentId);
var childComponentState = frame.ComponentStateField;
var childComponentState = diffContext.Renderer.InstantiateChildComponentOnFrame(ref frame, parentComponentId);

// Set initial parameters
var initialParametersLifetime = new ParameterViewLifetime(diffContext.BatchBuilder);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,3 @@ public enum RenderTreeFrameType : short
/// </summary>
Markup = 8,
}

33 changes: 27 additions & 6 deletions src/Components/Components/src/RenderTree/Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory,
// has always taken ILoggerFactory so to avoid the per-instance string allocation of the logger name we just pass the
// logger name in here as a string literal.
_logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Components.RenderTree.Renderer");
_componentFactory = new ComponentFactory(componentActivator);
_componentFactory = new ComponentFactory(componentActivator, this);
}

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

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

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

var newComponent = InstantiateComponent(frame.ComponentTypeField);
var newComponent = _componentFactory.InstantiateComponent(_serviceProvider, frame.ComponentTypeField, parentComponentId);
var newComponentState = AttachAndInitComponent(newComponent, parentComponentId);
frame.ComponentStateField = newComponentState;
frame.ComponentIdField = newComponentState.ComponentId;

return newComponentState;
}

internal void AddToPendingTasksWithErrorHandling(Task task, ComponentState? owningComponentState)
Expand Down Expand Up @@ -1159,6 +1159,27 @@ void NotifyExceptions(List<Exception> exceptions)
}
}

/// <summary>
/// Determines how to handle an <see cref="IComponentRenderMode"/> when obtaining a component instance.
/// This is only called for components that have specified a render mode. Subclasses may override this
/// method to return a component of a different type, or throw, depending on whether the renderer
/// supports the render mode and how it implements that support.
/// </summary>
/// <param name="componentType">The type of component that was requested.</param>
/// <param name="parentComponentId">The parent component ID, or null if it is a root component.</param>
/// <param name="componentActivator">An <see cref="IComponentActivator"/> that should be used when instantiating component objects.</param>
/// <param name="componentTypeRenderMode">The <see cref="IComponentRenderMode"/> declared on <paramref name="componentType"/>.</param>
/// <returns>An <see cref="IComponent"/> instance.</returns>
protected internal virtual IComponent ResolveComponentForRenderMode(
[DynamicallyAccessedMembers(Component)] Type componentType,
int? parentComponentId,
IComponentActivator componentActivator,
IComponentRenderMode componentTypeRenderMode)
{
// Nothing is supported by default. Subclasses must override this to opt into supporting specific render modes.
throw new NotSupportedException($"Cannot supply a component of type '{componentType}' because the current platform does not support the render mode '{componentTypeRenderMode}'.");
}

/// <summary>
/// Releases all resources currently used by this <see cref="Renderer"/> instance.
/// </summary>
Expand Down
Loading

0 comments on commit ec9a2d8

Please sign in to comment.