Skip to content

Allow declaring render modes, and emit the corresponding markers into HTML #48190

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Member Author

@SteveSandersonMS SteveSandersonMS May 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conceptually, the changes in this file are:

  • Extend the existing per-component-type cache to also track the render mode declared on the component type, so there's no new runtime cost for determining this (not even an extra cache lookup)
  • When a nonnull rendermode is found, use the new ResolveComponentForRenderMode to do the component instantiation. This is how a rendermode gets translated into concrete, platform-specific behavior.

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?>();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nullability annotation was wrong before.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to allow multiple for this?

Copy link
Member Author

@SteveSandersonMS SteveSandersonMS May 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. What would we do if there were multiple?

This is about specifying a required rendermode, not a set of allowed rendermodes (which, if implemented in the future, would be a separate API).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean AllowMultiple = false

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the default. If I'm missing something and it has to be specified explicitly in this case, please let me know!

{
/// <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)}");
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check was redundant since InstantiateChildComponentOnFrame already does that.


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