Modify IViewComponentHelper to remove method selection ambiguity.

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

View File

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

View File

@ -0,0 +1,36 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Primitives;
using TagHelperSample.Web.Services;
namespace TagHelperSample.Web.Components
{
[ViewComponent(Name = "Movies")]
public class MoviesComponent : ViewComponent
{
private readonly IMemoryCache _cache;
private readonly MoviesService _moviesService;
public MoviesComponent(MoviesService moviesService, IMemoryCache cache)
{
_moviesService = moviesService;
_cache = cache;
}
public IViewComponentResult Invoke(string movieName)
{
string quote;
if (!_cache.TryGetValue(movieName, out quote))
{
IChangeToken expirationToken;
quote = _moviesService.GetCriticsQuote(out expirationToken);
_cache.Set(movieName, quote, new MemoryCacheEntryOptions().AddExpirationToken(expirationToken));
}
return Content(quote);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#if MOCK_SUPPORT
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions;
@ -57,7 +57,7 @@ namespace Microsoft.AspNet.Mvc
var viewComponentContext = new ViewComponentContext(
viewComponentDescriptor,
new object[0],
new Dictionary<string, object>(),
new HtmlTestEncoder(),
viewContext,
writer);
@ -65,5 +65,4 @@ namespace Microsoft.AspNet.Mvc
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.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.AspNet.Http;
@ -85,7 +86,7 @@ namespace Microsoft.AspNet.Mvc
var viewComponentContext = new ViewComponentContext(
viewComponentDescriptor,
new object[0],
new Dictionary<string, object>(),
new HtmlTestEncoder(),
viewContext,
writer);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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