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); 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>
<div class="sidebar"> <div class="sidebar">
<cache expires-after="TimeSpan.FromMinutes(20)"> <cache expires-after="TimeSpan.FromMinutes(20)">
@Component.Invoke("FeaturedMovies") @await Component.InvokeAsync("FeaturedMovies")
</cache> </cache>
</div> </div>
<div style="clear: left"></div> <div style="clear: left"></div>

View File

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

View File

@ -7,22 +7,27 @@ using Microsoft.AspNet.Html;
namespace Microsoft.AspNet.Mvc namespace Microsoft.AspNet.Mvc
{ {
/// <summary>
/// Supports the rendering of view components in a view.
/// </summary>
public interface IViewComponentHelper 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); /// <summary>
/// Invokes a view component of type <paramref name="componentType" />.
void RenderInvoke(string name, params object[] args); /// </summary>
/// <param name="componentType">The view component <see cref="Type"/>.</param>
void RenderInvoke(Type componentType, params object[] args); /// <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" />.
Task<IHtmlContent> InvokeAsync(string name, params object[] args); /// </returns>
Task<IHtmlContent> InvokeAsync(Type componentType, object arguments);
Task<IHtmlContent> InvokeAsync(Type componentType, params object[] args);
Task RenderInvokeAsync(string name, params object[] args);
Task RenderInvokeAsync(Type componentType, params object[] args);
} }
} }

View File

@ -10,15 +10,21 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures.Logging
{ {
public static class DefaultViewComponentInvokerLoggerExtensions 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; private static readonly Action<ILogger, string, double, string, Exception> _viewComponentExecuted;
static DefaultViewComponentInvokerLoggerExtensions() static DefaultViewComponentInvokerLoggerExtensions()
{ {
_viewComponentExecuting = LoggerMessage.Define<string>( _viewComponentExecuting = LoggerMessage.Define<string, string[]>(
LogLevel.Debug, LogLevel.Debug,
1, 1,
"Executing view component {ViewComponentName}"); "Executing view component {ViewComponentName} with arguments ({Arguments}).");
_viewComponentExecuted = LoggerMessage.Define<string, double, string>( _viewComponentExecuted = LoggerMessage.Define<string, double, string>(
LogLevel.Debug, LogLevel.Debug,
@ -32,9 +38,29 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures.Logging
return logger.BeginScopeImpl(new ViewComponentLogScope(context.ViewComponentDescriptor)); 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( public static void ViewComponentExecuted(

View File

@ -8,41 +8,29 @@ namespace Microsoft.AspNet.Mvc.Logging
{ {
public static class ViewComponentResultLoggerExtensions public static class ViewComponentResultLoggerExtensions
{ {
private static readonly Action<ILogger, string, string[], Exception> _viewComponentResultExecuting; private static readonly Action<ILogger, string, Exception> _viewComponentResultExecuting;
static ViewComponentResultLoggerExtensions() static ViewComponentResultLoggerExtensions()
{ {
_viewComponentResultExecuting = LoggerMessage.Define<string, string[]>( _viewComponentResultExecuting = LoggerMessage.Define<string>(
LogLevel.Information, LogLevel.Information,
1, 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)) if (logger.IsEnabled(LogLevel.Information))
{ {
var formattedArguments = new string[arguments.Length]; _viewComponentResultExecuting(logger, viewComponentName, null);
for (var i = 0; i < arguments.Length; i++)
{
formattedArguments[i] = Convert.ToString(arguments[i]);
}
_viewComponentResultExecuting(logger, viewComponentName, formattedArguments, 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)) if (logger.IsEnabled(LogLevel.Information))
{ {
var formattedArguments = new string[arguments.Length]; _viewComponentResultExecuting(logger, viewComponentType.Name, null);
for (var i = 0; i < arguments.Length; i++)
{
formattedArguments[i] = Convert.ToString(arguments[i]);
}
_viewComponentResultExecuting(logger, viewComponentType.Name, formattedArguments, null);
} }
} }
} }

View File

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

View File

@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
} }
/// <summary> /// <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> /// </summary>
internal static string ViewComponent_AsyncMethod_ShouldReturnTask internal static string ViewComponent_AsyncMethod_ShouldReturnTask
{ {
@ -35,11 +35,11 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
} }
/// <summary> /// <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> /// </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> /// <summary>
@ -59,7 +59,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
} }
/// <summary> /// <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> /// </summary>
internal static string ViewComponent_SyncMethod_ShouldReturnValue internal static string ViewComponent_SyncMethod_ShouldReturnValue
{ {
@ -67,11 +67,11 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
} }
/// <summary> /// <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> /// </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> /// <summary>
@ -107,7 +107,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
} }
/// <summary> /// <summary>
/// Could not find an '{0}' method matching the parameters. /// Could not find an '{0}' or '{1}' method for the view component '{2}'.
/// </summary> /// </summary>
internal static string ViewComponent_CannotFindMethod internal static string ViewComponent_CannotFindMethod
{ {
@ -115,27 +115,11 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
} }
/// <summary> /// <summary>
/// Could not find an '{0}' method matching the parameters. /// Could not find an '{0}' or '{1}' method for the view component '{2}'.
/// </summary> /// </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); return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_CannotFindMethod"), p0, p1, p2);
}
/// <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);
} }
/// <summary> /// <summary>
@ -859,7 +843,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
} }
/// <summary> /// <summary>
/// The view component method '{0}' cannot return a {1}. /// Method '{0}' of view component '{1}' cannot return a {2}.
/// </summary> /// </summary>
internal static string ViewComponent_SyncMethod_CannotReturnTask internal static string ViewComponent_SyncMethod_CannotReturnTask
{ {
@ -867,11 +851,27 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
} }
/// <summary> /// <summary>
/// The view component method '{0}' cannot return a {1}. /// Method '{0}' of view component '{1}' cannot return a {2}.
/// </summary> /// </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) private static string GetString(string name, params string[] formatterNames)

View File

@ -7,48 +7,76 @@ using Microsoft.AspNet.Html;
namespace Microsoft.AspNet.Mvc.Rendering namespace Microsoft.AspNet.Mvc.Rendering
{ {
/// <summary>
/// Extension methods for <see cref="IViewComponentHelper"/>.
/// </summary>
public static class ViewComponentHelperExtensions 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) if (helper == null)
{ {
throw new ArgumentNullException(nameof(helper)); 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) if (helper == null)
{ {
throw new ArgumentNullException(nameof(helper)); throw new ArgumentNullException(nameof(helper));
} }
helper.RenderInvoke(typeof(TComponent), args); return helper.InvokeAsync(componentType, arguments: null);
} }
public static Task<IHtmlContent> InvokeAsync<TComponent>( /// <summary>
this IViewComponentHelper helper, /// Invokes a view component of type <typeparam name="TComponent"/>.
params object[] args) /// </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) if (helper == null)
{ {
throw new ArgumentNullException(nameof(helper)); 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) if (helper == null)
{ {
throw new ArgumentNullException(nameof(helper)); throw new ArgumentNullException(nameof(helper));
} }
return helper.RenderInvokeAsync(typeof(TComponent), args); return helper.InvokeAsync(typeof(TComponent), arguments: null);
} }
} }
} }

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0 Version 2.0
The primary goals of this format is to allow a simple XML format The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes various data types are done through the TypeConverter classes
associated with the data types. associated with the data types.
Example: Example:
... ado.net/XML headers & schema ... ... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader> <resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment> <comment>This is a comment</comment>
</data> </data>
There are any number of "resheader" rows that contain simple There are any number of "resheader" rows that contain simple
name/value pairs. name/value pairs.
Each data row contains a name, and value. The row also contains a Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture. text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the Classes that don't support this are serialized and stored with the
mimetype set. mimetype set.
The mimetype is used for serialized objects, and tells the The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly: extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below. read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64 mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64 mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter : using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
@ -122,13 +122,13 @@
<comment>{1} is the newline character</comment> <comment>{1} is the newline character</comment>
</data> </data>
<data name="ViewComponent_AsyncMethod_ShouldReturnTask" xml:space="preserve"> <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>
<data name="ViewComponent_MustReturnValue" xml:space="preserve"> <data name="ViewComponent_MustReturnValue" xml:space="preserve">
<value>A view component must return a non-null value.</value> <value>A view component must return a non-null value.</value>
</data> </data>
<data name="ViewComponent_SyncMethod_ShouldReturnValue" xml:space="preserve"> <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>
<data name="ViewComponent_CannotFindComponent" xml:space="preserve"> <data name="ViewComponent_CannotFindComponent" xml:space="preserve">
<value>A view component named '{0}' could not be found.</value> <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> <value>An invoker could not be created for the view component '{0}'.</value>
</data> </data>
<data name="ViewComponent_CannotFindMethod" xml:space="preserve"> <data name="ViewComponent_CannotFindMethod" xml:space="preserve">
<value>Could not find an '{0}' method matching the parameters.</value> <value>Could not find an '{0}' or '{1}' method for the view component '{2}'.</value>
</data>
<data name="ViewComponent_CannotFindMethod_WithFallback" xml:space="preserve">
<value>Could not find an '{0}' or '{1}' method matching the parameters.</value>
</data> </data>
<data name="ViewComponent_InvalidReturnValue" xml:space="preserve"> <data name="ViewComponent_InvalidReturnValue" xml:space="preserve">
<value>View components only support returning {0}, {1} or {2}.</value> <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> <value>The collection already contains an entry with key '{0}'.</value>
</data> </data>
<data name="ViewComponent_SyncMethod_CannotReturnTask" xml:space="preserve"> <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> </data>
</root> </root>

View File

@ -3,7 +3,9 @@
using System; using System;
using System.Text; using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Html;
using Microsoft.AspNet.Mvc.Internal; using Microsoft.AspNet.Mvc.Internal;
using Microsoft.AspNet.Mvc.Logging; using Microsoft.AspNet.Mvc.Logging;
using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.ModelBinding;
@ -26,7 +28,7 @@ namespace Microsoft.AspNet.Mvc
/// <summary> /// <summary>
/// Gets or sets the arguments provided to the view component. /// Gets or sets the arguments provided to the view component.
/// </summary> /// </summary>
public object[] Arguments { get; set; } public object Arguments { get; set; }
/// <summary> /// <summary>
/// Gets or sets the <see cref="MediaTypeHeaderValue"/> representing the Content-Type header of the response. /// 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 loggerFactory = services.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger<ViewComponentResult>(); var logger = loggerFactory.CreateLogger<ViewComponentResult>();
var htmlEncoder = services.GetRequiredService<HtmlEncoder>();
var viewData = ViewData; var viewData = ViewData;
if (viewData == null) if (viewData == null)
@ -119,23 +122,29 @@ namespace Microsoft.AspNet.Mvc
htmlHelperOptions); htmlHelperOptions);
(viewComponentHelper as ICanHasViewContext)?.Contextualize(viewContext); (viewComponentHelper as ICanHasViewContext)?.Contextualize(viewContext);
var result = await GetViewComponentResult(viewComponentHelper, logger);
if (ViewComponentType == null && ViewComponentName == null) result.WriteTo(writer, htmlEncoder);
{ }
throw new InvalidOperationException(Resources.FormatViewComponentResult_NameOrTypeMustBeSet( }
nameof(ViewComponentName),
nameof(ViewComponentType))); private Task<IHtmlContent> GetViewComponentResult(IViewComponentHelper viewComponentHelper, ILogger logger)
} {
else if (ViewComponentType == null) if (ViewComponentType == null && ViewComponentName == null)
{ {
logger.ViewComponentResultExecuting(ViewComponentName, Arguments); throw new InvalidOperationException(Resources.FormatViewComponentResult_NameOrTypeMustBeSet(
await viewComponentHelper.RenderInvokeAsync(ViewComponentName, Arguments); nameof(ViewComponentName),
} nameof(ViewComponentType)));
else }
{ else if (ViewComponentType == null)
logger.ViewComponentResultExecuting(ViewComponentType, Arguments); {
await viewComponentHelper.RenderInvokeAsync(ViewComponentType, Arguments); 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.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Infrastructure; using Microsoft.AspNet.Mvc.Infrastructure;
using Microsoft.AspNet.Mvc.ViewFeatures;
namespace Microsoft.AspNet.Mvc.ViewComponents namespace Microsoft.AspNet.Mvc.ViewComponents
{ {
@ -14,6 +16,8 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
/// </summary> /// </summary>
public class DefaultViewComponentDescriptorProvider : IViewComponentDescriptorProvider public class DefaultViewComponentDescriptorProvider : IViewComponentDescriptorProvider
{ {
private const string AsyncMethodName = "InvokeAsync";
private const string SyncMethodName = "Invoke";
private readonly IAssemblyProvider _assemblyProvider; private readonly IAssemblyProvider _assemblyProvider;
/// <summary> /// <summary>
@ -32,7 +36,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
return types return types
.Where(IsViewComponentType) .Where(IsViewComponentType)
.Select(CreateCandidate); .Select(CreateDescriptor);
} }
/// <summary> /// <summary>
@ -47,11 +51,11 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
} }
/// <summary> /// <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> /// </summary>
/// <param name="typeInfo">The <see cref="TypeInfo"/>.</param> /// <param name="typeInfo">The <see cref="TypeInfo"/>.</param>
/// <returns> /// <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> /// </returns>
protected virtual bool IsViewComponentType(TypeInfo typeInfo) protected virtual bool IsViewComponentType(TypeInfo typeInfo)
{ {
@ -63,16 +67,70 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
return ViewComponentConventions.IsComponent(typeInfo); 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), FullName = ViewComponentConventions.GetComponentFullName(typeInfo),
ShortName = ViewComponentConventions.GetComponentName(typeInfo), ShortName = ViewComponentConventions.GetComponentName(typeInfo),
Type = typeInfo.AsType(), Type = type,
MethodInfo = FindMethod(type)
}; };
return candidate; 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.IO;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Html; using Microsoft.AspNet.Html;
@ -10,9 +9,13 @@ using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.ViewFeatures; using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Mvc.ViewFeatures.Buffer; using Microsoft.AspNet.Mvc.ViewFeatures.Buffer;
using Microsoft.AspNet.Mvc.ViewFeatures.Internal; using Microsoft.AspNet.Mvc.ViewFeatures.Internal;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNet.Mvc.ViewComponents namespace Microsoft.AspNet.Mvc.ViewComponents
{ {
/// <summary>
/// Default implementation for <see cref="IViewComponentHelper"/>.
/// </summary>
public class DefaultViewComponentHelper : IViewComponentHelper, ICanHasViewContext public class DefaultViewComponentHelper : IViewComponentHelper, ICanHasViewContext
{ {
private readonly IViewComponentDescriptorCollectionProvider _descriptorProvider; private readonly IViewComponentDescriptorCollectionProvider _descriptorProvider;
@ -22,6 +25,16 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
private readonly IViewBufferScope _viewBufferScope; private readonly IViewBufferScope _viewBufferScope;
private ViewContext _viewContext; 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( public DefaultViewComponentHelper(
IViewComponentDescriptorCollectionProvider descriptorProvider, IViewComponentDescriptorCollectionProvider descriptorProvider,
HtmlEncoder htmlEncoder, HtmlEncoder htmlEncoder,
@ -61,6 +74,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
_viewBufferScope = viewBufferScope; _viewBufferScope = viewBufferScope;
} }
/// <inheritdoc />
public void Contextualize(ViewContext viewContext) public void Contextualize(ViewContext viewContext)
{ {
if (viewContext == null) if (viewContext == null)
@ -71,133 +85,41 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
_viewContext = viewContext; _viewContext = viewContext;
} }
public IHtmlContent Invoke(string name, params object[] arguments) /// <inheritdoc />
public Task<IHtmlContent> InvokeAsync(string name, object arguments)
{ {
if (name == null) if (name == null)
{ {
throw new ArgumentNullException(nameof(name)); 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); var descriptor = _selector.SelectComponent(name);
if (descriptor == null) if (descriptor == null)
{ {
throw new InvalidOperationException(Resources.FormatViewComponent_CannotFindComponent(name)); 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) private ViewComponentDescriptor SelectComponent(Type componentType)
{ {
var descriptors = _descriptorProvider.ViewComponents; 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) if (descriptor.Type == componentType)
{ {
return descriptor; return descriptor;
@ -208,58 +130,30 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
componentType.FullName)); componentType.FullName));
} }
private Task InvokeCoreAsync( private async Task<IHtmlContent> InvokeCoreAsync(
TextWriter writer,
ViewComponentDescriptor descriptor, 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 namespace Microsoft.AspNet.Mvc.ViewComponents
{ {
/// <summary>
/// Default implementation for <see cref="IViewComponentInvoker"/>.
/// </summary>
public class DefaultViewComponentInvoker : IViewComponentInvoker public class DefaultViewComponentInvoker : IViewComponentInvoker
{ {
private readonly ITypeActivatorCache _typeActivatorCache; private readonly ITypeActivatorCache _typeActivatorCache;
@ -23,6 +26,13 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
private readonly DiagnosticSource _diagnosticSource; private readonly DiagnosticSource _diagnosticSource;
private readonly ILogger _logger; 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( public DefaultViewComponentInvoker(
ITypeActivatorCache typeActivatorCache, ITypeActivatorCache typeActivatorCache,
IViewComponentActivator viewComponentActivator, IViewComponentActivator viewComponentActivator,
@ -55,27 +65,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
_logger = logger; _logger = logger;
} }
public void Invoke(ViewComponentContext context) /// <inheritdoc />
{
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);
}
public async Task InvokeAsync(ViewComponentContext context) public async Task InvokeAsync(ViewComponentContext context)
{ {
if (context == null) if (context == null)
@ -83,32 +73,25 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
IViewComponentResult result; var methodInfo = context.ViewComponentDescriptor?.MethodInfo;
if (methodInfo == null)
var asyncMethod = ViewComponentMethodSelector.FindAsyncMethod(
context.ViewComponentDescriptor.Type.GetTypeInfo(),
context.Arguments);
if (asyncMethod == null)
{ {
// We support falling back to synchronous if there is no InvokeAsync method, in this case we'll still throw new InvalidOperationException(Resources.FormatPropertyOfTypeCannotBeNull(
// execute the IViewResult asynchronously. nameof(ViewComponentDescriptor.MethodInfo),
var syncMethod = ViewComponentMethodSelector.FindSyncMethod( nameof(ViewComponentDescriptor)));
context.ViewComponentDescriptor.Type.GetTypeInfo(), }
context.Arguments);
if (syncMethod == null) var isAsync = typeof(Task).GetTypeInfo().IsAssignableFrom(methodInfo.ReturnType.GetTypeInfo());
{ IViewComponentResult result;
throw new InvalidOperationException( if (isAsync)
Resources.FormatViewComponent_CannotFindMethod_WithFallback( {
ViewComponentMethodSelector.SyncMethodName, ViewComponentMethodSelector.AsyncMethodName)); result = await InvokeAsyncCore(context);
}
else
{
result = InvokeSyncCore(syncMethod, context);
}
} }
else 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); await result.ExecuteAsync(context);
@ -129,29 +112,20 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
return component; return component;
} }
private async Task<IViewComponentResult> InvokeAsyncCore( private async Task<IViewComponentResult> InvokeAsyncCore(ViewComponentContext context)
MethodInfo method,
ViewComponentContext context)
{ {
if (method == null)
{
throw new ArgumentNullException(nameof(method));
}
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var component = CreateComponent(context); var component = CreateComponent(context);
using (_logger.ViewComponentScope(context)) using (_logger.ViewComponentScope(context))
{ {
var method = context.ViewComponentDescriptor.MethodInfo;
var arguments = ControllerActionExecutor.PrepareArguments(context.Arguments, method.GetParameters());
_diagnosticSource.BeforeViewComponent(context, component); _diagnosticSource.BeforeViewComponent(context, component);
_logger.ViewComponentExecuting(context); _logger.ViewComponentExecuting(context, arguments);
var startTime = Environment.TickCount; 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); var viewComponentResult = CoerceToViewComponentResult(result);
_logger.ViewComponentExecuted(context, startTime, viewComponentResult); _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); var component = CreateComponent(context);
using (_logger.ViewComponentScope(context)) using (_logger.ViewComponentScope(context))
{ {
_diagnosticSource.BeforeViewComponent(context, component); var method = context.ViewComponentDescriptor.MethodInfo;
_logger.ViewComponentExecuting(context); var arguments = ControllerActionExecutor.PrepareArguments(
context.Arguments,
method.GetParameters());
_diagnosticSource.BeforeViewComponent(context, component);
_logger.ViewComponentExecuting(context, arguments);
var startTime = Environment.TickCount;
object result;
try try
{ {
var startTime = Environment.TickCount; result = method.Invoke(component, arguments);
var result = method.Invoke(component, context.Arguments);
var viewComponentResult = CoerceToViewComponentResult(result);
_logger.ViewComponentExecuted(context, startTime, viewComponentResult);
_diagnosticSource.AfterViewComponent(context, viewComponentResult, component);
return viewComponentResult;
} }
catch (TargetInvocationException ex) catch (TargetInvocationException ex)
{ {
@ -198,6 +162,12 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
exceptionInfo.Throw(); exceptionInfo.Throw();
return null; // Unreachable 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 namespace Microsoft.AspNet.Mvc.ViewComponents
{ {
/// <summary> /// <summary>
/// Discovers the View Components in the application. /// Discovers the view components in the application.
/// </summary> /// </summary>
public interface IViewComponentDescriptorProvider public interface IViewComponentDescriptorProvider
{ {

View File

@ -5,10 +5,17 @@ using System.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.ViewComponents namespace Microsoft.AspNet.Mvc.ViewComponents
{ {
/// <summary>
/// Specifies the contract for execution of a view component.
/// </summary>
public interface IViewComponentInvoker 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); Task InvokeAsync(ViewComponentContext context);
} }
} }

View File

@ -4,14 +4,14 @@
namespace Microsoft.AspNet.Mvc.ViewComponents namespace Microsoft.AspNet.Mvc.ViewComponents
{ {
/// <summary> /// <summary>
/// Selects a View Component based on a View Component name. /// Selects a view component based on a view component name.
/// </summary> /// </summary>
public interface IViewComponentSelector public interface IViewComponentSelector
{ {
/// <summary> /// <summary>
/// Selects a View Component based on <paramref name="componentName"/>. /// Selects a view component based on <paramref name="componentName"/>.
/// </summary> /// </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> /// <returns>A <see cref="ViewComponentDescriptor"/>, or <c>null</c> if no match is found.</returns>
ViewComponentDescriptor SelectComponent(string componentName); 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. // 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.Generic;
using System.IO; using System.IO;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Mvc.Rendering;
@ -10,7 +11,7 @@ using Microsoft.AspNet.Mvc.ViewFeatures;
namespace Microsoft.AspNet.Mvc.ViewComponents namespace Microsoft.AspNet.Mvc.ViewComponents
{ {
/// <summary> /// <summary>
/// A context for View Components. /// A context for view components.
/// </summary> /// </summary>
public class ViewComponentContext public class ViewComponentContext
{ {
@ -23,7 +24,6 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
public ViewComponentContext() public ViewComponentContext()
{ {
ViewComponentDescriptor = new ViewComponentDescriptor(); ViewComponentDescriptor = new ViewComponentDescriptor();
Arguments = new object[0];
ViewContext = new ViewContext(); ViewContext = new ViewContext();
} }
@ -31,14 +31,14 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
/// Creates a new <see cref="ViewComponentContext"/>. /// Creates a new <see cref="ViewComponentContext"/>.
/// </summary> /// </summary>
/// <param name="viewComponentDescriptor"> /// <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>
/// <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="viewContext">The <see cref="ViewContext"/>.</param>
/// <param name="writer">The <see cref="TextWriter"/> for writing output.</param> /// <param name="writer">The <see cref="TextWriter"/> for writing output.</param>
public ViewComponentContext( public ViewComponentContext(
ViewComponentDescriptor viewComponentDescriptor, ViewComponentDescriptor viewComponentDescriptor,
object[] arguments, IDictionary<string, object> arguments,
HtmlEncoder htmlEncoder, HtmlEncoder htmlEncoder,
ViewContext viewContext, ViewContext viewContext,
TextWriter writer) TextWriter writer)
@ -82,15 +82,15 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
} }
/// <summary> /// <summary>
/// Gets or sets the View Component arguments. /// Gets or sets the view component arguments.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The property setter is provided for unit test purposes only. /// The property setter is provided for unit test purposes only.
/// </remarks> /// </remarks>
public object[] Arguments { get; set; } public IDictionary<string, object> Arguments { get; set; }
/// <summary> /// <summary>
/// Gets or sets the <see cref="HtmlEncoder"/>. /// Gets or sets the <see cref="System.Text.Encodings.Web.HtmlEncoder"/>.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The property setter is provided for unit test purposes only. /// The property setter is provided for unit test purposes only.
@ -98,7 +98,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
public HtmlEncoder HtmlEncoder { get; set; } public HtmlEncoder HtmlEncoder { get; set; }
/// <summary> /// <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> /// </summary>
/// <remarks> /// <remarks>
/// The property setter is provided for unit test purposes only. /// The property setter is provided for unit test purposes only.
@ -106,7 +106,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
public ViewComponentDescriptor ViewComponentDescriptor { get; set; } public ViewComponentDescriptor ViewComponentDescriptor { get; set; }
/// <summary> /// <summary>
/// Gets or sets the <see cref="ViewContext"/>. /// Gets or sets the <see cref="Rendering.ViewContext"/>.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The property setter is provided for unit test purposes only. /// The property setter is provided for unit test purposes only.

View File

@ -3,11 +3,12 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Reflection;
namespace Microsoft.AspNet.Mvc.ViewComponents namespace Microsoft.AspNet.Mvc.ViewComponents
{ {
/// <summary> /// <summary>
/// A descriptor for a View Component. /// A descriptor for a view component.
/// </summary> /// </summary>
[DebuggerDisplay("{DisplayName}")] [DebuggerDisplay("{DisplayName}")]
public class ViewComponentDescriptor public class ViewComponentDescriptor
@ -23,7 +24,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
} }
/// <summary> /// <summary>
/// Gets or sets the display name of the View Component. /// Gets or sets the display name of the view component.
/// </summary> /// </summary>
public string DisplayName public string DisplayName
{ {
@ -53,8 +54,8 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// <para> /// <para>
/// The full name is defaulted to the full namespace of the View Component class, prepended to /// 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 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"/>. /// <code>ViewComponent</code> as a suffix, the suffix will be omitted from the <see cref="FullName"/>.
/// </para> /// </para>
/// <example> /// <example>
@ -89,7 +90,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// <para> /// <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"/>. /// <code>ViewComponent</code> as a suffix, the suffix will be omitted from the <see cref="ShortName"/>.
/// </para> /// </para>
/// <example> /// <example>
@ -115,8 +116,13 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
public string ShortName { get; set; } public string ShortName { get; set; }
/// <summary> /// <summary>
/// Gets or sets the <see cref="Type"/>. /// Gets or sets the <see cref="System.Type"/>.
/// </summary> /// </summary>
public Type Type { get; set; } 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.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Reflection;
using System.Text; using System.Text;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -42,13 +43,14 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.Text", FullName = "Full.Name.Text",
ShortName = "Text", ShortName = "Text",
Type = typeof(TextViewComponent), Type = typeof(TextViewComponent),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)),
}; };
var actionContext = CreateActionContext(descriptor); var actionContext = CreateActionContext(descriptor);
var viewComponentResult = new ViewComponentResult var viewComponentResult = new ViewComponentResult
{ {
Arguments = new object[] { "World!" }, Arguments = new { name = "World!" },
ViewData = null, ViewData = null,
TempData = null, TempData = null,
ViewComponentName = "Text" ViewComponentName = "Text"
@ -132,13 +134,43 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.Text", FullName = "Full.Name.Text",
ShortName = "Text", ShortName = "Text",
Type = typeof(TextViewComponent), Type = typeof(TextViewComponent),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)),
}; };
var actionContext = CreateActionContext(descriptor); var actionContext = CreateActionContext(descriptor);
var viewComponentResult = new ViewComponentResult() 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", ViewComponentName = "Text",
TempData = _tempDataDictionary, TempData = _tempDataDictionary,
}; };
@ -160,13 +192,14 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.AsyncText", FullName = "Full.Name.AsyncText",
ShortName = "AsyncText", ShortName = "AsyncText",
Type = typeof(AsyncTextViewComponent), Type = typeof(AsyncTextViewComponent),
MethodInfo = typeof(AsyncTextViewComponent).GetMethod(nameof(AsyncTextViewComponent.InvokeAsync)),
}; };
var actionContext = CreateActionContext(descriptor); var actionContext = CreateActionContext(descriptor);
var viewComponentResult = new ViewComponentResult() var viewComponentResult = new ViewComponentResult()
{ {
Arguments = new object[] { "World!" }, Arguments = new { name = "World!" },
ViewComponentName = "AsyncText", ViewComponentName = "AsyncText",
TempData = _tempDataDictionary, TempData = _tempDataDictionary,
}; };
@ -188,6 +221,7 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.Text", FullName = "Full.Name.Text",
ShortName = "Text", ShortName = "Text",
Type = typeof(TextViewComponent), Type = typeof(TextViewComponent),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)),
}; };
var adapter = new TestDiagnosticListener(); var adapter = new TestDiagnosticListener();
@ -196,7 +230,7 @@ namespace Microsoft.AspNet.Mvc
var viewComponentResult = new ViewComponentResult() var viewComponentResult = new ViewComponentResult()
{ {
Arguments = new object[] { "World!" }, Arguments = new { name = "World!" },
ViewComponentName = "Text", ViewComponentName = "Text",
TempData = _tempDataDictionary, TempData = _tempDataDictionary,
}; };
@ -226,13 +260,14 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.Text", FullName = "Full.Name.Text",
ShortName = "Text", ShortName = "Text",
Type = typeof(TextViewComponent), Type = typeof(TextViewComponent),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)),
}; };
var actionContext = CreateActionContext(descriptor); var actionContext = CreateActionContext(descriptor);
var viewComponentResult = new ViewComponentResult() var viewComponentResult = new ViewComponentResult()
{ {
Arguments = new object[] { "World!" }, Arguments = new { name = "World!" },
ViewComponentName = "Text", ViewComponentName = "Text",
TempData = _tempDataDictionary, TempData = _tempDataDictionary,
}; };
@ -254,13 +289,14 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.Text", FullName = "Full.Name.Text",
ShortName = "Text", ShortName = "Text",
Type = typeof(TextViewComponent), Type = typeof(TextViewComponent),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)),
}; };
var actionContext = CreateActionContext(descriptor); var actionContext = CreateActionContext(descriptor);
var viewComponentResult = new ViewComponentResult() var viewComponentResult = new ViewComponentResult()
{ {
Arguments = new object[] { "World!" }, Arguments = new { name = "World!" },
ViewComponentName = "Full.Name.Text", ViewComponentName = "Full.Name.Text",
TempData = _tempDataDictionary, TempData = _tempDataDictionary,
}; };
@ -282,13 +318,14 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.Text", FullName = "Full.Name.Text",
ShortName = "Text", ShortName = "Text",
Type = typeof(TextViewComponent), Type = typeof(TextViewComponent),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)),
}; };
var actionContext = CreateActionContext(descriptor); var actionContext = CreateActionContext(descriptor);
var viewComponentResult = new ViewComponentResult() var viewComponentResult = new ViewComponentResult()
{ {
Arguments = new object[] { "World!" }, Arguments = new { name = "World!" },
ViewComponentType = typeof(TextViewComponent), ViewComponentType = typeof(TextViewComponent),
TempData = _tempDataDictionary, TempData = _tempDataDictionary,
}; };
@ -310,13 +347,14 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.Text", FullName = "Full.Name.Text",
ShortName = "Text", ShortName = "Text",
Type = typeof(TextViewComponent), Type = typeof(TextViewComponent),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke))
}; };
var actionContext = CreateActionContext(descriptor); var actionContext = CreateActionContext(descriptor);
var viewComponentResult = new ViewComponentResult() var viewComponentResult = new ViewComponentResult()
{ {
Arguments = new object[] { "World!" }, Arguments = new { name = "World!" },
ViewComponentType = typeof(TextViewComponent), ViewComponentType = typeof(TextViewComponent),
StatusCode = 404, StatusCode = 404,
TempData = _tempDataDictionary, TempData = _tempDataDictionary,
@ -367,6 +405,7 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.Text", FullName = "Full.Name.Text",
ShortName = "Text", ShortName = "Text",
Type = typeof(TextViewComponent), Type = typeof(TextViewComponent),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)),
}; };
var actionContext = CreateActionContext(descriptor); var actionContext = CreateActionContext(descriptor);
@ -375,7 +414,7 @@ namespace Microsoft.AspNet.Mvc
var viewComponentResult = new ViewComponentResult() var viewComponentResult = new ViewComponentResult()
{ {
Arguments = new object[] { "World!" }, Arguments = new { name = "World!" },
ViewComponentName = "Text", ViewComponentName = "Text",
ContentType = contentType, ContentType = contentType,
TempData = _tempDataDictionary, TempData = _tempDataDictionary,
@ -403,6 +442,7 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.Text", FullName = "Full.Name.Text",
ShortName = "Text", ShortName = "Text",
Type = typeof(TextViewComponent), Type = typeof(TextViewComponent),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)),
}; };
var actionContext = CreateActionContext(descriptor); var actionContext = CreateActionContext(descriptor);
@ -412,7 +452,7 @@ namespace Microsoft.AspNet.Mvc
var viewComponentResult = new ViewComponentResult() var viewComponentResult = new ViewComponentResult()
{ {
Arguments = new object[] { "World!" }, Arguments = new { name = "World!" },
ViewComponentName = "Text", ViewComponentName = "Text",
ContentType = new MediaTypeHeaderValue("text/html") { Encoding = Encoding.UTF8 }, ContentType = new MediaTypeHeaderValue("text/html") { Encoding = Encoding.UTF8 },
TempData = _tempDataDictionary, TempData = _tempDataDictionary,
@ -439,6 +479,7 @@ namespace Microsoft.AspNet.Mvc
FullName = "Full.Name.Text", FullName = "Full.Name.Text",
ShortName = "Text", ShortName = "Text",
Type = typeof(TextViewComponent), Type = typeof(TextViewComponent),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)),
}; };
var actionContext = CreateActionContext(descriptor); var actionContext = CreateActionContext(descriptor);
@ -447,7 +488,7 @@ namespace Microsoft.AspNet.Mvc
var viewComponentResult = new ViewComponentResult() var viewComponentResult = new ViewComponentResult()
{ {
Arguments = new object[] { "World!" }, Arguments = new { name = "World!" },
ViewComponentName = "Text", ViewComponentName = "Text",
TempData = _tempDataDictionary, TempData = _tempDataDictionary,
}; };
@ -459,7 +500,10 @@ namespace Microsoft.AspNet.Mvc
Assert.Equal(expectedContentType, actionContext.HttpContext.Response.ContentType); 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 httpContext = new DefaultHttpContext();
var diagnosticSource = new DiagnosticListener("Microsoft.AspNet"); var diagnosticSource = new DiagnosticListener("Microsoft.AspNet");
@ -535,12 +579,6 @@ namespace Microsoft.AspNet.Mvc
private class AsyncTextViewComponent : ViewComponent private class AsyncTextViewComponent : ViewComponent
{ {
public HtmlString Invoke()
{
// Should never run.
throw null;
}
public Task<HtmlString> InvokeAsync(string name) public Task<HtmlString> InvokeAsync(string name)
{ {
return Task.FromResult(new HtmlString("Hello-Async, " + name)); return Task.FromResult(new HtmlString("Hello-Async, " + name));

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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 System.IO;
using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions; using Microsoft.AspNet.Mvc.Abstractions;
@ -43,7 +44,7 @@ namespace Microsoft.AspNet.Mvc
var viewContext = new ViewContext( var viewContext = new ViewContext(
actionContext, actionContext,
view, view,
viewData, viewData,
new TempDataDictionary(httpContext, new SessionStateTempDataProvider()), new TempDataDictionary(httpContext, new SessionStateTempDataProvider()),
TextWriter.Null, TextWriter.Null,
new HtmlHelperOptions()); new HtmlHelperOptions());
@ -57,7 +58,7 @@ namespace Microsoft.AspNet.Mvc
var viewComponentContext = new ViewComponentContext( var viewComponentContext = new ViewComponentContext(
viewComponentDescriptor, viewComponentDescriptor,
new object[0], new Dictionary<string, object>(),
new HtmlTestEncoder(), new HtmlTestEncoder(),
viewContext, viewContext,
writer); 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.IO;
using System.Threading.Tasks; 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; using Xunit;
namespace Microsoft.AspNet.Mvc.ViewComponents namespace Microsoft.AspNet.Mvc.ViewComponents

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Infrastructure; using Microsoft.AspNet.Mvc.Infrastructure;
using Xunit; using Xunit;
@ -23,9 +24,10 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
// Assert // Assert
var descriptor = Assert.Single(descriptors); 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("Microsoft.AspNet.Mvc.ViewComponents.Conventions", descriptor.FullName);
Assert.Equal("Conventions", descriptor.ShortName); Assert.Equal("Conventions", descriptor.ShortName);
Assert.Same(typeof(ConventionsViewComponent).GetMethod("Invoke"), descriptor.MethodInfo);
} }
[Fact] [Fact]
@ -42,15 +44,156 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
Assert.Equal(typeof(AttributeViewComponent), descriptor.Type); Assert.Equal(typeof(AttributeViewComponent), descriptor.Type);
Assert.Equal("AttributesAreGreat", descriptor.FullName); Assert.Equal("AttributesAreGreat", descriptor.FullName);
Assert.Equal("AttributesAreGreat", descriptor.ShortName); 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 private class ConventionsViewComponent
{ {
public string Invoke() => "Hello world";
} }
[ViewComponent(Name = "AttributesAreGreat")] [ViewComponent(Name = "AttributesAreGreat")]
private class AttributeViewComponent 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) private DefaultViewComponentDescriptorProvider CreateProvider(Type componentType)
@ -80,11 +223,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
return return
GetAssemblyProvider() GetAssemblyProvider()
.CandidateAssemblies .CandidateAssemblies
.SelectMany(a => a.DefinedTypes) .SelectMany(a => a.DefinedTypes);
#if DNX451
.Select(t => t.GetTypeInfo())
#endif
;
} }
private static IAssemblyProvider GetAssemblyProvider() private static IAssemblyProvider GetAssemblyProvider()

View File

@ -12,6 +12,8 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
{ {
public class DefaultViewComponentSelectorTest public class DefaultViewComponentSelectorTest
{ {
private static readonly string Namespace = typeof(DefaultViewComponentSelectorTest).Namespace;
[Fact] [Fact]
public void SelectComponent_ByShortNameWithSuffix() public void SelectComponent_ByShortNameWithSuffix()
{ {
@ -22,7 +24,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
var result = selector.SelectComponent("Suffix"); var result = selector.SelectComponent("Suffix");
// Assert // Assert
Assert.Equal(typeof(SuffixViewComponent), result.Type); Assert.Same(typeof(ViewComponentContainer.SuffixViewComponent), result.Type);
} }
[Fact] [Fact]
@ -32,10 +34,10 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
var selector = CreateSelector(); var selector = CreateSelector();
// Act // Act
var result = selector.SelectComponent("Microsoft.AspNet.Mvc.ViewComponents.Suffix"); var result = selector.SelectComponent($"{Namespace}.Suffix");
// Assert // Assert
Assert.Equal(typeof(SuffixViewComponent), result.Type); Assert.Same(typeof(ViewComponentContainer.SuffixViewComponent), result.Type);
} }
[Fact] [Fact]
@ -48,7 +50,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
var result = selector.SelectComponent("WithoutSuffix"); var result = selector.SelectComponent("WithoutSuffix");
// Assert // Assert
Assert.Equal(typeof(WithoutSuffix), result.Type); Assert.Same(typeof(ViewComponentContainer.WithoutSuffix), result.Type);
} }
[Fact] [Fact]
@ -58,10 +60,10 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
var selector = CreateSelector(); var selector = CreateSelector();
// Act // Act
var result = selector.SelectComponent("Microsoft.AspNet.Mvc.ViewComponents.WithoutSuffix"); var result = selector.SelectComponent($"{Namespace}.WithoutSuffix");
// Assert // Assert
Assert.Equal(typeof(WithoutSuffix), result.Type); Assert.Same(typeof(ViewComponentContainer.WithoutSuffix), result.Type);
} }
[Fact] [Fact]
@ -74,7 +76,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
var result = selector.SelectComponent("ByAttribute"); var result = selector.SelectComponent("ByAttribute");
// Assert // Assert
Assert.Equal(typeof(ByAttribute), result.Type); Assert.Same(typeof(ViewComponentContainer.ByAttribute), result.Type);
} }
[Fact] [Fact]
@ -87,7 +89,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
var result = selector.SelectComponent("ByNamingConvention"); var result = selector.SelectComponent("ByNamingConvention");
// Assert // Assert
Assert.Equal(typeof(ByNamingConventionViewComponent), result.Type); Assert.Same(typeof(ViewComponentContainer.ByNamingConventionViewComponent), result.Type);
} }
[Fact] [Fact]
@ -98,9 +100,9 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
var expected = var expected =
"The view component name 'Ambiguous' matched multiple types:" + Environment.NewLine + "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 + "Name: 'Namespace1.Ambiguous'" + Environment.NewLine +
"Type: 'Microsoft.AspNet.Mvc.ViewComponents.DefaultViewComponentSelectorTest+Ambiguous2' - " + $"Type: '{typeof(ViewComponentContainer.Ambiguous2)}' - " +
"Name: 'Namespace2.Ambiguous'"; "Name: 'Namespace2.Ambiguous'";
// Act // Act
@ -120,7 +122,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
var result = selector.SelectComponent("Namespace1.Ambiguous"); var result = selector.SelectComponent("Namespace1.Ambiguous");
// Assert // Assert
Assert.Equal(typeof(Ambiguous1), result.Type); Assert.Same(typeof(ViewComponentContainer.Ambiguous1), result.Type);
} }
[Theory] [Theory]
@ -135,44 +137,56 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
var result = selector.SelectComponent(name); var result = selector.SelectComponent(name);
// Assert // Assert
Assert.Equal(typeof(FullNameInAttribute), result.Type); Assert.Same(typeof(ViewComponentContainer.FullNameInAttribute), result.Type);
} }
private IViewComponentSelector CreateSelector() 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] [ViewComponent]
private class ByAttribute public class ByAttribute
{ {
} public string Invoke() => "Hello";
}
[ViewComponent(Name = "Namespace1.Ambiguous")] [ViewComponent(Name = "Namespace1.Ambiguous")]
private class Ambiguous1 public class Ambiguous1
{ {
} public string Invoke() => "Hello";
}
[ViewComponent(Name = "Namespace2.Ambiguous")] [ViewComponent(Name = "Namespace2.Ambiguous")]
private class Ambiguous2 public class Ambiguous2
{ {
} public string Invoke() => "Hello";
}
[ViewComponent(Name = "CoolNameSpace.FullNameInAttribute")] [ViewComponent(Name = "CoolNameSpace.FullNameInAttribute")]
private class FullNameInAttribute public class FullNameInAttribute
{ {
public string Invoke() => "Hello";
}
} }
// This will only consider types nested inside this class as ViewComponent classes // This will only consider types nested inside this class as ViewComponent classes
@ -181,10 +195,10 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
public FilteredViewComponentDescriptorProvider() public FilteredViewComponentDescriptorProvider()
: base(GetAssemblyProvider()) : 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) protected override bool IsViewComponentType(TypeInfo typeInfo)
{ {
@ -208,19 +222,10 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
{ {
var assemblyProvider = new StaticAssemblyProvider(); var assemblyProvider = new StaticAssemblyProvider();
assemblyProvider.CandidateAssemblies.Add( assemblyProvider.CandidateAssemblies.Add(
typeof(FilteredViewComponentSelector).GetTypeInfo().Assembly); typeof(ViewComponentContainer).GetTypeInfo().Assembly);
return assemblyProvider; 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. // 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. // 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 System.IO;
using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions; using Microsoft.AspNet.Mvc.Abstractions;
@ -57,7 +57,7 @@ namespace Microsoft.AspNet.Mvc
var viewComponentContext = new ViewComponentContext( var viewComponentContext = new ViewComponentContext(
viewComponentDescriptor, viewComponentDescriptor,
new object[0], new Dictionary<string, object>(),
new HtmlTestEncoder(), new HtmlTestEncoder(),
viewContext, viewContext,
writer); writer);
@ -65,5 +65,4 @@ namespace Microsoft.AspNet.Mvc
return viewComponentContext; return viewComponentContext;
} }
} }
} }
#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. // 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.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
@ -85,7 +86,7 @@ namespace Microsoft.AspNet.Mvc
var viewComponentContext = new ViewComponentContext( var viewComponentContext = new ViewComponentContext(
viewComponentDescriptor, viewComponentDescriptor,
new object[0], new Dictionary<string, object>(),
new HtmlTestEncoder(), new HtmlTestEncoder(),
viewContext, viewContext,
writer); 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. // 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.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions; using Microsoft.AspNet.Mvc.Abstractions;
@ -147,9 +149,9 @@ namespace Microsoft.AspNet.Mvc
{ {
// Arrange // Arrange
var expected = string.Join(Environment.NewLine, var expected = string.Join(Environment.NewLine,
"The view 'Components/Invoke/some-view' was not found. The following locations were searched:", "The view 'Components/Invoke/some-view' was not found. The following locations were searched:",
"location1", "location1",
"location2"); "location2");
var view = Mock.Of<IView>(); var view = Mock.Of<IView>();
@ -185,9 +187,9 @@ namespace Microsoft.AspNet.Mvc
{ {
// Arrange // Arrange
var expected = string.Join(Environment.NewLine, var expected = string.Join(Environment.NewLine,
"The view 'Components/Invoke/some-view' was not found. The following locations were searched:", "The view 'Components/Invoke/some-view' was not found. The following locations were searched:",
"location1", "location1",
"location2"); "location2");
var view = Mock.Of<IView>(); var view = Mock.Of<IView>();
@ -223,11 +225,11 @@ namespace Microsoft.AspNet.Mvc
{ {
// Arrange // Arrange
var expected = string.Join(Environment.NewLine, var expected = string.Join(Environment.NewLine,
"The view 'Components/Invoke/some-view' was not found. The following locations were searched:", "The view 'Components/Invoke/some-view' was not found. The following locations were searched:",
"location1", "location1",
"location2", "location2",
"location3", "location3",
"location4"); "location4");
var view = Mock.Of<IView>(); var view = Mock.Of<IView>();
@ -535,11 +537,12 @@ namespace Microsoft.AspNet.Mvc
{ {
ShortName = "Invoke", ShortName = "Invoke",
Type = typeof(object), Type = typeof(object),
MethodInfo = typeof(object).GetTypeInfo().DeclaredMethods.First()
}; };
var viewComponentContext = new ViewComponentContext( var viewComponentContext = new ViewComponentContext(
viewComponentDescriptor, viewComponentDescriptor,
new object[0], new Dictionary<string, object>(),
new HtmlTestEncoder(), new HtmlTestEncoder(),
viewContext, viewContext,
TextWriter.Null); 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 @model Person
<h1>@Model.Name</h1> <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 ViewWithRelativePath-content
<partial>@await Html.PartialAsync("../Shared/_PartialThatSetsTitle.cshtml")</partial> <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"; ViewData["Title"] = "Page title";
// The invoked partial sets a title, but this shouldn't override the current page's ViewData \ ViewBag. // The invoked partial sets a title, but this shouldn't override the current page's ViewData \ ViewBag.
await Html.RenderPartialAsync("_PartialThatSetsTitle"); await Html.RenderPartialAsync("_PartialThatSetsTitle");
await Component.RenderInvokeAsync("ComponentThatSetsTitle"); @await Component.InvokeAsync("ComponentThatSetsTitle")
Layout = "/Views/Shared/_LayoutWithTitle.cshtml"; Layout = "/Views/Shared/_LayoutWithTitle.cshtml";
} }

View File

@ -1,6 +1,6 @@
<Index> <Index>
@ViewContext.ExecutingFilePath @ViewContext.ExecutingFilePath
@ViewContext.View.Path @ViewContext.View.Path
@Component.Invoke("ComponentForViewWithPaths") @await Component.InvokeAsync("ComponentForViewWithPaths")
@Html.Partial("_Partial") @Html.Partial("_Partial")
</Index> </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. // 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.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
@ -58,7 +59,7 @@ namespace MvcSample.Web.Components
await result.ExecuteAsync(new ViewComponentContext( await result.ExecuteAsync(new ViewComponentContext(
viewComponentDescriptor, viewComponentDescriptor,
new object[0], new Dictionary<string, object>(),
_htmlEncoder, _htmlEncoder,
ViewContext, ViewContext,
writer)); writer));

View File

@ -35,7 +35,7 @@
<h3>Current Tag Cloud from Tag Helper</h3> <h3>Current Tag Cloud from Tag Helper</h3>
<tag-cloud count="Model.TagsToShow" surround="div" /> <tag-cloud count="Model.TagsToShow" surround="div" />
<h3>Current Tag Cloud from ViewComponentHelper:</h3> <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( RenderTemplate(
"Tag Cloud from Template: ", "Tag Cloud from Template: ",