[Fixes #3752] Refactor and cleanup view components
* Moved instantiation of view components into DefaultViewComponentActivator * Introduced IViewComponentFactory to handling setup of new view component instances. * Added a release method on IViewComponentActivator for handling tear down of view component instances.
This commit is contained in:
parent
2b0bea675e
commit
354400f12b
|
|
@ -118,8 +118,10 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
//
|
//
|
||||||
// View Components
|
// View Components
|
||||||
//
|
//
|
||||||
|
|
||||||
// These do caching so they should stay singleton
|
// These do caching so they should stay singleton
|
||||||
services.TryAddSingleton<IViewComponentSelector, DefaultViewComponentSelector>();
|
services.TryAddSingleton<IViewComponentSelector, DefaultViewComponentSelector>();
|
||||||
|
services.TryAddSingleton<IViewComponentFactory, DefaultViewComponentFactory>();
|
||||||
services.TryAddSingleton<IViewComponentActivator, DefaultViewComponentActivator>();
|
services.TryAddSingleton<IViewComponentActivator, DefaultViewComponentActivator>();
|
||||||
services.TryAddSingleton<
|
services.TryAddSingleton<
|
||||||
IViewComponentDescriptorCollectionProvider,
|
IViewComponentDescriptorCollectionProvider,
|
||||||
|
|
|
||||||
|
|
@ -858,6 +858,22 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AmbiguousMethods"), p0, p1, p2);
|
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AmbiguousMethods"), p0, p1, p2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The type '{0}' cannot be activated by '{1}' because it is either a value type, an interface, an abstract class or an open generic type.
|
||||||
|
/// </summary>
|
||||||
|
internal static string ValueInterfaceAbstractOrOpenGenericTypesCannotBeActivated
|
||||||
|
{
|
||||||
|
get { return GetString("ValueInterfaceAbstractOrOpenGenericTypesCannotBeActivated"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The type '{0}' cannot be activated by '{1}' because it is either a value type, an interface, an abstract class or an open generic type.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatValueInterfaceAbstractOrOpenGenericTypesCannotBeActivated(object p0, object p1)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("ValueInterfaceAbstractOrOpenGenericTypesCannotBeActivated"), p0, p1);
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetString(string name, params string[] formatterNames)
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
{
|
{
|
||||||
var value = _resourceManager.GetString(name);
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
|
||||||
|
|
@ -277,4 +277,7 @@
|
||||||
<data name="ViewComponent_AmbiguousMethods" xml:space="preserve">
|
<data name="ViewComponent_AmbiguousMethods" xml:space="preserve">
|
||||||
<value>View component '{0}' must have exactly one public method named '{1}' or '{2}'.</value>
|
<value>View component '{0}' must have exactly one public method named '{1}' or '{2}'.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ValueInterfaceAbstractOrOpenGenericTypesCannotBeActivated" xml:space="preserve">
|
||||||
|
<value>The type '{0}' cannot be activated by '{1}' because it is either a value type, an interface, an abstract class or an open generic type.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Microsoft.Extensions.Internal;
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||||
{
|
{
|
||||||
|
|
@ -18,49 +18,71 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public class DefaultViewComponentActivator : IViewComponentActivator
|
public class DefaultViewComponentActivator : IViewComponentActivator
|
||||||
{
|
{
|
||||||
private readonly Func<Type, PropertyActivator<ViewComponentContext>[]> _getPropertiesToActivate;
|
private readonly ITypeActivatorCache _typeActivatorCache;
|
||||||
private readonly ConcurrentDictionary<Type, PropertyActivator<ViewComponentContext>[]> _injectActions;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of <see cref="DefaultViewComponentActivator"/> class.
|
/// Initializes a new instance of <see cref="DefaultViewComponentActivator"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DefaultViewComponentActivator()
|
/// <param name="typeActivatorCache">
|
||||||
|
/// The <see cref="ITypeActivatorCache"/> used to create new view component instances.
|
||||||
|
/// </param>
|
||||||
|
public DefaultViewComponentActivator(ITypeActivatorCache typeActivatorCache)
|
||||||
{
|
{
|
||||||
_injectActions = new ConcurrentDictionary<Type, PropertyActivator<ViewComponentContext>[]>();
|
if (typeActivatorCache == null)
|
||||||
_getPropertiesToActivate = type =>
|
{
|
||||||
PropertyActivator<ViewComponentContext>.GetPropertiesToActivate(
|
throw new ArgumentNullException(nameof(typeActivatorCache));
|
||||||
type,
|
}
|
||||||
typeof(ViewComponentContextAttribute),
|
|
||||||
CreateActivateInfo);
|
_typeActivatorCache = typeActivatorCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual void Activate(object viewComponent, ViewComponentContext context)
|
public virtual object Create(ViewComponentContext context)
|
||||||
{
|
{
|
||||||
if (viewComponent == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(viewComponent));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context == null)
|
if (context == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(context));
|
throw new ArgumentNullException(nameof(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
var propertiesToActivate = _injectActions.GetOrAdd(
|
var componentType = context.ViewComponentDescriptor.Type.GetTypeInfo();
|
||||||
viewComponent.GetType(),
|
|
||||||
_getPropertiesToActivate);
|
|
||||||
|
|
||||||
for (var i = 0; i < propertiesToActivate.Length; i++)
|
if (componentType.IsValueType ||
|
||||||
|
componentType.IsInterface ||
|
||||||
|
componentType.IsAbstract ||
|
||||||
|
(componentType.IsGenericType && componentType.IsGenericTypeDefinition))
|
||||||
{
|
{
|
||||||
var activateInfo = propertiesToActivate[i];
|
var message = Resources.FormatValueInterfaceAbstractOrOpenGenericTypesCannotBeActivated(
|
||||||
activateInfo.Activate(viewComponent, context);
|
componentType.FullName,
|
||||||
|
GetType().FullName);
|
||||||
|
|
||||||
|
throw new InvalidOperationException(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var viewComponent = _typeActivatorCache.CreateInstance<object>(
|
||||||
|
context.ViewContext.HttpContext.RequestServices,
|
||||||
|
context.ViewComponentDescriptor.Type);
|
||||||
|
|
||||||
|
return viewComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
private PropertyActivator<ViewComponentContext> CreateActivateInfo(PropertyInfo property)
|
/// <inheritdoc />
|
||||||
|
public virtual void Release(ViewComponentContext context, object viewComponent)
|
||||||
{
|
{
|
||||||
return new PropertyActivator<ViewComponentContext>(property, context => context);
|
if (context == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(nameof(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewComponent == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(nameof(viewComponent));
|
||||||
|
}
|
||||||
|
|
||||||
|
var disposable = viewComponent as IDisposable;
|
||||||
|
if (disposable != null)
|
||||||
|
{
|
||||||
|
disposable.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.Extensions.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default implementation for <see cref="IViewComponentFactory"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class DefaultViewComponentFactory : IViewComponentFactory
|
||||||
|
{
|
||||||
|
private readonly IViewComponentActivator _activator;
|
||||||
|
private readonly Func<Type, PropertyActivator<ViewComponentContext>[]> _getPropertiesToActivate;
|
||||||
|
private readonly ConcurrentDictionary<Type, PropertyActivator<ViewComponentContext>[]> _injectActions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of <see cref="DefaultViewComponentFactory"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="activator">
|
||||||
|
/// The <see cref="IViewComponentActivator"/> used to create new view component instances.
|
||||||
|
/// </param>
|
||||||
|
public DefaultViewComponentFactory(IViewComponentActivator activator)
|
||||||
|
{
|
||||||
|
if (activator == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(activator));
|
||||||
|
}
|
||||||
|
|
||||||
|
_activator = activator;
|
||||||
|
|
||||||
|
_getPropertiesToActivate = type => PropertyActivator<ViewComponentContext>.GetPropertiesToActivate(
|
||||||
|
type,
|
||||||
|
typeof(ViewComponentContextAttribute),
|
||||||
|
CreateActivateInfo);
|
||||||
|
|
||||||
|
_injectActions = new ConcurrentDictionary<Type, PropertyActivator<ViewComponentContext>[]>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public object CreateViewComponent(ViewComponentContext context)
|
||||||
|
{
|
||||||
|
if (context == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
var component = _activator.Create(context);
|
||||||
|
|
||||||
|
InjectProperties(context, component);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InjectProperties(ViewComponentContext context, object viewComponent)
|
||||||
|
{
|
||||||
|
var propertiesToActivate = _injectActions.GetOrAdd(
|
||||||
|
viewComponent.GetType(),
|
||||||
|
type =>
|
||||||
|
PropertyActivator<ViewComponentContext>.GetPropertiesToActivate(
|
||||||
|
type,
|
||||||
|
typeof(ViewComponentContextAttribute),
|
||||||
|
CreateActivateInfo));
|
||||||
|
|
||||||
|
for (var i = 0; i < propertiesToActivate.Length; i++)
|
||||||
|
{
|
||||||
|
var activateInfo = propertiesToActivate[i];
|
||||||
|
activateInfo.Activate(viewComponent, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PropertyActivator<ViewComponentContext> CreateActivateInfo(PropertyInfo property)
|
||||||
|
{
|
||||||
|
return new PropertyActivator<ViewComponentContext>(property, context => context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void ReleaseViewComponent(ViewComponentContext context, object component)
|
||||||
|
{
|
||||||
|
if (context == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(component));
|
||||||
|
}
|
||||||
|
|
||||||
|
_activator.Release(context, component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,8 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DefaultViewComponentInvoker : IViewComponentInvoker
|
public class DefaultViewComponentInvoker : IViewComponentInvoker
|
||||||
{
|
{
|
||||||
private readonly ITypeActivatorCache _typeActivatorCache;
|
private readonly IViewComponentFactory _viewComponentFactory;
|
||||||
private readonly IViewComponentActivator _viewComponentActivator;
|
|
||||||
private readonly DiagnosticSource _diagnosticSource;
|
private readonly DiagnosticSource _diagnosticSource;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
|
@ -28,23 +27,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||||
/// Initializes a new instance of <see cref="DefaultViewComponentInvoker"/>.
|
/// Initializes a new instance of <see cref="DefaultViewComponentInvoker"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="typeActivatorCache">Caches factories for instantiating view component instances.</param>
|
/// <param name="typeActivatorCache">Caches factories for instantiating view component instances.</param>
|
||||||
/// <param name="viewComponentActivator">The <see cref="IViewComponentActivator"/>.</param>
|
/// <param name="viewComponentFactory">The <see cref="IViewComponentFactory"/>.</param>
|
||||||
/// <param name="diagnosticSource">The <see cref="DiagnosticSource"/>.</param>
|
/// <param name="diagnosticSource">The <see cref="DiagnosticSource"/>.</param>
|
||||||
/// <param name="logger">The <see cref="ILogger"/>.</param>
|
/// <param name="logger">The <see cref="ILogger"/>.</param>
|
||||||
public DefaultViewComponentInvoker(
|
public DefaultViewComponentInvoker(
|
||||||
ITypeActivatorCache typeActivatorCache,
|
IViewComponentFactory viewComponentFactory,
|
||||||
IViewComponentActivator viewComponentActivator,
|
|
||||||
DiagnosticSource diagnosticSource,
|
DiagnosticSource diagnosticSource,
|
||||||
ILogger logger)
|
ILogger logger)
|
||||||
{
|
{
|
||||||
if (typeActivatorCache == null)
|
if (viewComponentFactory == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(typeActivatorCache));
|
throw new ArgumentNullException(nameof(viewComponentFactory));
|
||||||
}
|
|
||||||
|
|
||||||
if (viewComponentActivator == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(viewComponentActivator));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (diagnosticSource == null)
|
if (diagnosticSource == null)
|
||||||
|
|
@ -57,8 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||||
throw new ArgumentNullException(nameof(logger));
|
throw new ArgumentNullException(nameof(logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
_typeActivatorCache = typeActivatorCache;
|
_viewComponentFactory = viewComponentFactory;
|
||||||
_viewComponentActivator = viewComponentActivator;
|
|
||||||
_diagnosticSource = diagnosticSource;
|
_diagnosticSource = diagnosticSource;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
@ -95,24 +87,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||||
await result.ExecuteAsync(context);
|
await result.ExecuteAsync(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private object CreateComponent(ViewComponentContext context)
|
|
||||||
{
|
|
||||||
if (context == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
var services = context.ViewContext.HttpContext.RequestServices;
|
|
||||||
var component = _typeActivatorCache.CreateInstance<object>(
|
|
||||||
services,
|
|
||||||
context.ViewComponentDescriptor.Type);
|
|
||||||
_viewComponentActivator.Activate(component, context);
|
|
||||||
return component;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<IViewComponentResult> InvokeAsyncCore(ViewComponentContext context)
|
private async Task<IViewComponentResult> InvokeAsyncCore(ViewComponentContext context)
|
||||||
{
|
{
|
||||||
var component = CreateComponent(context);
|
var component = _viewComponentFactory.CreateViewComponent(context);
|
||||||
|
|
||||||
using (_logger.ViewComponentScope(context))
|
using (_logger.ViewComponentScope(context))
|
||||||
{
|
{
|
||||||
|
|
@ -129,13 +106,15 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||||
_logger.ViewComponentExecuted(context, startTimestamp, viewComponentResult);
|
_logger.ViewComponentExecuted(context, startTimestamp, viewComponentResult);
|
||||||
_diagnosticSource.AfterViewComponent(context, viewComponentResult, component);
|
_diagnosticSource.AfterViewComponent(context, viewComponentResult, component);
|
||||||
|
|
||||||
|
_viewComponentFactory.ReleaseViewComponent(context, component);
|
||||||
|
|
||||||
return viewComponentResult;
|
return viewComponentResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IViewComponentResult InvokeSyncCore(ViewComponentContext context)
|
private IViewComponentResult InvokeSyncCore(ViewComponentContext context)
|
||||||
{
|
{
|
||||||
var component = CreateComponent(context);
|
var component = _viewComponentFactory.CreateViewComponent(context);
|
||||||
|
|
||||||
using (_logger.ViewComponentScope(context))
|
using (_logger.ViewComponentScope(context))
|
||||||
{
|
{
|
||||||
|
|
@ -155,6 +134,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||||
}
|
}
|
||||||
catch (TargetInvocationException ex)
|
catch (TargetInvocationException ex)
|
||||||
{
|
{
|
||||||
|
_viewComponentFactory.ReleaseViewComponent(context, component);
|
||||||
|
|
||||||
// Preserve callstack of any user-thrown exceptions.
|
// Preserve callstack of any user-thrown exceptions.
|
||||||
var exceptionInfo = ExceptionDispatchInfo.Capture(ex.InnerException);
|
var exceptionInfo = ExceptionDispatchInfo.Capture(ex.InnerException);
|
||||||
exceptionInfo.Throw();
|
exceptionInfo.Throw();
|
||||||
|
|
@ -165,6 +146,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||||
_logger.ViewComponentExecuted(context, startTimestamp, viewComponentResult);
|
_logger.ViewComponentExecuted(context, startTimestamp, viewComponentResult);
|
||||||
_diagnosticSource.AfterViewComponent(context, viewComponentResult, component);
|
_diagnosticSource.AfterViewComponent(context, viewComponentResult, component);
|
||||||
|
|
||||||
|
_viewComponentFactory.ReleaseViewComponent(context, component);
|
||||||
|
|
||||||
return viewComponentResult;
|
return viewComponentResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,19 +10,31 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||||
{
|
{
|
||||||
public class DefaultViewComponentInvokerFactory : IViewComponentInvokerFactory
|
public class DefaultViewComponentInvokerFactory : IViewComponentInvokerFactory
|
||||||
{
|
{
|
||||||
private readonly ITypeActivatorCache _typeActivatorCache;
|
private readonly IViewComponentFactory _viewComponentFactory;
|
||||||
private readonly IViewComponentActivator _viewComponentActivator;
|
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly DiagnosticSource _diagnosticSource;
|
private readonly DiagnosticSource _diagnosticSource;
|
||||||
|
|
||||||
public DefaultViewComponentInvokerFactory(
|
public DefaultViewComponentInvokerFactory(
|
||||||
ITypeActivatorCache typeActivatorCache,
|
IViewComponentFactory viewComponentFactory,
|
||||||
IViewComponentActivator viewComponentActivator,
|
|
||||||
DiagnosticSource diagnosticSource,
|
DiagnosticSource diagnosticSource,
|
||||||
ILoggerFactory loggerFactory)
|
ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
_typeActivatorCache = typeActivatorCache;
|
if (viewComponentFactory == null)
|
||||||
_viewComponentActivator = viewComponentActivator;
|
{
|
||||||
|
throw new ArgumentNullException(nameof(viewComponentFactory));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diagnosticSource == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(diagnosticSource));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loggerFactory == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(loggerFactory));
|
||||||
|
}
|
||||||
|
|
||||||
|
_viewComponentFactory = viewComponentFactory;
|
||||||
_diagnosticSource = diagnosticSource;
|
_diagnosticSource = diagnosticSource;
|
||||||
|
|
||||||
_logger = loggerFactory.CreateLogger<DefaultViewComponentInvoker>();
|
_logger = loggerFactory.CreateLogger<DefaultViewComponentInvoker>();
|
||||||
|
|
@ -40,8 +52,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||||
}
|
}
|
||||||
|
|
||||||
return new DefaultViewComponentInvoker(
|
return new DefaultViewComponentInvoker(
|
||||||
_typeActivatorCache,
|
_viewComponentFactory,
|
||||||
_viewComponentActivator,
|
|
||||||
_diagnosticSource,
|
_diagnosticSource,
|
||||||
_logger);
|
_logger);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,25 @@
|
||||||
namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides methods to activate an instantiated ViewComponent
|
/// Provides methods to instantiate and release a ViewComponent.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IViewComponentActivator
|
public interface IViewComponentActivator
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When implemented in a type, activates an instantiated ViewComponent.
|
/// Instantiates a ViewComponent.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="viewComponent">The ViewComponent to activate.</param>
|
|
||||||
/// <param name="context">
|
/// <param name="context">
|
||||||
/// The <see cref="ViewComponentContext"/> for the executing <see cref="ViewComponent"/>.
|
/// The <see cref="ViewComponentContext"/> for the executing <see cref="ViewComponent"/>.
|
||||||
/// </param>
|
/// </param>
|
||||||
void Activate(object viewComponent, ViewComponentContext context);
|
object Create(ViewComponentContext context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases a ViewComponent instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">
|
||||||
|
/// The <see cref="ViewComponentContext"/> associated with the <paramref name="component"/>.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="viewComponent">The <see cref="ViewComponent"/> to release.</param>
|
||||||
|
void Release(ViewComponentContext context, object viewComponent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides methods for creation and disposal of view components.
|
||||||
|
/// </summary>
|
||||||
|
public interface IViewComponentFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new controller for the specified <paramref name="context"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"><see cref="ViewComponentContext"/> for the view component.</param>
|
||||||
|
/// <returns>The view component.</returns>
|
||||||
|
object CreateViewComponent(ViewComponentContext context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases a view component instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The context associated with the <paramref name="component"/>.</param>
|
||||||
|
/// <param name="component">The view component.</param>
|
||||||
|
void ReleaseViewComponent(ViewComponentContext context, object component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -525,6 +525,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
services.AddSingleton<IViewComponentInvokerFactory, DefaultViewComponentInvokerFactory>();
|
services.AddSingleton<IViewComponentInvokerFactory, DefaultViewComponentInvokerFactory>();
|
||||||
services.AddSingleton<ITypeActivatorCache, TypeActivatorCache>();
|
services.AddSingleton<ITypeActivatorCache, TypeActivatorCache>();
|
||||||
services.AddSingleton<IViewComponentActivator, DefaultViewComponentActivator>();
|
services.AddSingleton<IViewComponentActivator, DefaultViewComponentActivator>();
|
||||||
|
services.AddSingleton<IViewComponentFactory, DefaultViewComponentFactory>();
|
||||||
services.AddSingleton<IViewComponentDescriptorProvider>(new FixedSetViewComponentDescriptorProvider(descriptors));
|
services.AddSingleton<IViewComponentDescriptorProvider>(new FixedSetViewComponentDescriptorProvider(descriptors));
|
||||||
services.AddSingleton<IModelMetadataProvider, EmptyModelMetadataProvider>();
|
services.AddSingleton<IModelMetadataProvider, EmptyModelMetadataProvider>();
|
||||||
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
|
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
|
||||||
|
|
@ -573,6 +574,16 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string ReadBody(HttpResponse response)
|
||||||
|
{
|
||||||
|
response.Body.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
using (var reader = new StreamReader(response.Body))
|
||||||
|
{
|
||||||
|
return reader.ReadToEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class TextViewComponent : ViewComponent
|
private class TextViewComponent : ViewComponent
|
||||||
{
|
{
|
||||||
public HtmlString Invoke(string name)
|
public HtmlString Invoke(string name)
|
||||||
|
|
@ -588,15 +599,5 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
return Task.FromResult(new HtmlString("Hello-Async, " + name));
|
return Task.FromResult(new HtmlString("Hello-Async, " + name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ReadBody(HttpResponse response)
|
|
||||||
{
|
|
||||||
response.Body.Seek(0, SeekOrigin.Begin);
|
|
||||||
|
|
||||||
using (var reader = new StreamReader(response.Body))
|
|
||||||
{
|
|
||||||
return reader.ReadToEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http.Internal;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||||
|
|
@ -13,35 +17,116 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||||
public void DefaultViewComponentActivator_ActivatesViewComponentContext()
|
public void DefaultViewComponentActivator_ActivatesViewComponentContext()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var activator = new DefaultViewComponentActivator();
|
var expectedInstance = new TestViewComponent();
|
||||||
|
|
||||||
var context = new ViewComponentContext();
|
var typeActivator = new Mock<ITypeActivatorCache>();
|
||||||
var instance = new TestViewComponent();
|
typeActivator
|
||||||
|
.Setup(ta => ta.CreateInstance<object>(It.IsAny<IServiceProvider>(), It.IsAny<Type>()))
|
||||||
|
.Returns(expectedInstance);
|
||||||
|
|
||||||
|
var activator = new DefaultViewComponentActivator(typeActivator.Object);
|
||||||
|
|
||||||
|
var context = CreateContext(typeof(TestViewComponent));
|
||||||
|
expectedInstance.ViewComponentContext = context;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
activator.Activate(instance, context);
|
var instance = activator.Create(context) as ViewComponent;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
Assert.NotNull(instance);
|
||||||
Assert.Same(context, instance.ViewComponentContext);
|
Assert.Same(context, instance.ViewComponentContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(int))]
|
||||||
|
[InlineData(typeof(OpenGenericType<>))]
|
||||||
|
[InlineData(typeof(AbstractType))]
|
||||||
|
[InlineData(typeof(InterfaceType))]
|
||||||
|
public void Create_ThrowsIfControllerCannotBeActivated(Type type)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actionDescriptor = new ViewComponentDescriptor
|
||||||
|
{
|
||||||
|
Type = type
|
||||||
|
};
|
||||||
|
|
||||||
|
var context = new ViewComponentContext
|
||||||
|
{
|
||||||
|
ViewComponentDescriptor = actionDescriptor,
|
||||||
|
ViewContext = new ViewContext
|
||||||
|
{
|
||||||
|
HttpContext = new DefaultHttpContext()
|
||||||
|
{
|
||||||
|
RequestServices = Mock.Of<IServiceProvider>()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var activator = new DefaultViewComponentActivator(new TypeActivatorCache());
|
||||||
|
|
||||||
|
// Act and Assert
|
||||||
|
var exception = Assert.Throws<InvalidOperationException>(() => activator.Create(context));
|
||||||
|
Assert.Equal(
|
||||||
|
$"The type '{type.FullName}' cannot be activated by '{typeof(DefaultViewComponentActivator).FullName}' " +
|
||||||
|
"because it is either a value type, an interface, an abstract class or an open generic type.",
|
||||||
|
exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DefaultViewComponentActivator_ActivatesViewComponentContext_IgnoresNonPublic()
|
public void DefaultViewComponentActivator_ActivatesViewComponentContext_IgnoresNonPublic()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var activator = new DefaultViewComponentActivator();
|
var expectedInstance = new VisibilityViewComponent();
|
||||||
|
|
||||||
var context = new ViewComponentContext();
|
var typeActivator = new Mock<ITypeActivatorCache>();
|
||||||
var instance = new VisibilityViewComponent();
|
typeActivator
|
||||||
|
.Setup(ta => ta.CreateInstance<object>(It.IsAny<IServiceProvider>(), It.IsAny<Type>()))
|
||||||
|
.Returns(expectedInstance);
|
||||||
|
|
||||||
|
var activator = new DefaultViewComponentActivator(typeActivator.Object);
|
||||||
|
|
||||||
|
var context = CreateContext(typeof(VisibilityViewComponent));
|
||||||
|
expectedInstance.ViewComponentContext = context;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
activator.Activate(instance, context);
|
var instance = activator.Create(context) as VisibilityViewComponent;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
Assert.NotNull(instance);
|
||||||
Assert.Same(context, instance.ViewComponentContext);
|
Assert.Same(context, instance.ViewComponentContext);
|
||||||
Assert.Null(instance.C);
|
Assert.Null(instance.C);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ViewComponentContext CreateContext(Type componentType)
|
||||||
|
{
|
||||||
|
return new ViewComponentContext
|
||||||
|
{
|
||||||
|
ViewComponentDescriptor = new ViewComponentDescriptor
|
||||||
|
{
|
||||||
|
Type = componentType
|
||||||
|
},
|
||||||
|
ViewContext = new ViewContext
|
||||||
|
{
|
||||||
|
HttpContext = new DefaultHttpContext
|
||||||
|
{
|
||||||
|
RequestServices = Mock.Of<IServiceProvider>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OpenGenericType<T> : Controller
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private abstract class AbstractType : Controller
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface InterfaceType
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
private class TestViewComponent : ViewComponent
|
private class TestViewComponent : ViewComponent
|
||||||
{
|
{
|
||||||
public Task ExecuteAsync()
|
public Task ExecuteAsync()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||||
|
{
|
||||||
|
public class DefaultViewComponentFactoryTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CreateViewComponent_ActivatesProperties_OnTheInstance()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = new ViewComponentContext
|
||||||
|
{
|
||||||
|
};
|
||||||
|
|
||||||
|
var component = new ActivablePropertiesViewComponent();
|
||||||
|
var activator = new Mock<IViewComponentActivator>();
|
||||||
|
activator.Setup(a => a.Create(context))
|
||||||
|
.Returns(component);
|
||||||
|
|
||||||
|
var factory = new DefaultViewComponentFactory(activator.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = factory.CreateViewComponent(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var activablePropertiesComponent = Assert.IsType<ActivablePropertiesViewComponent>(result);
|
||||||
|
|
||||||
|
Assert.Same(component, activablePropertiesComponent);
|
||||||
|
Assert.Same(component.Context, activablePropertiesComponent.Context);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReleaseViewComponent_CallsDispose_OnTheInstance()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = new ViewComponentContext
|
||||||
|
{
|
||||||
|
};
|
||||||
|
|
||||||
|
var component = new ActivablePropertiesViewComponent();
|
||||||
|
|
||||||
|
var viewComponentActivator = new Mock<IViewComponentActivator>();
|
||||||
|
viewComponentActivator.Setup(vca => vca.Release(context, component))
|
||||||
|
.Callback<ViewComponentContext, object>((c, o) => (o as IDisposable)?.Dispose());
|
||||||
|
|
||||||
|
var factory = new DefaultViewComponentFactory(viewComponentActivator.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
factory.ReleaseViewComponent(context, component);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(true, component.Disposed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ActivablePropertiesViewComponent : IDisposable
|
||||||
|
{
|
||||||
|
[ViewComponentContext]
|
||||||
|
public ViewComponentContext Context { get; set; }
|
||||||
|
|
||||||
|
public bool Disposed { get; private set; }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Invoke()
|
||||||
|
{
|
||||||
|
return "something";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue