Modify IViewComponentHelper to remove method selection ambiguity.

Fixes #612
This commit is contained in:
Pranav K 2015-12-15 14:24:19 -08:00
parent ce0e35ff75
commit 399e516065
38 changed files with 712 additions and 896 deletions

View File

@ -38,18 +38,5 @@ namespace TagHelperSample.Web.Components
return View(movies);
}
public IViewComponentResult Invoke(string movieName)
{
string quote;
if (!_cache.TryGetValue(movieName, out quote))
{
IChangeToken expirationToken;
quote = _moviesService.GetCriticsQuote(out expirationToken);
_cache.Set(movieName, quote, new MemoryCacheEntryOptions().AddExpirationToken(expirationToken));
}
return Content(quote);
}
}
}

View File

@ -0,0 +1,36 @@
// 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 Microsoft.AspNet.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Primitives;
using TagHelperSample.Web.Services;
namespace TagHelperSample.Web.Components
{
[ViewComponent(Name = "Movies")]
public class MoviesComponent : ViewComponent
{
private readonly IMemoryCache _cache;
private readonly MoviesService _moviesService;
public MoviesComponent(MoviesService moviesService, IMemoryCache cache)
{
_moviesService = moviesService;
_cache = cache;
}
public IViewComponentResult Invoke(string movieName)
{
string quote;
if (!_cache.TryGetValue(movieName, out quote))
{
IChangeToken expirationToken;
quote = _moviesService.GetCriticsQuote(out expirationToken);
_cache.Set(movieName, quote, new MemoryCacheEntryOptions().AddExpirationToken(expirationToken));
}
return Content(quote);
}
}
}

View File

@ -35,7 +35,7 @@
</div>
<div class="sidebar">
<cache expires-after="TimeSpan.FromMinutes(20)">
@Component.Invoke("FeaturedMovies")
@await Component.InvokeAsync("FeaturedMovies")
</cache>
</div>
<div style="clear: left"></div>

View File

@ -10,7 +10,7 @@
</div>
<em>Critics say:</em>
<cache vary-by="@movie.Name">
@Component.Invoke("FeaturedMovies", movie.Name)
@await Component.InvokeAsync("Movies", new { movieName = movie.Name })
</cache>
</dd>
}

View File

@ -7,22 +7,27 @@ using Microsoft.AspNet.Html;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Supports the rendering of view components in a view.
/// </summary>
public interface IViewComponentHelper
{
IHtmlContent Invoke(string name, params object[] args);
/// <summary>
/// Invokes a view component with the specified <paramref name="name"/>.
/// </summary>
/// <param name="name">The name of the view component.</param>
/// <param name="arguments">Arguments to be passed to the invoked view component method.</param>
/// <returns>A <see cref="Task"/> that on completion returns the rendered <see cref="IHtmlContent" />.
/// </returns>
Task<IHtmlContent> InvokeAsync(string name, object arguments);
IHtmlContent Invoke(Type componentType, params object[] args);
void RenderInvoke(string name, params object[] args);
void RenderInvoke(Type componentType, params object[] args);
Task<IHtmlContent> InvokeAsync(string name, params object[] args);
Task<IHtmlContent> InvokeAsync(Type componentType, params object[] args);
Task RenderInvokeAsync(string name, params object[] args);
Task RenderInvokeAsync(Type componentType, params object[] args);
/// <summary>
/// Invokes a view component of type <paramref name="componentType" />.
/// </summary>
/// <param name="componentType">The view component <see cref="Type"/>.</param>
/// <param name="arguments">Arguments to be passed to the invoked view component method.</param>
/// <returns>A <see cref="Task"/> that on completion returns the rendered <see cref="IHtmlContent" />.
/// </returns>
Task<IHtmlContent> InvokeAsync(Type componentType, object arguments);
}
}

View File

@ -10,15 +10,21 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures.Logging
{
public static class DefaultViewComponentInvokerLoggerExtensions
{
private static readonly Action<ILogger, string, Exception> _viewComponentExecuting;
private static readonly string[] EmptyArguments =
#if NET451
new string[0];
#else
Array.Empty<string>();
#endif
private static readonly Action<ILogger, string, string[], Exception> _viewComponentExecuting;
private static readonly Action<ILogger, string, double, string, Exception> _viewComponentExecuted;
static DefaultViewComponentInvokerLoggerExtensions()
{
_viewComponentExecuting = LoggerMessage.Define<string>(
_viewComponentExecuting = LoggerMessage.Define<string, string[]>(
LogLevel.Debug,
1,
"Executing view component {ViewComponentName}");
"Executing view component {ViewComponentName} with arguments ({Arguments}).");
_viewComponentExecuted = LoggerMessage.Define<string, double, string>(
LogLevel.Debug,
@ -32,9 +38,29 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures.Logging
return logger.BeginScopeImpl(new ViewComponentLogScope(context.ViewComponentDescriptor));
}
public static void ViewComponentExecuting(this ILogger logger, ViewComponentContext context)
public static void ViewComponentExecuting(
this ILogger logger,
ViewComponentContext context,
object[] arguments)
{
_viewComponentExecuting(logger, context.ViewComponentDescriptor.DisplayName, null);
var formattedArguments = GetFormattedArguments(arguments);
_viewComponentExecuting(logger, context.ViewComponentDescriptor.DisplayName, formattedArguments, null);
}
private static string[] GetFormattedArguments(object[] arguments)
{
if (arguments == null || arguments.Length == 0)
{
return EmptyArguments;
}
var formattedArguments = new string[arguments.Length];
for (var i = 0; i < formattedArguments.Length; i++)
{
formattedArguments[i] = Convert.ToString(arguments[i]);
}
return formattedArguments;
}
public static void ViewComponentExecuted(

View File

@ -8,41 +8,29 @@ namespace Microsoft.AspNet.Mvc.Logging
{
public static class ViewComponentResultLoggerExtensions
{
private static readonly Action<ILogger, string, string[], Exception> _viewComponentResultExecuting;
private static readonly Action<ILogger, string, Exception> _viewComponentResultExecuting;
static ViewComponentResultLoggerExtensions()
{
_viewComponentResultExecuting = LoggerMessage.Define<string, string[]>(
_viewComponentResultExecuting = LoggerMessage.Define<string>(
LogLevel.Information,
1,
"Executing ViewComponentResult, running {ViewComponentName} with arguments ({Arguments}).");
"Executing ViewComponentResult, running {ViewComponentName}.");
}
public static void ViewComponentResultExecuting(this ILogger logger, string viewComponentName, object[] arguments)
public static void ViewComponentResultExecuting(this ILogger logger, string viewComponentName)
{
if (logger.IsEnabled(LogLevel.Information))
{
var formattedArguments = new string[arguments.Length];
for (var i = 0; i < arguments.Length; i++)
{
formattedArguments[i] = Convert.ToString(arguments[i]);
}
_viewComponentResultExecuting(logger, viewComponentName, formattedArguments, null);
_viewComponentResultExecuting(logger, viewComponentName, null);
}
}
public static void ViewComponentResultExecuting(this ILogger logger, Type viewComponentType, object[] arguments)
public static void ViewComponentResultExecuting(this ILogger logger, Type viewComponentType)
{
if (logger.IsEnabled(LogLevel.Information))
{
var formattedArguments = new string[arguments.Length];
for (var i = 0; i < arguments.Length; i++)
{
formattedArguments[i] = Convert.ToString(arguments[i]);
}
_viewComponentResultExecuting(logger, viewComponentType.Name, formattedArguments, null);
_viewComponentResultExecuting(logger, viewComponentType.Name, null);
}
}
}

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<DisabledCustomTools>.resx</DisabledCustomTools>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">

View File

@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
}
/// <summary>
/// The async view component method '{0}' should be declared to return Task&lt;T&gt;.
/// Method '{0}' of view component '{1}' should be declared to return {2}&lt;T&gt;.
/// </summary>
internal static string ViewComponent_AsyncMethod_ShouldReturnTask
{
@ -35,11 +35,11 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
}
/// <summary>
/// The async view component method '{0}' should be declared to return Task&lt;T&gt;.
/// Method '{0}' of view component '{1}' should be declared to return {2}&lt;T&gt;.
/// </summary>
internal static string FormatViewComponent_AsyncMethod_ShouldReturnTask(object p0)
internal static string FormatViewComponent_AsyncMethod_ShouldReturnTask(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AsyncMethod_ShouldReturnTask"), p0);
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AsyncMethod_ShouldReturnTask"), p0, p1, p2);
}
/// <summary>
@ -59,7 +59,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
}
/// <summary>
/// The view component method '{0}' should be declared to return a value.
/// Method '{0}' of view component '{1}' should be declared to return a value.
/// </summary>
internal static string ViewComponent_SyncMethod_ShouldReturnValue
{
@ -67,11 +67,11 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
}
/// <summary>
/// The view component method '{0}' should be declared to return a value.
/// Method '{0}' of view component '{1}' should be declared to return a value.
/// </summary>
internal static string FormatViewComponent_SyncMethod_ShouldReturnValue(object p0)
internal static string FormatViewComponent_SyncMethod_ShouldReturnValue(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_SyncMethod_ShouldReturnValue"), p0);
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_SyncMethod_ShouldReturnValue"), p0, p1);
}
/// <summary>
@ -107,7 +107,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
}
/// <summary>
/// Could not find an '{0}' method matching the parameters.
/// Could not find an '{0}' or '{1}' method for the view component '{2}'.
/// </summary>
internal static string ViewComponent_CannotFindMethod
{
@ -115,27 +115,11 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
}
/// <summary>
/// Could not find an '{0}' method matching the parameters.
/// Could not find an '{0}' or '{1}' method for the view component '{2}'.
/// </summary>
internal static string FormatViewComponent_CannotFindMethod(object p0)
internal static string FormatViewComponent_CannotFindMethod(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_CannotFindMethod"), p0);
}
/// <summary>
/// Could not find an '{0}' or '{1}' method matching the parameters.
/// </summary>
internal static string ViewComponent_CannotFindMethod_WithFallback
{
get { return GetString("ViewComponent_CannotFindMethod_WithFallback"); }
}
/// <summary>
/// Could not find an '{0}' or '{1}' method matching the parameters.
/// </summary>
internal static string FormatViewComponent_CannotFindMethod_WithFallback(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_CannotFindMethod_WithFallback"), p0, p1);
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_CannotFindMethod"), p0, p1, p2);
}
/// <summary>
@ -859,7 +843,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
}
/// <summary>
/// The view component method '{0}' cannot return a {1}.
/// Method '{0}' of view component '{1}' cannot return a {2}.
/// </summary>
internal static string ViewComponent_SyncMethod_CannotReturnTask
{
@ -867,11 +851,27 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
}
/// <summary>
/// The view component method '{0}' cannot return a {1}.
/// Method '{0}' of view component '{1}' cannot return a {2}.
/// </summary>
internal static string FormatViewComponent_SyncMethod_CannotReturnTask(object p0, object p1)
internal static string FormatViewComponent_SyncMethod_CannotReturnTask(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_SyncMethod_CannotReturnTask"), p0, p1);
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_SyncMethod_CannotReturnTask"), p0, p1, p2);
}
/// <summary>
/// View component '{0}' must have exactly one public method named '{1}' or '{2}'.
/// </summary>
internal static string ViewComponent_AmbiguousMethods
{
get { return GetString("ViewComponent_AmbiguousMethods"); }
}
/// <summary>
/// View component '{0}' must have exactly one public method named '{1}' or '{2}'.
/// </summary>
internal static string FormatViewComponent_AmbiguousMethods(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AmbiguousMethods"), p0, p1, p2);
}
private static string GetString(string name, params string[] formatterNames)

View File

@ -7,48 +7,76 @@ using Microsoft.AspNet.Html;
namespace Microsoft.AspNet.Mvc.Rendering
{
/// <summary>
/// Extension methods for <see cref="IViewComponentHelper"/>.
/// </summary>
public static class ViewComponentHelperExtensions
{
public static IHtmlContent Invoke<TComponent>(this IViewComponentHelper helper, params object[] args)
/// <summary>
/// Invokes a view component with the specified <paramref name="name"/>.
/// </summary>
/// <param name="name">The name of the view component.</param>
/// <returns>A <see cref="Task"/> that on completion returns the rendered <see cref="IHtmlContent" />.
/// </returns>
public static Task<IHtmlContent> InvokeAsync(this IViewComponentHelper helper, string name)
{
if (helper == null)
{
throw new ArgumentNullException(nameof(helper));
}
return helper.Invoke(typeof(TComponent), args);
return helper.InvokeAsync(name, arguments: null);
}
public static void RenderInvoke<TComponent>(this IViewComponentHelper helper, params object[] args)
/// <summary>
/// Invokes a view component of type <paramref name="componentType" />.
/// </summary>
/// <param name="componentType">The view component <see cref="Type"/>.</param>
/// <returns>A <see cref="Task"/> that on completion returns the rendered <see cref="IHtmlContent" />.
/// </returns>
public static Task<IHtmlContent> InvokeAsync(this IViewComponentHelper helper, Type componentType)
{
if (helper == null)
{
throw new ArgumentNullException(nameof(helper));
}
helper.RenderInvoke(typeof(TComponent), args);
return helper.InvokeAsync(componentType, arguments: null);
}
public static Task<IHtmlContent> InvokeAsync<TComponent>(
this IViewComponentHelper helper,
params object[] args)
/// <summary>
/// Invokes a view component of type <typeparam name="TComponent"/>.
/// </summary>
/// <param name="helper">The <see cref="IViewComponentHelper"/>.</param>
/// <param name="arguments">Arguments to be passed to the invoked view component method.</param>
/// <typeparam name="TComponent">The <see cref="Type"/> of the view component.</typeparam>
/// <returns>A <see cref="Task"/> that on completion returns the rendered <see cref="IHtmlContent" />.
/// </returns>
public static Task<IHtmlContent> InvokeAsync<TComponent>(this IViewComponentHelper helper, object arguments)
{
if (helper == null)
{
throw new ArgumentNullException(nameof(helper));
}
return helper.InvokeAsync(typeof(TComponent), args);
return helper.InvokeAsync(typeof(TComponent), arguments);
}
public static Task RenderInvokeAsync<TComponent>(this IViewComponentHelper helper, params object[] args)
/// <summary>
/// Invokes a view component of type <typeparam name="TComponent"/>.
/// </summary>
/// <param name="helper">The <see cref="IViewComponentHelper"/>.</param>
/// <typeparam name="TComponent">The <see cref="Type"/> of the view component.</typeparam>
/// <returns>A <see cref="Task"/> that on completion returns the rendered <see cref="IHtmlContent" />.
/// </returns>
public static Task<IHtmlContent> InvokeAsync<TComponent>(this IViewComponentHelper helper)
{
if (helper == null)
{
throw new ArgumentNullException(nameof(helper));
}
return helper.RenderInvokeAsync(typeof(TComponent), args);
return helper.InvokeAsync(typeof(TComponent), arguments: null);
}
}
}

View File

@ -122,13 +122,13 @@
<comment>{1} is the newline character</comment>
</data>
<data name="ViewComponent_AsyncMethod_ShouldReturnTask" xml:space="preserve">
<value>The async view component method '{0}' should be declared to return Task&lt;T&gt;.</value>
<value>Method '{0}' of view component '{1}' should be declared to return {2}&lt;T&gt;.</value>
</data>
<data name="ViewComponent_MustReturnValue" xml:space="preserve">
<value>A view component must return a non-null value.</value>
</data>
<data name="ViewComponent_SyncMethod_ShouldReturnValue" xml:space="preserve">
<value>The view component method '{0}' should be declared to return a value.</value>
<value>Method '{0}' of view component '{1}' should be declared to return a value.</value>
</data>
<data name="ViewComponent_CannotFindComponent" xml:space="preserve">
<value>A view component named '{0}' could not be found.</value>
@ -137,10 +137,7 @@
<value>An invoker could not be created for the view component '{0}'.</value>
</data>
<data name="ViewComponent_CannotFindMethod" xml:space="preserve">
<value>Could not find an '{0}' method matching the parameters.</value>
</data>
<data name="ViewComponent_CannotFindMethod_WithFallback" xml:space="preserve">
<value>Could not find an '{0}' or '{1}' method matching the parameters.</value>
<value>Could not find an '{0}' or '{1}' method for the view component '{2}'.</value>
</data>
<data name="ViewComponent_InvalidReturnValue" xml:space="preserve">
<value>View components only support returning {0}, {1} or {2}.</value>
@ -278,6 +275,9 @@
<value>The collection already contains an entry with key '{0}'.</value>
</data>
<data name="ViewComponent_SyncMethod_CannotReturnTask" xml:space="preserve">
<value>The view component method '{0}' cannot return a {1}.</value>
<value>Method '{0}' of view component '{1}' cannot return a {2}.</value>
</data>
<data name="ViewComponent_AmbiguousMethods" xml:space="preserve">
<value>View component '{0}' must have exactly one public method named '{1}' or '{2}'.</value>
</data>
</root>

View File

@ -3,7 +3,9 @@
using System;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNet.Html;
using Microsoft.AspNet.Mvc.Internal;
using Microsoft.AspNet.Mvc.Logging;
using Microsoft.AspNet.Mvc.ModelBinding;
@ -26,7 +28,7 @@ namespace Microsoft.AspNet.Mvc
/// <summary>
/// Gets or sets the arguments provided to the view component.
/// </summary>
public object[] Arguments { get; set; }
public object Arguments { get; set; }
/// <summary>
/// Gets or sets the <see cref="MediaTypeHeaderValue"/> representing the Content-Type header of the response.
@ -77,6 +79,7 @@ namespace Microsoft.AspNet.Mvc
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger<ViewComponentResult>();
var htmlEncoder = services.GetRequiredService<HtmlEncoder>();
var viewData = ViewData;
if (viewData == null)
@ -119,23 +122,29 @@ namespace Microsoft.AspNet.Mvc
htmlHelperOptions);
(viewComponentHelper as ICanHasViewContext)?.Contextualize(viewContext);
var result = await GetViewComponentResult(viewComponentHelper, logger);
if (ViewComponentType == null && ViewComponentName == null)
{
throw new InvalidOperationException(Resources.FormatViewComponentResult_NameOrTypeMustBeSet(
nameof(ViewComponentName),
nameof(ViewComponentType)));
}
else if (ViewComponentType == null)
{
logger.ViewComponentResultExecuting(ViewComponentName, Arguments);
await viewComponentHelper.RenderInvokeAsync(ViewComponentName, Arguments);
}
else
{
logger.ViewComponentResultExecuting(ViewComponentType, Arguments);
await viewComponentHelper.RenderInvokeAsync(ViewComponentType, Arguments);
}
result.WriteTo(writer, htmlEncoder);
}
}
private Task<IHtmlContent> GetViewComponentResult(IViewComponentHelper viewComponentHelper, ILogger logger)
{
if (ViewComponentType == null && ViewComponentName == null)
{
throw new InvalidOperationException(Resources.FormatViewComponentResult_NameOrTypeMustBeSet(
nameof(ViewComponentName),
nameof(ViewComponentType)));
}
else if (ViewComponentType == null)
{
logger.ViewComponentResultExecuting(ViewComponentName);
return viewComponentHelper.InvokeAsync(ViewComponentName, Arguments);
}
else
{
logger.ViewComponentResultExecuting(ViewComponentType);
return viewComponentHelper.InvokeAsync(ViewComponentType, Arguments);
}
}
}

View File

@ -5,7 +5,9 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Infrastructure;
using Microsoft.AspNet.Mvc.ViewFeatures;
namespace Microsoft.AspNet.Mvc.ViewComponents
{
@ -14,6 +16,8 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
/// </summary>
public class DefaultViewComponentDescriptorProvider : IViewComponentDescriptorProvider
{
private const string AsyncMethodName = "InvokeAsync";
private const string SyncMethodName = "Invoke";
private readonly IAssemblyProvider _assemblyProvider;
/// <summary>
@ -32,7 +36,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
return types
.Where(IsViewComponentType)
.Select(CreateCandidate);
.Select(CreateDescriptor);
}
/// <summary>
@ -47,11 +51,11 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
}
/// <summary>
/// Determines whether or not the given <see cref="TypeInfo"/> is a View Component class.
/// Determines whether or not the given <see cref="TypeInfo"/> is a view component class.
/// </summary>
/// <param name="typeInfo">The <see cref="TypeInfo"/>.</param>
/// <returns>
/// <c>true</c> if <paramref name="typeInfo"/>represents a View Component class, otherwise <c>false</c>.
/// <c>true</c> if <paramref name="typeInfo"/>represents a view component class, otherwise <c>false</c>.
/// </returns>
protected virtual bool IsViewComponentType(TypeInfo typeInfo)
{
@ -63,16 +67,70 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
return ViewComponentConventions.IsComponent(typeInfo);
}
private static ViewComponentDescriptor CreateCandidate(TypeInfo typeInfo)
private static ViewComponentDescriptor CreateDescriptor(TypeInfo typeInfo)
{
var candidate = new ViewComponentDescriptor()
var type = typeInfo.AsType();
var candidate = new ViewComponentDescriptor
{
FullName = ViewComponentConventions.GetComponentFullName(typeInfo),
ShortName = ViewComponentConventions.GetComponentName(typeInfo),
Type = typeInfo.AsType(),
Type = type,
MethodInfo = FindMethod(type)
};
return candidate;
}
private static MethodInfo FindMethod(Type componentType)
{
var componentName = componentType.FullName;
var methods = componentType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(method =>
string.Equals(method.Name, AsyncMethodName, StringComparison.Ordinal) ||
string.Equals(method.Name, SyncMethodName, StringComparison.Ordinal))
.ToArray();
if (methods.Length == 0)
{
throw new InvalidOperationException(
Resources.FormatViewComponent_CannotFindMethod(SyncMethodName, AsyncMethodName, componentName));
}
else if (methods.Length > 1)
{
throw new InvalidOperationException(
Resources.FormatViewComponent_AmbiguousMethods(componentName, AsyncMethodName, SyncMethodName));
}
var selectedMethod = methods[0];
if (string.Equals(selectedMethod.Name, AsyncMethodName, StringComparison.Ordinal))
{
if (!selectedMethod.ReturnType.GetTypeInfo().IsGenericType ||
selectedMethod.ReturnType.GetGenericTypeDefinition() != typeof(Task<>))
{
throw new InvalidOperationException(Resources.FormatViewComponent_AsyncMethod_ShouldReturnTask(
AsyncMethodName,
componentName,
nameof(Task)));
}
}
else
{
if (selectedMethod.ReturnType == typeof(void))
{
throw new InvalidOperationException(Resources.FormatViewComponent_SyncMethod_ShouldReturnValue(
SyncMethodName,
componentName));
}
else if (selectedMethod.ReturnType.IsAssignableFrom(typeof(Task)))
{
throw new InvalidOperationException(Resources.FormatViewComponent_SyncMethod_CannotReturnTask(
SyncMethodName,
componentName,
nameof(Task)));
}
}
return selectedMethod;
}
}
}

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNet.Html;
@ -10,9 +9,13 @@ using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Mvc.ViewFeatures.Buffer;
using Microsoft.AspNet.Mvc.ViewFeatures.Internal;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNet.Mvc.ViewComponents
{
/// <summary>
/// Default implementation for <see cref="IViewComponentHelper"/>.
/// </summary>
public class DefaultViewComponentHelper : IViewComponentHelper, ICanHasViewContext
{
private readonly IViewComponentDescriptorCollectionProvider _descriptorProvider;
@ -22,6 +25,16 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
private readonly IViewBufferScope _viewBufferScope;
private ViewContext _viewContext;
/// <summary>
/// Initializes a new instance of <see cref="DefaultViewComponentHelper"/>.
/// </summary>
/// <param name="descriptorProvider">The <see cref="IViewComponentDescriptorCollectionProvider"/>
/// used to locate view components.</param>
/// <param name="htmlEncoder">The <see cref="HtmlEncoder"/>.</param>
/// <param name="selector">The <see cref="IViewComponentSelector"/>.</param>
/// <param name="invokerFactory">The <see cref="IViewComponentInvokerFactory"/>.</param>
/// <param name="viewBufferScope">The <see cref="IViewBufferScope"/> that manages the lifetime of
/// <see cref="ViewBuffer"/> instances.</param>
public DefaultViewComponentHelper(
IViewComponentDescriptorCollectionProvider descriptorProvider,
HtmlEncoder htmlEncoder,
@ -61,6 +74,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
_viewBufferScope = viewBufferScope;
}
/// <inheritdoc />
public void Contextualize(ViewContext viewContext)
{
if (viewContext == null)
@ -71,133 +85,41 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
_viewContext = viewContext;
}
public IHtmlContent Invoke(string name, params object[] arguments)
/// <inheritdoc />
public Task<IHtmlContent> InvokeAsync(string name, object arguments)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
var descriptor = SelectComponent(name);
var viewBuffer = new ViewBuffer(_viewBufferScope, name);
using (var writer = new HtmlContentWrapperTextWriter(viewBuffer, _viewContext.Writer.Encoding))
{
InvokeCore(writer, descriptor, arguments);
return writer.ContentBuilder;
}
}
public IHtmlContent Invoke(Type componentType, params object[] arguments)
{
if (componentType == null)
{
throw new ArgumentNullException(nameof(componentType));
}
var descriptor = SelectComponent(componentType);
var viewBuffer = new ViewBuffer(_viewBufferScope, componentType.Name);
using (var writer = new HtmlContentWrapperTextWriter(viewBuffer, _viewContext.Writer.Encoding))
{
InvokeCore(writer, descriptor, arguments);
return writer.ContentBuilder;
}
}
public void RenderInvoke(string name, params object[] arguments)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
var descriptor = SelectComponent(name);
InvokeCore(_viewContext.Writer, descriptor, arguments);
}
public void RenderInvoke(Type componentType, params object[] arguments)
{
if (componentType == null)
{
throw new ArgumentNullException(nameof(componentType));
}
var descriptor = SelectComponent(componentType);
InvokeCore(_viewContext.Writer, descriptor, arguments);
}
public async Task<IHtmlContent> InvokeAsync(string name, params object[] arguments)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
var descriptor = SelectComponent(name);
var viewBuffer = new ViewBuffer(_viewBufferScope, name);
using (var writer = new HtmlContentWrapperTextWriter(viewBuffer, _viewContext.Writer.Encoding))
{
await InvokeCoreAsync(writer, descriptor, arguments);
return writer.ContentBuilder;
}
}
public async Task<IHtmlContent> InvokeAsync(Type componentType, params object[] arguments)
{
if (componentType == null)
{
throw new ArgumentNullException(nameof(componentType));
}
var descriptor = SelectComponent(componentType);
var viewBuffer = new ViewBuffer(_viewBufferScope, componentType.Name);
using (var writer = new HtmlContentWrapperTextWriter(viewBuffer, _viewContext.Writer.Encoding))
{
await InvokeCoreAsync(writer, descriptor, arguments);
return writer.ContentBuilder;
}
}
public Task RenderInvokeAsync(string name, params object[] arguments)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
var descriptor = SelectComponent(name);
return InvokeCoreAsync(_viewContext.Writer, descriptor, arguments);
}
public Task RenderInvokeAsync(Type componentType, params object[] arguments)
{
if (componentType == null)
{
throw new ArgumentNullException(nameof(componentType));
}
var descriptor = SelectComponent(componentType);
return InvokeCoreAsync(_viewContext.Writer, descriptor, arguments);
}
private ViewComponentDescriptor SelectComponent(string name)
{
var descriptor = _selector.SelectComponent(name);
if (descriptor == null)
{
throw new InvalidOperationException(Resources.FormatViewComponent_CannotFindComponent(name));
}
return descriptor;
return InvokeCoreAsync(descriptor, arguments);
}
/// <inheritdoc />
public Task<IHtmlContent> InvokeAsync(Type componentType, object arguments)
{
if (componentType == null)
{
throw new ArgumentNullException(nameof(componentType));
}
var descriptor = SelectComponent(componentType);
return InvokeCoreAsync(descriptor, arguments);
}
private ViewComponentDescriptor SelectComponent(Type componentType)
{
var descriptors = _descriptorProvider.ViewComponents;
foreach (var descriptor in descriptors.Items)
for (var i = 0; i < descriptors.Items.Count; i++)
{
var descriptor = descriptors.Items[i];
if (descriptor.Type == componentType)
{
return descriptor;
@ -208,58 +130,30 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
componentType.FullName));
}
private Task InvokeCoreAsync(
TextWriter writer,
private async Task<IHtmlContent> InvokeCoreAsync(
ViewComponentDescriptor descriptor,
object[] arguments)
object arguments)
{
if (writer == null)
var viewBuffer = new ViewBuffer(_viewBufferScope, descriptor.FullName);
using (var writer = new HtmlContentWrapperTextWriter(viewBuffer, _viewContext.Writer.Encoding))
{
throw new ArgumentNullException(nameof(writer));
var context = new ViewComponentContext(
descriptor,
PropertyHelper.ObjectToDictionary(arguments),
_htmlEncoder,
_viewContext,
writer);
var invoker = _invokerFactory.CreateInstance(context);
if (invoker == null)
{
throw new InvalidOperationException(
Resources.FormatViewComponent_IViewComponentFactory_ReturnedNull(descriptor.FullName));
}
await invoker.InvokeAsync(context);
return writer.ContentBuilder;
}
if (descriptor == null)
{
throw new ArgumentNullException(nameof(descriptor));
}
var context = new ViewComponentContext(descriptor, arguments, _htmlEncoder, _viewContext, writer);
var invoker = _invokerFactory.CreateInstance(context);
if (invoker == null)
{
throw new InvalidOperationException(
Resources.FormatViewComponent_IViewComponentFactory_ReturnedNull(descriptor.Type.FullName));
}
return invoker.InvokeAsync(context);
}
private void InvokeCore(
TextWriter writer,
ViewComponentDescriptor descriptor,
object[] arguments)
{
if (writer == null)
{
throw new ArgumentNullException(nameof(writer));
}
if (descriptor == null)
{
throw new ArgumentNullException(nameof(descriptor));
}
var context = new ViewComponentContext(descriptor, arguments, _htmlEncoder, _viewContext, writer);
var invoker = _invokerFactory.CreateInstance(context);
if (invoker == null)
{
throw new InvalidOperationException(
Resources.FormatViewComponent_IViewComponentFactory_ReturnedNull(descriptor.Type.FullName));
}
invoker.Invoke(context);
}
}
}

View File

@ -16,6 +16,9 @@ using Microsoft.Extensions.Logging;
namespace Microsoft.AspNet.Mvc.ViewComponents
{
/// <summary>
/// Default implementation for <see cref="IViewComponentInvoker"/>.
/// </summary>
public class DefaultViewComponentInvoker : IViewComponentInvoker
{
private readonly ITypeActivatorCache _typeActivatorCache;
@ -23,6 +26,13 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
private readonly DiagnosticSource _diagnosticSource;
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of <see cref="DefaultViewComponentInvoker"/>.
/// </summary>
/// <param name="typeActivatorCache">Caches factories for instantiating view component instances.</param>
/// <param name="viewComponentActivator">The <see cref="IViewComponentActivator"/>.</param>
/// <param name="diagnosticSource">The <see cref="DiagnosticSource"/>.</param>
/// <param name="logger">The <see cref="ILogger"/>.</param>
public DefaultViewComponentInvoker(
ITypeActivatorCache typeActivatorCache,
IViewComponentActivator viewComponentActivator,
@ -55,27 +65,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
_logger = logger;
}
public void Invoke(ViewComponentContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var method = ViewComponentMethodSelector.FindSyncMethod(
context.ViewComponentDescriptor.Type.GetTypeInfo(),
context.Arguments);
if (method == null)
{
throw new InvalidOperationException(
Resources.FormatViewComponent_CannotFindMethod(ViewComponentMethodSelector.SyncMethodName));
}
var result = InvokeSyncCore(method, context);
result.Execute(context);
}
/// <inheritdoc />
public async Task InvokeAsync(ViewComponentContext context)
{
if (context == null)
@ -83,32 +73,25 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
throw new ArgumentNullException(nameof(context));
}
IViewComponentResult result;
var asyncMethod = ViewComponentMethodSelector.FindAsyncMethod(
context.ViewComponentDescriptor.Type.GetTypeInfo(),
context.Arguments);
if (asyncMethod == null)
var methodInfo = context.ViewComponentDescriptor?.MethodInfo;
if (methodInfo == null)
{
// We support falling back to synchronous if there is no InvokeAsync method, in this case we'll still
// execute the IViewResult asynchronously.
var syncMethod = ViewComponentMethodSelector.FindSyncMethod(
context.ViewComponentDescriptor.Type.GetTypeInfo(),
context.Arguments);
if (syncMethod == null)
{
throw new InvalidOperationException(
Resources.FormatViewComponent_CannotFindMethod_WithFallback(
ViewComponentMethodSelector.SyncMethodName, ViewComponentMethodSelector.AsyncMethodName));
}
else
{
result = InvokeSyncCore(syncMethod, context);
}
throw new InvalidOperationException(Resources.FormatPropertyOfTypeCannotBeNull(
nameof(ViewComponentDescriptor.MethodInfo),
nameof(ViewComponentDescriptor)));
}
var isAsync = typeof(Task).GetTypeInfo().IsAssignableFrom(methodInfo.ReturnType.GetTypeInfo());
IViewComponentResult result;
if (isAsync)
{
result = await InvokeAsyncCore(context);
}
else
{
result = await InvokeAsyncCore(asyncMethod, context);
// We support falling back to synchronous if there is no InvokeAsync method, in this case we'll still
// execute the IViewResult asynchronously.
result = InvokeSyncCore(context);
}
await result.ExecuteAsync(context);
@ -129,29 +112,20 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
return component;
}
private async Task<IViewComponentResult> InvokeAsyncCore(
MethodInfo method,
ViewComponentContext context)
private async Task<IViewComponentResult> InvokeAsyncCore(ViewComponentContext context)
{
if (method == null)
{
throw new ArgumentNullException(nameof(method));
}
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var component = CreateComponent(context);
using (_logger.ViewComponentScope(context))
{
var method = context.ViewComponentDescriptor.MethodInfo;
var arguments = ControllerActionExecutor.PrepareArguments(context.Arguments, method.GetParameters());
_diagnosticSource.BeforeViewComponent(context, component);
_logger.ViewComponentExecuting(context);
_logger.ViewComponentExecuting(context, arguments);
var startTime = Environment.TickCount;
var result = await ControllerActionExecutor.ExecuteAsync(method, component, context.Arguments);
var result = await ControllerActionExecutor.ExecuteAsync(method, component, arguments);
var viewComponentResult = CoerceToViewComponentResult(result);
_logger.ViewComponentExecuted(context, startTime, viewComponentResult);
@ -161,35 +135,25 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
}
}
public IViewComponentResult InvokeSyncCore(MethodInfo method, ViewComponentContext context)
private IViewComponentResult InvokeSyncCore(ViewComponentContext context)
{
if (method == null)
{
throw new ArgumentNullException(nameof(method));
}
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var component = CreateComponent(context);
using (_logger.ViewComponentScope(context))
{
_diagnosticSource.BeforeViewComponent(context, component);
_logger.ViewComponentExecuting(context);
var method = context.ViewComponentDescriptor.MethodInfo;
var arguments = ControllerActionExecutor.PrepareArguments(
context.Arguments,
method.GetParameters());
_diagnosticSource.BeforeViewComponent(context, component);
_logger.ViewComponentExecuting(context, arguments);
var startTime = Environment.TickCount;
object result;
try
{
var startTime = Environment.TickCount;
var result = method.Invoke(component, context.Arguments);
var viewComponentResult = CoerceToViewComponentResult(result);
_logger.ViewComponentExecuted(context, startTime, viewComponentResult);
_diagnosticSource.AfterViewComponent(context, viewComponentResult, component);
return viewComponentResult;
result = method.Invoke(component, arguments);
}
catch (TargetInvocationException ex)
{
@ -198,6 +162,12 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
exceptionInfo.Throw();
return null; // Unreachable
}
var viewComponentResult = CoerceToViewComponentResult(result);
_logger.ViewComponentExecuted(context, startTime, viewComponentResult);
_diagnosticSource.AfterViewComponent(context, viewComponentResult, component);
return viewComponentResult;
}
}

View File

@ -6,7 +6,7 @@ using System.Collections.Generic;
namespace Microsoft.AspNet.Mvc.ViewComponents
{
/// <summary>
/// Discovers the View Components in the application.
/// Discovers the view components in the application.
/// </summary>
public interface IViewComponentDescriptorProvider
{

View File

@ -5,10 +5,17 @@ using System.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.ViewComponents
{
/// <summary>
/// Specifies the contract for execution of a view component.
/// </summary>
public interface IViewComponentInvoker
{
void Invoke(ViewComponentContext context);
/// <summary>
/// Executes the view component specified by <see cref="ViewComponentContext.ViewComponentDescriptor"/>
/// of <paramref name="context"/> and writes the result to <see cref="ViewComponentContext.Writer"/>.
/// </summary>
/// <param name="context">The <see cref="ViewComponentContext"/>.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous operation of execution.</returns>
Task InvokeAsync(ViewComponentContext context);
}
}

View File

@ -4,14 +4,14 @@
namespace Microsoft.AspNet.Mvc.ViewComponents
{
/// <summary>
/// Selects a View Component based on a View Component name.
/// Selects a view component based on a view component name.
/// </summary>
public interface IViewComponentSelector
{
/// <summary>
/// Selects a View Component based on <paramref name="componentName"/>.
/// Selects a view component based on <paramref name="componentName"/>.
/// </summary>
/// <param name="componentName">The View Component name.</param>
/// <param name="componentName">The view component name.</param>
/// <returns>A <see cref="ViewComponentDescriptor"/>, or <c>null</c> if no match is found.</returns>
ViewComponentDescriptor SelectComponent(string componentName);
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Encodings.Web;
using Microsoft.AspNet.Mvc.Rendering;
@ -10,7 +11,7 @@ using Microsoft.AspNet.Mvc.ViewFeatures;
namespace Microsoft.AspNet.Mvc.ViewComponents
{
/// <summary>
/// A context for View Components.
/// A context for view components.
/// </summary>
public class ViewComponentContext
{
@ -23,7 +24,6 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
public ViewComponentContext()
{
ViewComponentDescriptor = new ViewComponentDescriptor();
Arguments = new object[0];
ViewContext = new ViewContext();
}
@ -31,14 +31,14 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
/// Creates a new <see cref="ViewComponentContext"/>.
/// </summary>
/// <param name="viewComponentDescriptor">
/// The <see cref="ViewComponentContext"/> for the View Component being invoked.
/// The <see cref="ViewComponentContext"/> for the view component being invoked.
/// </param>
/// <param name="arguments">The View Component arguments.</param>
/// <param name="arguments">The view component arguments.</param>
/// <param name="viewContext">The <see cref="ViewContext"/>.</param>
/// <param name="writer">The <see cref="TextWriter"/> for writing output.</param>
public ViewComponentContext(
ViewComponentDescriptor viewComponentDescriptor,
object[] arguments,
IDictionary<string, object> arguments,
HtmlEncoder htmlEncoder,
ViewContext viewContext,
TextWriter writer)
@ -82,15 +82,15 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
}
/// <summary>
/// Gets or sets the View Component arguments.
/// Gets or sets the view component arguments.
/// </summary>
/// <remarks>
/// The property setter is provided for unit test purposes only.
/// </remarks>
public object[] Arguments { get; set; }
public IDictionary<string, object> Arguments { get; set; }
/// <summary>
/// Gets or sets the <see cref="HtmlEncoder"/>.
/// Gets or sets the <see cref="System.Text.Encodings.Web.HtmlEncoder"/>.
/// </summary>
/// <remarks>
/// The property setter is provided for unit test purposes only.
@ -98,7 +98,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
public HtmlEncoder HtmlEncoder { get; set; }
/// <summary>
/// Gets or sets the <see cref="ViewComponentDescriptor"/> for the View Component being invoked.
/// Gets or sets the <see cref="ViewComponents.ViewComponentDescriptor"/> for the view component being invoked.
/// </summary>
/// <remarks>
/// The property setter is provided for unit test purposes only.
@ -106,7 +106,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
public ViewComponentDescriptor ViewComponentDescriptor { get; set; }
/// <summary>
/// Gets or sets the <see cref="ViewContext"/>.
/// Gets or sets the <see cref="Rendering.ViewContext"/>.
/// </summary>
/// <remarks>
/// The property setter is provided for unit test purposes only.

View File

@ -3,11 +3,12 @@
using System;
using System.Diagnostics;
using System.Reflection;
namespace Microsoft.AspNet.Mvc.ViewComponents
{
/// <summary>
/// A descriptor for a View Component.
/// A descriptor for a view component.
/// </summary>
[DebuggerDisplay("{DisplayName}")]
public class ViewComponentDescriptor
@ -23,7 +24,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
}
/// <summary>
/// Gets or sets the display name of the View Component.
/// Gets or sets the display name of the view component.
/// </summary>
public string DisplayName
{
@ -53,8 +54,8 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
/// </summary>
/// <remarks>
/// <para>
/// The full name is defaulted to the full namespace of the View Component class, prepended to
/// the the class name with a '.' character as the separator. If the View Component class uses
/// The full name is defaulted to the full namespace of the view component class, prepended to
/// the the class name with a '.' character as the separator. If the view component class uses
/// <code>ViewComponent</code> as a suffix, the suffix will be omitted from the <see cref="FullName"/>.
/// </para>
/// <example>
@ -89,7 +90,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
/// </summary>
/// <remarks>
/// <para>
/// The short name is defaulted to the name of the View Component class. If the View Component class uses
/// The short name is defaulted to the name of the view component class. If the view component class uses
/// <code>ViewComponent</code> as a suffix, the suffix will be omitted from the <see cref="ShortName"/>.
/// </para>
/// <example>
@ -115,8 +116,13 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
public string ShortName { get; set; }
/// <summary>
/// Gets or sets the <see cref="Type"/>.
/// Gets or sets the <see cref="System.Type"/>.
/// </summary>
public Type Type { get; set; }
/// <summary>
/// Gets or sets the <see cref="System.Reflection.MethodInfo"/> to invoke.
/// </summary>
public MethodInfo MethodInfo { get; set; }
}
}

View File

@ -1,98 +0,0 @@
// 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.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.ViewFeatures;
namespace Microsoft.AspNet.Mvc.ViewComponents
{
public static class ViewComponentMethodSelector
{
public const string AsyncMethodName = "InvokeAsync";
public const string SyncMethodName = "Invoke";
public static MethodInfo FindAsyncMethod(TypeInfo componentType, object[] args)
{
if (componentType == null)
{
throw new ArgumentNullException(nameof(componentType));
}
var method = GetMethod(componentType, args, AsyncMethodName);
if (method == null)
{
return null;
}
if (!method.ReturnType.GetTypeInfo().IsGenericType ||
method.ReturnType.GetGenericTypeDefinition() != typeof(Task<>))
{
throw new InvalidOperationException(
Resources.FormatViewComponent_AsyncMethod_ShouldReturnTask(AsyncMethodName));
}
return method;
}
public static MethodInfo FindSyncMethod(TypeInfo componentType, object[] args)
{
if (componentType == null)
{
throw new ArgumentNullException(nameof(componentType));
}
var method = GetMethod(componentType, args, SyncMethodName);
if (method == null)
{
return null;
}
if (method.ReturnType == typeof(void))
{
throw new InvalidOperationException(
Resources.FormatViewComponent_SyncMethod_ShouldReturnValue(SyncMethodName));
}
else if (method.ReturnType.IsAssignableFrom(typeof(Task)))
{
throw new InvalidOperationException(
Resources.FormatViewComponent_SyncMethod_CannotReturnTask(SyncMethodName, nameof(Task)));
}
return method;
}
private static MethodInfo GetMethod(TypeInfo componentType, object[] args, string methodName)
{
Type[] types;
if (args == null || args.Length == 0)
{
types = Type.EmptyTypes;
}
else
{
types = new Type[args.Length];
for (var i = 0; i < args.Length; i++)
{
types[i] = args[i]?.GetType() ?? typeof(object);
}
}
#if NET451
return componentType.AsType().GetMethod(
methodName,
BindingFlags.Public | BindingFlags.Instance,
binder: null,
types: types,
modifiers: null);
#else
var method = componentType.AsType().GetMethod(methodName, types: types);
// At most one method (including static and instance methods) with the same parameter types can exist
// per type.
return method != null && method.IsStatic ? null : method;
#endif
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
@ -42,13 +43,14 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.Text",
ShortName = "Text",
Type = typeof(TextViewComponent),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)),
};
var actionContext = CreateActionContext(descriptor);
var viewComponentResult = new ViewComponentResult
{
Arguments = new object[] { "World!" },
Arguments = new { name = "World!" },
ViewData = null,
TempData = null,
ViewComponentName = "Text"
@ -132,13 +134,43 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.Text",
ShortName = "Text",
Type = typeof(TextViewComponent),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)),
};
var actionContext = CreateActionContext(descriptor);
var viewComponentResult = new ViewComponentResult()
{
Arguments = new object[] { "World!" },
Arguments = new { name = "World!" },
ViewComponentName = "Text",
TempData = _tempDataDictionary,
};
// Act
await viewComponentResult.ExecuteResultAsync(actionContext);
// Assert
var body = ReadBody(actionContext.HttpContext.Response);
Assert.Equal("Hello, World!", body);
}
[Fact]
public async Task ExecuteResultAsync_UsesDictionaryArguments()
{
// Arrange
var descriptor = new ViewComponentDescriptor()
{
FullName = "Full.Name.Text",
ShortName = "Text",
Type = typeof(TextViewComponent),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)),
};
var actionContext = CreateActionContext(descriptor);
var viewComponentResult = new ViewComponentResult()
{
Arguments = new Dictionary<string, object> { ["name"] = "World!" },
ViewComponentName = "Text",
TempData = _tempDataDictionary,
};
@ -160,13 +192,14 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.AsyncText",
ShortName = "AsyncText",
Type = typeof(AsyncTextViewComponent),
MethodInfo = typeof(AsyncTextViewComponent).GetMethod(nameof(AsyncTextViewComponent.InvokeAsync)),
};
var actionContext = CreateActionContext(descriptor);
var viewComponentResult = new ViewComponentResult()
{
Arguments = new object[] { "World!" },
Arguments = new { name = "World!" },
ViewComponentName = "AsyncText",
TempData = _tempDataDictionary,
};
@ -188,6 +221,7 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.Text",
ShortName = "Text",
Type = typeof(TextViewComponent),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)),
};
var adapter = new TestDiagnosticListener();
@ -196,7 +230,7 @@ namespace Microsoft.AspNet.Mvc
var viewComponentResult = new ViewComponentResult()
{
Arguments = new object[] { "World!" },
Arguments = new { name = "World!" },
ViewComponentName = "Text",
TempData = _tempDataDictionary,
};
@ -226,13 +260,14 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.Text",
ShortName = "Text",
Type = typeof(TextViewComponent),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)),
};
var actionContext = CreateActionContext(descriptor);
var viewComponentResult = new ViewComponentResult()
{
Arguments = new object[] { "World!" },
Arguments = new { name = "World!" },
ViewComponentName = "Text",
TempData = _tempDataDictionary,
};
@ -254,13 +289,14 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.Text",
ShortName = "Text",
Type = typeof(TextViewComponent),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)),
};
var actionContext = CreateActionContext(descriptor);
var viewComponentResult = new ViewComponentResult()
{
Arguments = new object[] { "World!" },
Arguments = new { name = "World!" },
ViewComponentName = "Full.Name.Text",
TempData = _tempDataDictionary,
};
@ -282,13 +318,14 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.Text",
ShortName = "Text",
Type = typeof(TextViewComponent),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)),
};
var actionContext = CreateActionContext(descriptor);
var viewComponentResult = new ViewComponentResult()
{
Arguments = new object[] { "World!" },
Arguments = new { name = "World!" },
ViewComponentType = typeof(TextViewComponent),
TempData = _tempDataDictionary,
};
@ -310,13 +347,14 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.Text",
ShortName = "Text",
Type = typeof(TextViewComponent),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke))
};
var actionContext = CreateActionContext(descriptor);
var viewComponentResult = new ViewComponentResult()
{
Arguments = new object[] { "World!" },
Arguments = new { name = "World!" },
ViewComponentType = typeof(TextViewComponent),
StatusCode = 404,
TempData = _tempDataDictionary,
@ -367,6 +405,7 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.Text",
ShortName = "Text",
Type = typeof(TextViewComponent),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)),
};
var actionContext = CreateActionContext(descriptor);
@ -375,7 +414,7 @@ namespace Microsoft.AspNet.Mvc
var viewComponentResult = new ViewComponentResult()
{
Arguments = new object[] { "World!" },
Arguments = new { name = "World!" },
ViewComponentName = "Text",
ContentType = contentType,
TempData = _tempDataDictionary,
@ -403,6 +442,7 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.Text",
ShortName = "Text",
Type = typeof(TextViewComponent),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)),
};
var actionContext = CreateActionContext(descriptor);
@ -412,7 +452,7 @@ namespace Microsoft.AspNet.Mvc
var viewComponentResult = new ViewComponentResult()
{
Arguments = new object[] { "World!" },
Arguments = new { name = "World!" },
ViewComponentName = "Text",
ContentType = new MediaTypeHeaderValue("text/html") { Encoding = Encoding.UTF8 },
TempData = _tempDataDictionary,
@ -439,6 +479,7 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.Text",
ShortName = "Text",
Type = typeof(TextViewComponent),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)),
};
var actionContext = CreateActionContext(descriptor);
@ -447,7 +488,7 @@ namespace Microsoft.AspNet.Mvc
var viewComponentResult = new ViewComponentResult()
{
Arguments = new object[] { "World!" },
Arguments = new { name = "World!" },
ViewComponentName = "Text",
TempData = _tempDataDictionary,
};
@ -459,7 +500,10 @@ namespace Microsoft.AspNet.Mvc
Assert.Equal(expectedContentType, actionContext.HttpContext.Response.ContentType);
}
private IServiceCollection CreateServices(object diagnosticListener, HttpContext context, params ViewComponentDescriptor[] descriptors)
private IServiceCollection CreateServices(
object diagnosticListener,
HttpContext context,
params ViewComponentDescriptor[] descriptors)
{
var httpContext = new DefaultHttpContext();
var diagnosticSource = new DiagnosticListener("Microsoft.AspNet");
@ -535,12 +579,6 @@ namespace Microsoft.AspNet.Mvc
private class AsyncTextViewComponent : ViewComponent
{
public HtmlString Invoke()
{
// Should never run.
throw null;
}
public Task<HtmlString> InvokeAsync(string name)
{
return Task.FromResult(new HtmlString("Hello-Async, " + name));

View File

@ -1,6 +1,7 @@
// 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.Collections.Generic;
using System.IO;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions;
@ -57,7 +58,7 @@ namespace Microsoft.AspNet.Mvc
var viewComponentContext = new ViewComponentContext(
viewComponentDescriptor,
new object[0],
new Dictionary<string, object>(),
new HtmlTestEncoder(),
viewContext,
writer);

View File

@ -2,13 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Routing;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.ViewComponents

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Infrastructure;
using Xunit;
@ -23,9 +24,10 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
// Assert
var descriptor = Assert.Single(descriptors);
Assert.Equal(typeof(ConventionsViewComponent), descriptor.Type);
Assert.Same(typeof(ConventionsViewComponent), descriptor.Type);
Assert.Equal("Microsoft.AspNet.Mvc.ViewComponents.Conventions", descriptor.FullName);
Assert.Equal("Conventions", descriptor.ShortName);
Assert.Same(typeof(ConventionsViewComponent).GetMethod("Invoke"), descriptor.MethodInfo);
}
[Fact]
@ -42,15 +44,156 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
Assert.Equal(typeof(AttributeViewComponent), descriptor.Type);
Assert.Equal("AttributesAreGreat", descriptor.FullName);
Assert.Equal("AttributesAreGreat", descriptor.ShortName);
Assert.Same(typeof(AttributeViewComponent).GetMethod("InvokeAsync"), descriptor.MethodInfo);
}
[Theory]
[InlineData(typeof(NoMethodsViewComponent))]
[InlineData(typeof(NonPublicInvokeAsyncViewComponent))]
[InlineData(typeof(NonPublicInvokeViewComponent))]
public void GetViewComponents_ThrowsIfTypeHasNoInvocationMethods(Type type)
{
// Arrange
var expected = $"Could not find an 'Invoke' or 'InvokeAsync' method for the view component '{type}'.";
var provider = CreateProvider(type);
// Act
var ex = Assert.Throws<InvalidOperationException>(() => provider.GetViewComponents().ToArray());
Assert.Equal(expected, ex.Message);
}
[Theory]
[InlineData(typeof(MultipleInvokeViewComponent))]
[InlineData(typeof(MultipleInvokeAsyncViewComponent))]
[InlineData(typeof(InvokeAndInvokeAsyncViewComponent))]
public void GetViewComponents_ThrowsIfTypeHasAmbiguousInvocationMethods(Type type)
{
// Arrange
var expected = $"View component '{type}' must have exactly one public method named " +
"'InvokeAsync' or 'Invoke'.";
var provider = CreateProvider(type);
// Act
var ex = Assert.Throws<InvalidOperationException>(() => provider.GetViewComponents().ToArray());
Assert.Equal(expected, ex.Message);
}
[Theory]
[InlineData(typeof(NonGenericTaskReturningInvokeAsyncViewComponent))]
[InlineData(typeof(VoidReturningInvokeAsyncViewComponent))]
[InlineData(typeof(NonTaskReturningInvokeAsyncViewComponent))]
public void GetViewComponents_ThrowsIfInvokeAsyncDoesNotHaveCorrectReturnType(Type type)
{
// Arrange
var expected = $"Method 'InvokeAsync' of view component '{type}' should be declared to return Task<T>.";
var provider = CreateProvider(type);
// Act and Assert
var ex = Assert.Throws<InvalidOperationException>(() => provider.GetViewComponents().ToArray());
Assert.Equal(expected, ex.Message);
}
[Fact]
public void GetViewComponents_ThrowsIfInvokeReturnsATask()
{
// Arrange
var type = typeof(TaskReturningInvokeViewComponent);
var expected = $"Method 'Invoke' of view component '{type}' cannot return a Task.";
var provider = CreateProvider(type);
// Act and Assert
var ex = Assert.Throws<InvalidOperationException>(() => provider.GetViewComponents().ToArray());
Assert.Equal(expected, ex.Message);
}
[Fact]
public void GetViewComponents_ThrowsIfInvokeIsVoidReturning()
{
// Arrange
var type = typeof(VoidReturningInvokeViewComponent);
var expected = $"Method 'Invoke' of view component '{type}' should be declared to return a value.";
var provider = CreateProvider(type);
// Act and Assert
var ex = Assert.Throws<InvalidOperationException>(() => provider.GetViewComponents().ToArray());
Assert.Equal(expected, ex.Message);
}
private class ConventionsViewComponent
{
public string Invoke() => "Hello world";
}
[ViewComponent(Name = "AttributesAreGreat")]
private class AttributeViewComponent
{
public Task<string> InvokeAsync() => Task.FromResult("Hello world");
}
private class MultipleInvokeViewComponent
{
public IViewComponentResult Invoke() => null;
public IViewComponentResult Invoke(int a) => null;
}
private class NoMethodsViewComponent
{
}
private class NonPublicInvokeViewComponent
{
private IViewComponentResult Invoke() => null;
}
private class NonPublicInvokeAsyncViewComponent
{
protected Task<IViewComponentResult> InvokeAsync() => null;
}
private class MultipleInvokeAsyncViewComponent
{
public Task<IViewComponentResult> InvokeAsync(string a) => null;
public Task<IViewComponentResult> InvokeAsync(int a) => null;
public Task<IViewComponentResult> InvokeAsync(int a, int b) => null;
}
private class InvokeAndInvokeAsyncViewComponent
{
public Task<IViewComponentResult> InvokeAsync(string a) => null;
public string InvokeAsync(int a) => null;
}
private class NonGenericTaskReturningInvokeAsyncViewComponent
{
public Task InvokeAsync() => Task.FromResult(0);
}
private class VoidReturningInvokeAsyncViewComponent
{
public void InvokeAsync()
{
}
}
public class NonTaskReturningInvokeAsyncViewComponent
{
public long InvokeAsync() => 0L;
}
public class TaskReturningInvokeViewComponent
{
public Task Invoke() => Task.FromResult(0);
}
public class VoidReturningInvokeViewComponent
{
public void Invoke(int x)
{
}
}
private DefaultViewComponentDescriptorProvider CreateProvider(Type componentType)
@ -80,11 +223,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
return
GetAssemblyProvider()
.CandidateAssemblies
.SelectMany(a => a.DefinedTypes)
#if DNX451
.Select(t => t.GetTypeInfo())
#endif
;
.SelectMany(a => a.DefinedTypes);
}
private static IAssemblyProvider GetAssemblyProvider()

View File

@ -12,6 +12,8 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
{
public class DefaultViewComponentSelectorTest
{
private static readonly string Namespace = typeof(DefaultViewComponentSelectorTest).Namespace;
[Fact]
public void SelectComponent_ByShortNameWithSuffix()
{
@ -22,7 +24,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
var result = selector.SelectComponent("Suffix");
// Assert
Assert.Equal(typeof(SuffixViewComponent), result.Type);
Assert.Same(typeof(ViewComponentContainer.SuffixViewComponent), result.Type);
}
[Fact]
@ -32,10 +34,10 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
var selector = CreateSelector();
// Act
var result = selector.SelectComponent("Microsoft.AspNet.Mvc.ViewComponents.Suffix");
var result = selector.SelectComponent($"{Namespace}.Suffix");
// Assert
Assert.Equal(typeof(SuffixViewComponent), result.Type);
Assert.Same(typeof(ViewComponentContainer.SuffixViewComponent), result.Type);
}
[Fact]
@ -48,7 +50,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
var result = selector.SelectComponent("WithoutSuffix");
// Assert
Assert.Equal(typeof(WithoutSuffix), result.Type);
Assert.Same(typeof(ViewComponentContainer.WithoutSuffix), result.Type);
}
[Fact]
@ -58,10 +60,10 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
var selector = CreateSelector();
// Act
var result = selector.SelectComponent("Microsoft.AspNet.Mvc.ViewComponents.WithoutSuffix");
var result = selector.SelectComponent($"{Namespace}.WithoutSuffix");
// Assert
Assert.Equal(typeof(WithoutSuffix), result.Type);
Assert.Same(typeof(ViewComponentContainer.WithoutSuffix), result.Type);
}
[Fact]
@ -74,7 +76,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
var result = selector.SelectComponent("ByAttribute");
// Assert
Assert.Equal(typeof(ByAttribute), result.Type);
Assert.Same(typeof(ViewComponentContainer.ByAttribute), result.Type);
}
[Fact]
@ -87,7 +89,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
var result = selector.SelectComponent("ByNamingConvention");
// Assert
Assert.Equal(typeof(ByNamingConventionViewComponent), result.Type);
Assert.Same(typeof(ViewComponentContainer.ByNamingConventionViewComponent), result.Type);
}
[Fact]
@ -98,9 +100,9 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
var expected =
"The view component name 'Ambiguous' matched multiple types:" + Environment.NewLine +
"Type: 'Microsoft.AspNet.Mvc.ViewComponents.DefaultViewComponentSelectorTest+Ambiguous1' - " +
$"Type: '{typeof(ViewComponentContainer.Ambiguous1)}' - " +
"Name: 'Namespace1.Ambiguous'" + Environment.NewLine +
"Type: 'Microsoft.AspNet.Mvc.ViewComponents.DefaultViewComponentSelectorTest+Ambiguous2' - " +
$"Type: '{typeof(ViewComponentContainer.Ambiguous2)}' - " +
"Name: 'Namespace2.Ambiguous'";
// Act
@ -120,7 +122,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
var result = selector.SelectComponent("Namespace1.Ambiguous");
// Assert
Assert.Equal(typeof(Ambiguous1), result.Type);
Assert.Same(typeof(ViewComponentContainer.Ambiguous1), result.Type);
}
[Theory]
@ -135,44 +137,56 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
var result = selector.SelectComponent(name);
// Assert
Assert.Equal(typeof(FullNameInAttribute), result.Type);
Assert.Same(typeof(ViewComponentContainer.FullNameInAttribute), result.Type);
}
private IViewComponentSelector CreateSelector()
{
return new FilteredViewComponentSelector();
var provider = new DefaultViewComponentDescriptorCollectionProvider(
new FilteredViewComponentDescriptorProvider());
return new DefaultViewComponentSelector(provider);
}
private class SuffixViewComponent : ViewComponent
private class ViewComponentContainer
{
}
public class SuffixViewComponent : ViewComponent
{
public string Invoke() => "Hello";
}
private class WithoutSuffix : ViewComponent
{
}
public class WithoutSuffix : ViewComponent
{
public string Invoke() => "Hello";
}
private class ByNamingConventionViewComponent
{
}
public class ByNamingConventionViewComponent
{
public string Invoke() => "Hello";
}
[ViewComponent]
private class ByAttribute
{
}
[ViewComponent]
public class ByAttribute
{
public string Invoke() => "Hello";
}
[ViewComponent(Name = "Namespace1.Ambiguous")]
private class Ambiguous1
{
}
[ViewComponent(Name = "Namespace1.Ambiguous")]
public class Ambiguous1
{
public string Invoke() => "Hello";
}
[ViewComponent(Name = "Namespace2.Ambiguous")]
private class Ambiguous2
{
}
[ViewComponent(Name = "Namespace2.Ambiguous")]
public class Ambiguous2
{
public string Invoke() => "Hello";
}
[ViewComponent(Name = "CoolNameSpace.FullNameInAttribute")]
private class FullNameInAttribute
{
[ViewComponent(Name = "CoolNameSpace.FullNameInAttribute")]
public class FullNameInAttribute
{
public string Invoke() => "Hello";
}
}
// This will only consider types nested inside this class as ViewComponent classes
@ -181,10 +195,10 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
public FilteredViewComponentDescriptorProvider()
: base(GetAssemblyProvider())
{
AllowedTypes = typeof(DefaultViewComponentSelectorTest).GetNestedTypes(BindingFlags.NonPublic);
AllowedTypes = typeof(ViewComponentContainer).GetNestedTypes(bindingAttr: BindingFlags.Public);
}
public Type[] AllowedTypes { get; private set; }
public Type[] AllowedTypes { get; }
protected override bool IsViewComponentType(TypeInfo typeInfo)
{
@ -208,19 +222,10 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
{
var assemblyProvider = new StaticAssemblyProvider();
assemblyProvider.CandidateAssemblies.Add(
typeof(FilteredViewComponentSelector).GetTypeInfo().Assembly);
typeof(ViewComponentContainer).GetTypeInfo().Assembly);
return assemblyProvider;
}
}
// This will only consider types nested inside this class as ViewComponent classes
private class FilteredViewComponentSelector : DefaultViewComponentSelector
{
public FilteredViewComponentSelector()
: base(new DefaultViewComponentDescriptorCollectionProvider(new FilteredViewComponentDescriptorProvider()))
{
}
}
}
}

View File

@ -1,7 +1,7 @@
// 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.
#if MOCK_SUPPORT
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions;
@ -57,7 +57,7 @@ namespace Microsoft.AspNet.Mvc
var viewComponentContext = new ViewComponentContext(
viewComponentDescriptor,
new object[0],
new Dictionary<string, object>(),
new HtmlTestEncoder(),
viewContext,
writer);
@ -66,4 +66,3 @@ namespace Microsoft.AspNet.Mvc
}
}
}
#endif

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.AspNet.Http;
@ -85,7 +86,7 @@ namespace Microsoft.AspNet.Mvc
var viewComponentContext = new ViewComponentContext(
viewComponentDescriptor,
new object[0],
new Dictionary<string, object>(),
new HtmlTestEncoder(),
viewContext,
writer);

View File

@ -1,282 +0,0 @@
// 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.Generic;
using System.Globalization;
using System.Reflection;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNet.Mvc.ViewComponents
{
public class ViewComponentMethodSelectorTest
{
[Theory]
[InlineData(typeof(ViewComponentWithSyncInvoke), new object[] { "" })]
[InlineData(typeof(ViewComponentWithAsyncInvoke), new object[] { 42, false })]
[InlineData(typeof(ViewComponentWithNonPublicNonInstanceInvokes), new object[] { })]
[InlineData(typeof(ViewComponentWithNonPublicNonInstanceInvokes), new object[] { "" })]
public void FindAsyncMethod_ReturnsNull_IfMatchCannotBeFound(Type type, object[] args)
{
// Arrange
var typeInfo = type.GetTypeInfo();
// Act
var method = ViewComponentMethodSelector.FindAsyncMethod(typeInfo, args);
// Assert
Assert.Null(method);
}
[Theory]
[InlineData(typeof(ViewComponentWithAsyncInvoke), new object[0])]
[InlineData(typeof(ViewComponentWithSyncInvoke), new object[] { "" })]
[InlineData(typeof(ViewComponentWithAsyncInvoke), new object[] { "" })]
[InlineData(typeof(ViewComponentWithSyncInvoke), new object[] { 42 })]
[InlineData(typeof(ViewComponentWithAsyncInvoke), new object[] { "", 42 })]
[InlineData(typeof(ViewComponentWithNonPublicNonInstanceInvokes), new object[] { })]
[InlineData(typeof(ViewComponentWithNonPublicNonInstanceInvokes), new object[] { "" })]
[InlineData(typeof(BaseClass), new object[] { })]
public void FindSyncMethod_ReturnsNull_IfMatchCannotBeFound(Type type, object[] args)
{
// Arrange
var typeInfo = type.GetTypeInfo();
// Act
var method = ViewComponentMethodSelector.FindSyncMethod(typeInfo, args);
// Assert
Assert.Null(method);
}
[Theory]
[InlineData(new object[] { new object[] { "Hello" } })]
[InlineData(new object[] { new object[] { 4 } })]
[InlineData(new object[] { new object[] { "", 5 } })]
public void FindAsyncMethod_ThrowsIfInvokeAsyncDoesNotHaveCorrectReturnType(object[] args)
{
// Arrange
var typeInfo = typeof(TypeWithInvalidInvokeAsync).GetTypeInfo();
// Act and Assert
var ex = Assert.Throws<InvalidOperationException>(
() => ViewComponentMethodSelector.FindAsyncMethod(typeInfo, args));
Assert.Equal("The async view component method 'InvokeAsync' should be declared to return Task<T>.",
ex.Message);
}
[Fact]
public void FindSyncMethod_ThrowsIfInvokeSyncIsAVoidMethod()
{
// Arrange
var expectedMessage = "The view component method 'Invoke' should be declared to return a value.";
var typeInfo = typeof(TypeWithInvalidInvokeSync).GetTypeInfo();
// Act and Assert
var ex = Assert.Throws<InvalidOperationException>(
() => ViewComponentMethodSelector.FindSyncMethod(typeInfo, new object[] { 4 }));
Assert.Equal(expectedMessage, ex.Message);
}
[Fact]
public void FindSyncMethod_ThrowsIfInvokeSyncReturnsTask()
{
// Arrange
var expectedMessage = "The view component method 'Invoke' cannot return a Task.";
var typeInfo = typeof(TypeWithInvalidInvokeSync).GetTypeInfo();
// Act and Assert
var ex = Assert.Throws<InvalidOperationException>(
() => ViewComponentMethodSelector.FindSyncMethod(typeInfo, new object[] { "" }));
Assert.Equal(expectedMessage, ex.Message);
}
public static TheoryData FindAsyncMethod_ReturnsMethodMatchingParametersData
{
get
{
var derivedClass = new DerivedClass();
return new TheoryData<Type, object[], string>
{
{ typeof(ViewComponentWithAsyncInvoke), new object[] { "", }, "1" },
{ typeof(ViewComponentWithAsyncInvoke), new object[] { "", 2 }, "2" },
{ typeof(ViewComponentWithAsyncInvoke), new object[] { "", 0, 1 }, "3" },
{ typeof(ViewComponentWithAsyncInvoke), new object[] { 1, false, 1 }, "4" },
{ typeof(MethodsWithValueConversions), new object[] { 2, (byte)1, (byte)2 }, "2" },
{ typeof(MethodsWithValueConversions), new object[] { derivedClass, derivedClass }, "4" },
{ typeof(MethodsWithValueConversions), new object[] { CultureInfo.InvariantCulture }, "6" },
};
}
}
[Theory]
[MemberData(nameof(FindAsyncMethod_ReturnsMethodMatchingParametersData))]
public void FindAsyncMethod_ReturnsMethodMatchingParameters(Type type, object[] args, string expectedId)
{
// Arrange
var typeInfo = type.GetTypeInfo();
// Act
var method = ViewComponentMethodSelector.FindAsyncMethod(typeInfo, args);
// Assert
Assert.NotNull(method);
var data = method.GetCustomAttribute<MethodDataAttribute>();
Assert.Equal(expectedId, data.Data);
}
public static TheoryData FindSyncMethod_ReturnsMethodMatchingParametersData
{
get
{
var derivedClass = new DerivedAgain();
return new TheoryData<Type, object[], string>
{
{ typeof(ViewComponentWithSyncInvoke), new object[] { }, "1" },
{ typeof(ViewComponentWithSyncInvoke), new object[] { 2, 3 }, "2" },
{ typeof(ViewComponentWithSyncInvoke), new object[] { "", 0, true }, "3" },
{ typeof(MethodsWithValueConversions), new object[] { 1, (byte)2, 3.0f }, "1" },
{ typeof(MethodsWithValueConversions), new object[] { derivedClass, derivedClass }, "3" },
{ typeof(MethodsWithValueConversions), new object[] { "Hello world" }, "5" },
{ typeof(DerivedClass), new object[] { }, "Derived1" },
#if !DNXCORE50
{ typeof(DerivedAgain), new object[] { "" }, "Derived2" },
#endif
};
}
}
[Theory]
[MemberData(nameof(FindSyncMethod_ReturnsMethodMatchingParametersData))]
public void FindSyncMethod_ReturnsMethodMatchingParameters(Type type, object[] args, string expectedId)
{
// Arrange
var typeInfo = type.GetTypeInfo();
// Act
var method = ViewComponentMethodSelector.FindSyncMethod(typeInfo, args);
// Assert
Assert.NotNull(method);
var data = method.GetCustomAttribute<MethodDataAttribute>();
Assert.Equal(expectedId, data.Data);
}
private class ViewComponentWithSyncInvoke
{
[MethodData("1")]
public int Invoke() => 3;
[MethodData("2")]
public int Invoke(int a, int? b) => a + b.Value;
[MethodData("3")]
public int Invoke(string a, int b, bool? c) => 3;
}
private class ViewComponentWithAsyncInvoke
{
[MethodData("1")]
public Task<string> InvokeAsync(string value) => Task.FromResult(value.ToUpperInvariant());
[MethodData("2")]
public Task<string> InvokeAsync(string a, int b) => Task.FromResult(a + b);
[MethodData("3")]
public Task<string> InvokeAsync(string a, int? b, int c) => Task.FromResult(a + b + c);
[MethodData("4")]
public Task<string> InvokeAsync(int? a, bool? b, int c) => Task.FromResult(a.ToString() + b + c);
[MethodData("5")]
public Task<string> InvokeAsync(object value) => Task.FromResult(value.ToString());
}
public class MethodsWithValueConversions
{
[MethodData("1")]
public int Invoke(long a, char b, double c) => 1;
[MethodData("2")]
public Task<int> InvokeAsync(float a, float b, byte c) => Task.FromResult(1);
[MethodData("3")]
public int Invoke(BaseClass a, DerivedClass b) => 1;
[MethodData("4")]
public Task<int> InvokeAsync(BaseClass a, DerivedClass b) => Task.FromResult(1);
[MethodData("5")]
public int Invoke(IEnumerable<char> value) => 1;
[MethodData("6")]
public Task<int> InvokeAsync(IFormatProvider formatProvider) => Task.FromResult(1);
}
private class ViewComponentWithNonPublicNonInstanceInvokes
{
public static int Invoke() => 1;
private int Invoke(string a) => 2;
public static Task<int> InvokeAsync() => Task.FromResult(3);
protected Task<string> InvokeAsync(string a) => Task.FromResult(a);
}
public class BaseClass
{
[MethodData("Base")]
public static int Invoke() => 1;
}
public class DerivedClass : BaseClass
{
[MethodData("Derived1")]
public new int Invoke() => 1;
[MethodData("Derived2")]
public int Invoke(string x) => 2;
}
public class DerivedAgain : DerivedClass
{
[MethodData("DerivedAgain")]
public new static int Invoke(string x) => 2;
}
private class TypeWithInvalidInvokeAsync
{
public Task InvokeAsync(string value) => Task.FromResult(value);
public void InvokeAsync(int value)
{
}
public long InvokeAsync(string a, int b) => b;
}
private class TypeWithInvalidInvokeSync
{
public Task Invoke(string value) => Task.FromResult(value);
public void Invoke(int value)
{
}
}
private class MethodDataAttribute : Attribute
{
public MethodDataAttribute(string data)
{
Data = data;
}
public string Data { get; }
}
}
}

View File

@ -2,9 +2,11 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions;
@ -147,9 +149,9 @@ namespace Microsoft.AspNet.Mvc
{
// Arrange
var expected = string.Join(Environment.NewLine,
"The view 'Components/Invoke/some-view' was not found. The following locations were searched:",
"location1",
"location2");
"The view 'Components/Invoke/some-view' was not found. The following locations were searched:",
"location1",
"location2");
var view = Mock.Of<IView>();
@ -185,9 +187,9 @@ namespace Microsoft.AspNet.Mvc
{
// Arrange
var expected = string.Join(Environment.NewLine,
"The view 'Components/Invoke/some-view' was not found. The following locations were searched:",
"location1",
"location2");
"The view 'Components/Invoke/some-view' was not found. The following locations were searched:",
"location1",
"location2");
var view = Mock.Of<IView>();
@ -223,11 +225,11 @@ namespace Microsoft.AspNet.Mvc
{
// Arrange
var expected = string.Join(Environment.NewLine,
"The view 'Components/Invoke/some-view' was not found. The following locations were searched:",
"location1",
"location2",
"location3",
"location4");
"The view 'Components/Invoke/some-view' was not found. The following locations were searched:",
"location1",
"location2",
"location3",
"location4");
var view = Mock.Of<IView>();
@ -535,11 +537,12 @@ namespace Microsoft.AspNet.Mvc
{
ShortName = "Invoke",
Type = typeof(object),
MethodInfo = typeof(object).GetTypeInfo().DeclaredMethods.First()
};
var viewComponentContext = new ViewComponentContext(
viewComponentDescriptor,
new object[0],
new Dictionary<string, object>(),
new HtmlTestEncoder(),
viewContext,
TextWriter.Null);

View File

@ -1 +1 @@
@Component.Invoke("JsonTextInView")
@await Component.InvokeAsync("JsonTextInView")

View File

@ -1 +1 @@
@Component.Invoke("RequestId")
@await Component.InvokeAsync("RequestId")

View File

@ -1,3 +1,3 @@
@model Person
<h1>@Model.Name</h1>
@Component.Invoke("InheritingViewComponent", Model.Address)
@await Component.InvokeAsync("InheritingViewComponent", new { address = Model.Address })

View File

@ -12,4 +12,4 @@
}
ViewWithRelativePath-content
<partial>@await Html.PartialAsync("../Shared/_PartialThatSetsTitle.cshtml")</partial>
@await Component.InvokeAsync("ComponentWithRelativePath", person)
@await Component.InvokeAsync("ComponentWithRelativePath", new { person })

View File

@ -2,6 +2,6 @@
ViewData["Title"] = "Page title";
// The invoked partial sets a title, but this shouldn't override the current page's ViewData \ ViewBag.
await Html.RenderPartialAsync("_PartialThatSetsTitle");
await Component.RenderInvokeAsync("ComponentThatSetsTitle");
@await Component.InvokeAsync("ComponentThatSetsTitle")
Layout = "/Views/Shared/_LayoutWithTitle.cshtml";
}

View File

@ -1,6 +1,6 @@
<Index>
@ViewContext.ExecutingFilePath
@ViewContext.View.Path
@Component.Invoke("ComponentForViewWithPaths")
@await Component.InvokeAsync("ComponentForViewWithPaths")
@Html.Partial("_Partial")
</Index>

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Encodings.Web;
@ -58,7 +59,7 @@ namespace MvcSample.Web.Components
await result.ExecuteAsync(new ViewComponentContext(
viewComponentDescriptor,
new object[0],
new Dictionary<string, object>(),
_htmlEncoder,
ViewContext,
writer));

View File

@ -35,7 +35,7 @@
<h3>Current Tag Cloud from Tag Helper</h3>
<tag-cloud count="Model.TagsToShow" surround="div" />
<h3>Current Tag Cloud from ViewComponentHelper:</h3>
<section bold>@await Component.InvokeAsync("Tags", 15)</section>
<section bold>@await Component.InvokeAsync("Tags", new { count = 15 })</section>
@{
RenderTemplate(
"Tag Cloud from Template: ",