Updates to IComponentActivator PR
This commit is contained in:
parent
dae55edfec
commit
53588b45dc
|
|
@ -107,6 +107,11 @@ namespace Microsoft.AspNetCore.Components
|
|||
protected virtual bool ShouldRender() { throw null; }
|
||||
protected void StateHasChanged() { }
|
||||
}
|
||||
public partial class DefaultComponentActivator : Microsoft.AspNetCore.Components.IComponentActivator
|
||||
{
|
||||
public DefaultComponentActivator() { }
|
||||
public Microsoft.AspNetCore.Components.IComponent CreateInstance(System.Type componentType) { throw null; }
|
||||
}
|
||||
public abstract partial class Dispatcher
|
||||
{
|
||||
protected Dispatcher() { }
|
||||
|
|
@ -234,6 +239,10 @@ namespace Microsoft.AspNetCore.Components
|
|||
void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle);
|
||||
System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters);
|
||||
}
|
||||
public partial interface IComponentActivator
|
||||
{
|
||||
Microsoft.AspNetCore.Components.IComponent CreateInstance(System.Type componentType);
|
||||
}
|
||||
public partial interface IHandleAfterRender
|
||||
{
|
||||
System.Threading.Tasks.Task OnAfterRenderAsync();
|
||||
|
|
|
|||
|
|
@ -6,14 +6,9 @@ using System.Collections.Concurrent;
|
|||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Components.Reflection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
/// <remarks>
|
||||
/// The <see cref="Instance"/> property on this type is used as a static global cache. Ensure any changes to this type
|
||||
/// are thread safe and can be safely cached statically.
|
||||
/// </remarks>
|
||||
internal class ComponentFactory
|
||||
{
|
||||
private static readonly BindingFlags _injectablePropertyBindingFlags
|
||||
|
|
@ -22,19 +17,20 @@ namespace Microsoft.AspNetCore.Components
|
|||
private readonly ConcurrentDictionary<Type, Action<IServiceProvider, IComponent>> _cachedInitializers
|
||||
= new ConcurrentDictionary<Type, Action<IServiceProvider, IComponent>>();
|
||||
|
||||
public static readonly ComponentFactory Instance = new ComponentFactory();
|
||||
private readonly IComponentActivator _componentActivator;
|
||||
|
||||
public ComponentFactory(IComponentActivator componentActivator)
|
||||
{
|
||||
_componentActivator = componentActivator ?? throw new ArgumentNullException(nameof(componentActivator));
|
||||
}
|
||||
|
||||
public IComponent InstantiateComponent(IServiceProvider serviceProvider, Type componentType)
|
||||
{
|
||||
var activator = serviceProvider.GetService<IComponentActivator>();
|
||||
|
||||
var instance = activator != null
|
||||
? activator.CreateInstance(componentType)
|
||||
: Activator.CreateInstance(componentType);
|
||||
|
||||
if (!(instance is IComponent component))
|
||||
var component = _componentActivator.CreateInstance(componentType);
|
||||
if (component is null)
|
||||
{
|
||||
throw new ArgumentException($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", nameof(componentType));
|
||||
// The default activator 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);
|
||||
|
|
|
|||
|
|
@ -6,14 +6,25 @@ using System;
|
|||
namespace Microsoft.AspNetCore.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Default implementation of component activator.
|
||||
/// Default implementation of <see cref="IComponentActivator"/>.
|
||||
/// </summary>
|
||||
public class DefaultComponentActivator : IComponentActivator
|
||||
{
|
||||
// If no IComponentActivator is supplied by DI, the renderer uses this instance.
|
||||
// It's internal because in the future, we might want to add per-scope state and then
|
||||
// it would no longer be applicable to have a shared instance.
|
||||
internal static IComponentActivator Instance { get; } = new DefaultComponentActivator();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IComponent? CreateInstance(Type componentType)
|
||||
public IComponent CreateInstance(Type componentType)
|
||||
{
|
||||
return Activator.CreateInstance(componentType) as IComponent;
|
||||
var instance = Activator.CreateInstance(componentType);
|
||||
if (!(instance is IComponent component))
|
||||
{
|
||||
throw new ArgumentException($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", nameof(componentType));
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,16 @@ namespace Microsoft.AspNetCore.Components
|
|||
{
|
||||
/// <summary>
|
||||
/// Represents an activator that can be used to instantiate components.
|
||||
/// The activator is not responsible for dependency injection, since the framework
|
||||
/// performs dependency injection to the resulting instances separately.
|
||||
/// </summary>
|
||||
public interface IComponentActivator
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an component of the specified type using that type's default constructor.
|
||||
/// Creates a component of the specified type.
|
||||
/// </summary>
|
||||
/// <param name="componentType">The type of component to create.</param>
|
||||
/// <returns>A reference to the newly created component.</returns>
|
||||
IComponent? CreateInstance(Type componentType);
|
||||
IComponent CreateInstance(Type componentType);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Profiling;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.RenderTree
|
||||
|
|
@ -29,6 +30,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
private readonly Dictionary<ulong, EventCallback> _eventBindings = new Dictionary<ulong, EventCallback>();
|
||||
private readonly Dictionary<ulong, ulong> _eventHandlerIdReplacements = new Dictionary<ulong, ulong>();
|
||||
private readonly ILogger<Renderer> _logger;
|
||||
private readonly ComponentFactory _componentFactory;
|
||||
|
||||
private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it
|
||||
private bool _isBatchInProgress;
|
||||
|
|
@ -70,6 +72,10 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = loggerFactory.CreateLogger<Renderer>();
|
||||
|
||||
var componentActivator = serviceProvider.GetService<IComponentActivator>()
|
||||
?? DefaultComponentActivator.Instance;
|
||||
_componentFactory = new ComponentFactory(componentActivator);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -89,7 +95,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
|||
/// <param name="componentType">The type of the component to instantiate.</param>
|
||||
/// <returns>The component instance.</returns>
|
||||
protected IComponent InstantiateComponent(Type componentType)
|
||||
=> ComponentFactory.Instance.InstantiateComponent(_serviceProvider, componentType);
|
||||
=> _componentFactory.InstantiateComponent(_serviceProvider, componentType);
|
||||
|
||||
/// <summary>
|
||||
/// Associates the <see cref="IComponent"/> with the <see cref="Renderer"/>, assigning
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
|
|
@ -17,7 +16,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
{
|
||||
// Arrange
|
||||
var componentType = typeof(EmptyComponent);
|
||||
var factory = new ComponentFactory();
|
||||
var factory = new ComponentFactory(new DefaultComponentActivator());
|
||||
|
||||
// Act
|
||||
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
|
||||
|
|
@ -28,44 +27,31 @@ namespace Microsoft.AspNetCore.Components
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void InstantiateComponent_CreatesInstance_WithActivator()
|
||||
public void InstantiateComponent_CreatesInstance_NonComponent()
|
||||
{
|
||||
// Arrange
|
||||
var componentType = typeof(List<string>);
|
||||
var factory = new ComponentFactory(new DefaultComponentActivator());
|
||||
|
||||
// Assert
|
||||
var ex = Assert.Throws<ArgumentException>(() => factory.InstantiateComponent(GetServiceProvider(), componentType));
|
||||
Assert.StartsWith($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstantiateComponent_CreatesInstance_WithCustomActivator()
|
||||
{
|
||||
// Arrange
|
||||
var componentType = typeof(EmptyComponent);
|
||||
var factory = new ComponentFactory();
|
||||
|
||||
// Act
|
||||
var instance = factory.InstantiateComponent(GetServiceProviderWithActivator(), componentType);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(instance);
|
||||
Assert.IsType<EmptyComponent>(instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstantiateComponent_CreatesInstance_WithActivator_NonComponent()
|
||||
{
|
||||
// Arrange
|
||||
var componentType = typeof(NonComponent);
|
||||
var factory = new ComponentFactory();
|
||||
|
||||
// Assert
|
||||
Assert.Throws<ArgumentException>(()=>factory.InstantiateComponent(GetServiceProviderWithActivator(), componentType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstantiateComponent_AssignsPropertiesWithInjectAttribute()
|
||||
{
|
||||
// Arrange
|
||||
var componentType = typeof(ComponentWithInjectProperties);
|
||||
var factory = new ComponentFactory();
|
||||
var factory = new ComponentFactory(new CustomComponentActivator<ComponentWithInjectProperties>());
|
||||
|
||||
// Act
|
||||
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(instance);
|
||||
var component = Assert.IsType<ComponentWithInjectProperties>(instance);
|
||||
var component = Assert.IsType<ComponentWithInjectProperties>(instance); // Custom activator returns a different type
|
||||
|
||||
// Public, and non-public properties, and properties with non-public setters should get assigned
|
||||
Assert.NotNull(component.Property1);
|
||||
Assert.NotNull(component.GetProperty2());
|
||||
|
|
@ -73,12 +59,24 @@ namespace Microsoft.AspNetCore.Components
|
|||
Assert.NotNull(component.Property4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstantiateComponent_ThrowsForNullInstance()
|
||||
{
|
||||
// Arrange
|
||||
var componentType = typeof(EmptyComponent);
|
||||
var factory = new ComponentFactory(new NullResultComponentActivator());
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => factory.InstantiateComponent(GetServiceProvider(), componentType));
|
||||
Assert.Equal($"The component activator returned a null value for a component of type {componentType.FullName}.", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstantiateComponent_AssignsPropertiesWithInjectAttributeOnBaseType()
|
||||
{
|
||||
// Arrange
|
||||
var componentType = typeof(DerivedComponent);
|
||||
var factory = new ComponentFactory();
|
||||
var factory = new ComponentFactory(new CustomComponentActivator<DerivedComponent>());
|
||||
|
||||
// Act
|
||||
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
|
||||
|
|
@ -101,7 +99,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
{
|
||||
// Arrange
|
||||
var componentType = typeof(ComponentWithNonInjectableProperties);
|
||||
var factory = new ComponentFactory();
|
||||
var factory = new ComponentFactory(new DefaultComponentActivator());
|
||||
|
||||
// Act
|
||||
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
|
||||
|
|
@ -122,15 +120,6 @@ namespace Microsoft.AspNetCore.Components
|
|||
.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private static IServiceProvider GetServiceProviderWithActivator()
|
||||
{
|
||||
return new ServiceCollection()
|
||||
.AddTransient<TestService1>()
|
||||
.AddTransient<TestService2>()
|
||||
.AddSingleton<IComponentActivator, DefaultComponentActivator>()
|
||||
.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private class EmptyComponent : IComponent
|
||||
{
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
|
|
@ -197,9 +186,23 @@ namespace Microsoft.AspNetCore.Components
|
|||
public TestService2 Property5 { get; set; }
|
||||
}
|
||||
|
||||
private class NonComponent { }
|
||||
|
||||
public class TestService1 { }
|
||||
public class TestService2 { }
|
||||
|
||||
private class CustomComponentActivator<TResult> : IComponentActivator where TResult : IComponent, new()
|
||||
{
|
||||
public IComponent CreateInstance(Type componentType)
|
||||
{
|
||||
return new TResult();
|
||||
}
|
||||
}
|
||||
|
||||
private class NullResultComponentActivator : IComponentActivator
|
||||
{
|
||||
public IComponent CreateInstance(Type componentType)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
}
|
||||
|
||||
private T InstantiateComponent<T>() where T: IComponent
|
||||
=> _renderer.InstantiateComponent<T>();
|
||||
=> (T)_renderer.InstantiateComponent<T>();
|
||||
|
||||
class HasPropertiesWithoutInjectAttribute : TestComponent
|
||||
{
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
|
||||
var counter = serviceProvider.GetRequiredService<Counter>();
|
||||
var renderer = new TestRenderer(serviceProvider);
|
||||
var component1 = renderer.InstantiateComponent<MyOwningComponent>();
|
||||
var component1 = (MyOwningComponent)renderer.InstantiateComponent<MyOwningComponent>();
|
||||
|
||||
Assert.NotNull(component1.MyService);
|
||||
Assert.Equal(1, counter.CreatedCount);
|
||||
|
|
|
|||
|
|
@ -3733,6 +3733,35 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
Assert.Equal($"The {nameof(ParameterView)} instance can no longer be read because it has expired. {nameof(ParameterView)} can only be read synchronously and must not be stored for later use.", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUseCustomComponentActivator()
|
||||
{
|
||||
// Arrange
|
||||
var serviceProvider = new TestServiceProvider();
|
||||
var componentActivator = new TestComponentActivator<MessageComponent>();
|
||||
serviceProvider.AddService<IComponentActivator>(componentActivator);
|
||||
var renderer = new TestRenderer(serviceProvider);
|
||||
|
||||
// Act: Ask for TestComponent
|
||||
var suppliedComponent = renderer.InstantiateComponent<TestComponent>();
|
||||
|
||||
// Assert: We actually receive MessageComponent
|
||||
Assert.IsType<MessageComponent>(suppliedComponent);
|
||||
Assert.Collection(componentActivator.RequestedComponentTypes,
|
||||
requestedType => Assert.Equal(typeof(TestComponent), requestedType));
|
||||
}
|
||||
|
||||
private class TestComponentActivator<TResult> : IComponentActivator where TResult: IComponent, new()
|
||||
{
|
||||
public List<Type> RequestedComponentTypes { get; } = new List<Type>();
|
||||
|
||||
public IComponent CreateInstance(Type componentType)
|
||||
{
|
||||
RequestedComponentTypes.Add(componentType);
|
||||
return new TResult();
|
||||
}
|
||||
}
|
||||
|
||||
private class NoOpRenderer : Renderer
|
||||
{
|
||||
public NoOpRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance)
|
||||
|
|
|
|||
|
|
@ -74,7 +74,6 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
services.AddScoped<IJSRuntime, RemoteJSRuntime>();
|
||||
services.AddScoped<INavigationInterception, RemoteNavigationInterception>();
|
||||
services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
|
||||
services.AddScoped<IComponentActivator, DefaultComponentActivator>();
|
||||
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CircuitOptions>, CircuitOptionsJSInteropDetailedErrorsConfiguration>());
|
||||
|
||||
|
|
|
|||
|
|
@ -88,8 +88,8 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
|
|||
return task;
|
||||
}
|
||||
|
||||
public T InstantiateComponent<T>() where T : IComponent
|
||||
=> (T)InstantiateComponent(typeof(T));
|
||||
public IComponent InstantiateComponent<T>()
|
||||
=> InstantiateComponent(typeof(T));
|
||||
|
||||
protected override void HandleException(Exception exception)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -212,13 +212,12 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
services.TryAddScoped<NavigationManager, HttpNavigationManager>();
|
||||
services.TryAddScoped<IJSRuntime, UnsupportedJavaScriptRuntime>();
|
||||
services.TryAddScoped<INavigationInterception, UnsupportedNavigationInterception>();
|
||||
|
||||
|
||||
services.TryAddTransient<ControllerSaveTempDataPropertyFilter>();
|
||||
|
||||
// This does caching so it should stay singleton
|
||||
services.TryAddSingleton<ITempDataProvider, CookieTempDataProvider>();
|
||||
services.TryAddSingleton<TempDataSerializer, DefaultTempDataSerializer>();
|
||||
services.TryAddSingleton<IComponentActivator, DefaultComponentActivator>();
|
||||
|
||||
//
|
||||
// Antiforgery
|
||||
|
|
|
|||
Loading…
Reference in New Issue