Implementation of ViewComponents

This commit is contained in:
Ryan Nowak 2014-03-12 18:41:58 -07:00
parent ed36084c25
commit 86ac978451
37 changed files with 1250 additions and 14 deletions

View File

@ -0,0 +1,43 @@

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc;
namespace MvcSample.Web.Components
{
[ViewComponent(Name="Tags")]
public class TagCloud : ViewComponent
{
private readonly string[] Tags =
("Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua" +
"Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat Duis aute irure " +
"dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur Excepteur sint occaecat cupidatat" +
"non proident, sunt in culpa qui officia deserunt mollit anim id est laborum")
.Split(new char[] {' '}, StringSplitOptions.RemoveEmptyEntries)
.OrderBy(s => Guid.NewGuid().ToString())
.ToArray();
public async Task<IViewComponentResult> InvokeAsync(int count)
{
var tags = await GetTagsAsync(count);
return View(tags);
}
public IViewComponentResult Invoke(int count)
{
var tags = GetTags(count);
return View(tags);
}
private Task<string[]> GetTagsAsync(int count)
{
return Task.FromResult(GetTags(count));
}
private string[] GetTags(int count)
{
return Tags.Take(count).ToArray();
}
}
}

View File

@ -0,0 +1,8 @@
@model string[]
<div style="width: 300px; height: 300px">
@foreach (var tag in Model)
{
<span>@tag</span>
}
</div>

View File

@ -41,4 +41,7 @@
<p>You can easily find a web hosting company that offers the right mix of features and price for your applications.</p>
<p><a class="btn btn-default" href="http://go.microsoft.com/fwlink/?LinkId=301867">Learn more &raquo;</a></p>
</div>
<div style="float: right; border: 5px solid red;">
@await Component.InvokeAsync("Tags", 15)
</div>
</div>

View File

@ -4,6 +4,7 @@ using System.Globalization;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.DependencyInjection;
using Microsoft.AspNet.Mvc.Rendering;
namespace Microsoft.AspNet.Mvc
@ -33,13 +34,7 @@ namespace Microsoft.AspNet.Mvc
context.HttpContext.Response.ContentType = "text/html";
using (var writer = new StreamWriter(context.HttpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true))
{
var viewContext = new ViewContext(_serviceProvider, context.HttpContext, context.RouteValues)
{
Url = new UrlHelper(context.HttpContext, context.Router, context.RouteValues),
ViewData = ViewData,
Writer = writer,
};
var viewContext = CreateViewContext(context, writer);
await view.RenderAsync(viewContext, writer);
}
}
@ -57,5 +52,24 @@ namespace Microsoft.AspNet.Mvc
return result.View;
}
private ViewContext CreateViewContext([NotNull] ActionContext actionContext, [NotNull] TextWriter writer)
{
var urlHelper = new UrlHelper(actionContext.HttpContext, actionContext.Router, actionContext.RouteValues);
var viewContext = new ViewContext(_serviceProvider, actionContext.HttpContext, actionContext.RouteValues)
{
Url = urlHelper,
ViewData = ViewData,
Writer = writer,
};
viewContext.Component = new DefaultViewComponentHelper(
_serviceProvider.GetService<IViewComponentSelector>(),
_serviceProvider.GetService<IViewComponentInvokerFactory>(),
viewContext);
return viewContext;
}
}
}

View File

@ -58,6 +58,150 @@ namespace Microsoft.AspNet.Mvc.Core
return GetString("ReflectedActionFilterEndPoint_UnexpectedActionDescriptor");
}
/// <summary>
/// The view component name '{0}' matched multiple types: {1}
/// </summary>
internal static string ViewComponent_AmbiguousTypeMatch
{
get { return GetString("ViewComponent_AmbiguousTypeMatch"); }
}
/// <summary>
/// The view component name '{0}' matched multiple types: {1}
/// </summary>
internal static string FormatViewComponent_AmbiguousTypeMatch(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AmbiguousTypeMatch"), p0, p1);
}
/// <summary>
/// The async view component method '{0}' should be declared to return Task&lt;T&gt;.
/// </summary>
internal static string ViewComponent_AsyncMethod_ShouldReturnTask
{
get { return GetString("ViewComponent_AsyncMethod_ShouldReturnTask"); }
}
/// <summary>
/// The async view component method '{0}' should be declared to return Task&lt;T&gt;.
/// </summary>
internal static string FormatViewComponent_AsyncMethod_ShouldReturnTask(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AsyncMethod_ShouldReturnTask"), p0);
}
/// <summary>
/// A view component must return a non-null value.
/// </summary>
internal static string ViewComponent_MustReturnValue
{
get { return GetString("ViewComponent_MustReturnValue"); }
}
/// <summary>
/// A view component must return a non-null value.
/// </summary>
internal static string FormatViewComponent_MustReturnValue()
{
return GetString("ViewComponent_MustReturnValue");
}
/// <summary>
/// The view component method '{0}' should be declared to return a value.
/// </summary>
internal static string ViewComponent_SyncMethod_ShouldReturnValue
{
get { return GetString("ViewComponent_SyncMethod_ShouldReturnValue"); }
}
/// <summary>
/// The view component method '{0}' should be declared to return a value.
/// </summary>
internal static string FormatViewComponent_SyncMethod_ShouldReturnValue(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_SyncMethod_ShouldReturnValue"), p0);
}
/// <summary>
/// A view component named '{0}' could not be found.
/// </summary>
internal static string ViewComponent_CannotFindComponent
{
get { return GetString("ViewComponent_CannotFindComponent"); }
}
/// <summary>
/// A view component named '{0}' could not be found.
/// </summary>
internal static string FormatViewComponent_CannotFindComponent(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_CannotFindComponent"), p0);
}
/// <summary>
/// An invoker could not be created for the view component '{0}'.
/// </summary>
internal static string ViewComponent_IViewComponentFactory_ReturnedNull
{
get { return GetString("ViewComponent_IViewComponentFactory_ReturnedNull"); }
}
/// <summary>
/// An invoker could not be created for the view component '{0}'.
/// </summary>
internal static string FormatViewComponent_IViewComponentFactory_ReturnedNull(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_IViewComponentFactory_ReturnedNull"), p0);
}
/// <summary>
/// Could not find an '{0}' method matching the parameters.
/// </summary>
internal static string ViewComponent_CannotFindMethod
{
get { return GetString("ViewComponent_CannotFindMethod"); }
}
/// <summary>
/// Could not find an '{0}' method matching the parameters.
/// </summary>
internal static string FormatViewComponent_CannotFindMethod(object p0)
{
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);
}
/// <summary>
/// View components only support returning {0}, {1} or {2}.
/// </summary>
internal static string ViewComponent_InvalidReturnValue
{
get { return GetString("ViewComponent_InvalidReturnValue"); }
}
/// <summary>
/// View components only support returning {0}, {1} or {2}.
/// </summary>
internal static string FormatViewComponent_InvalidReturnValue(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_InvalidReturnValue"), p0, p1, p2);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -24,11 +24,16 @@ namespace Microsoft.AspNet.Mvc
public static async Task<object> ExecuteAsync(MethodInfo actionMethodInfo, object instance, IDictionary<string, object> actionArguments)
{
var methodArguments = PrepareArguments(actionArguments, actionMethodInfo.GetParameters());
var orderedArguments = PrepareArguments(actionArguments, actionMethodInfo.GetParameters());
return await ExecuteAsync(actionMethodInfo, instance, orderedArguments);
}
public static async Task<object> ExecuteAsync(MethodInfo actionMethodInfo, object instance, object[] orderedActionArguments)
{
object invocationResult = null;
try
{
invocationResult = actionMethodInfo.Invoke(instance, methodArguments);
invocationResult = actionMethodInfo.Invoke(instance, orderedActionArguments);
}
catch (TargetInvocationException targetInvocationException)
{

View File

@ -120,10 +120,37 @@
<data name="ActionExecutor_WrappedTaskInstance" xml:space="preserve">
<value>The method '{0}' on type '{1}' returned an instance of '{2}'. Make sure to call Unwrap on the returned value to avoid unobserved faulted Task.</value>
</data>
<data name="ActionExecutor_UnexpectedTaskInstance" xml:space="preserve">
<data name="ActionExecutor_UnexpectedTaskInstance" xml:space="preserve">
<value>The method '{0}' on type '{1}' returned a Task instance even though it is not an asynchronous method.</value>
</data>
<data name="ReflectedActionFilterEndPoint_UnexpectedActionDescriptor" xml:space="preserve">
<value>The class ReflectedActionFilterEndPoint only supports ReflectedActionDescriptors.</value>
</data>
<data name="ViewComponent_AmbiguousTypeMatch" xml:space="preserve">
<value>The view component name '{0}' matched multiple types: {1}</value>
</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>
</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>
</data>
<data name="ViewComponent_CannotFindComponent" xml:space="preserve">
<value>A view component named '{0}' could not be found.</value>
</data>
<data name="ViewComponent_IViewComponentFactory_ReturnedNull" xml:space="preserve">
<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>
</data>
<data name="ViewComponent_InvalidReturnValue" xml:space="preserve">
<value>View components only support returning {0}, {1} or {2}.</value>
</data>
</root>

View File

@ -0,0 +1,32 @@

using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Rendering;
namespace Microsoft.AspNet.Mvc
{
public class ContentViewComponentResult : IViewComponentResult
{
private readonly HtmlString _encoded;
public ContentViewComponentResult([NotNull] string content)
{
_encoded = new HtmlString(WebUtility.HtmlEncode(content));
}
public ContentViewComponentResult([NotNull] HtmlString encoded)
{
_encoded = encoded;
}
public void Execute([NotNull] ViewComponentContext context)
{
context.Writer.Write(_encoded.ToString());
}
public async Task ExecuteAsync([NotNull] ViewComponentContext context)
{
await context.Writer.WriteAsync(_encoded.ToString());
}
}
}

View File

@ -0,0 +1,116 @@

using System;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Rendering;
namespace Microsoft.AspNet.Mvc
{
public class DefaultViewComponentHelper : IViewComponentHelper
{
private readonly IViewComponentInvokerFactory _invokerFactory;
private readonly IViewComponentSelector _selector;
private readonly ViewContext _viewContext;
public DefaultViewComponentHelper(
[NotNull] IViewComponentSelector selector,
[NotNull] IViewComponentInvokerFactory invokerFactory,
[NotNull] ViewContext viewContext)
{
_selector = selector;
_invokerFactory = invokerFactory;
_viewContext = viewContext;
}
public HtmlString Invoke([NotNull] string name, params object[] args)
{
var componentType = SelectComponent(name);
return Invoke(componentType, args);
}
public HtmlString Invoke([NotNull] Type componentType, params object[] args)
{
using (var writer = new StringWriter())
{
InvokeCore(writer, componentType, args);
return new HtmlString(writer.ToString());
}
}
public void RenderInvoke([NotNull] string name, params object[] args)
{
var componentType = SelectComponent(name);
InvokeCore(_viewContext.Writer, componentType, args);
}
public void RenderInvoke([NotNull] Type componentType, params object[] args)
{
InvokeCore(_viewContext.Writer, componentType, args);
}
public async Task<HtmlString> InvokeAsync([NotNull] string name, params object[] args)
{
var componentType = SelectComponent(name);
return await InvokeAsync(componentType, args);
}
public async Task<HtmlString> InvokeAsync([NotNull] Type componentType, params object[] args)
{
using (var writer = new StringWriter())
{
await InvokeCoreAsync(writer, componentType, args);
return new HtmlString(writer.ToString());
}
}
public async Task RenderInvokeAsync([NotNull] string name, params object[] args)
{
var componentType = SelectComponent(name);
await InvokeCoreAsync(_viewContext.Writer, componentType, args);
}
public async Task RenderInvokeAsync([NotNull] Type componentType, params object[] args)
{
await InvokeCoreAsync(_viewContext.Writer, componentType, args);
}
private Type SelectComponent([NotNull] string name)
{
var componentType = _selector.SelectComponent(name);
if (componentType == null)
{
throw new InvalidOperationException(Resources.FormatViewComponent_CannotFindComponent(name));
}
return componentType;
}
private async Task InvokeCoreAsync([NotNull] TextWriter writer, [NotNull] Type componentType, object[] args)
{
var invoker = _invokerFactory.CreateInstance(componentType.GetTypeInfo(), args);
if (invoker == null)
{
throw new InvalidOperationException(
Resources.FormatViewComponent_IViewComponentFactory_ReturnedNull(componentType));
}
var context = new ViewComponentContext(componentType.GetTypeInfo(), _viewContext, writer);
await invoker.InvokeAsync(context);
}
private void InvokeCore([NotNull] TextWriter writer, [NotNull] Type componentType, object[] arguments)
{
var invoker = _invokerFactory.CreateInstance(componentType.GetTypeInfo(), arguments);
if (invoker == null)
{
throw new InvalidOperationException(
Resources.FormatViewComponent_IViewComponentFactory_ReturnedNull(componentType));
}
var context = new ViewComponentContext(componentType.GetTypeInfo(), _viewContext, writer);
invoker.Invoke(context);
}
}
}

View File

@ -0,0 +1,169 @@

using System;
using System.Linq;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.AspNet.DependencyInjection;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Core.ViewComponents;
using Microsoft.AspNet.Mvc.Rendering;
namespace Microsoft.AspNet.Mvc
{
public class DefaultViewComponentInvoker : IViewComponentInvoker
{
private readonly IServiceProvider _serviceProvider;
private readonly TypeInfo _componentType;
private readonly object[] _args;
public DefaultViewComponentInvoker(
[NotNull] IServiceProvider serviceProvider,
[NotNull] TypeInfo componentType,
object[] args)
{
_serviceProvider = serviceProvider;
_componentType = componentType;
_args = args ?? new object[0];
}
public void Invoke([NotNull] ViewComponentContext context)
{
var method = ViewComponentMethodSelector.FindSyncMethod(_componentType, _args);
if (method == null)
{
throw new InvalidOperationException(
Resources.FormatViewComponent_CannotFindMethod(ViewComponentMethodSelector.SyncMethodName));
}
var result = InvokeSyncCore(method, context.ViewContext);
result.Execute(context);
}
public async Task InvokeAsync([NotNull] ViewComponentContext context)
{
IViewComponentResult result;
var asyncMethod = ViewComponentMethodSelector.FindAsyncMethod(_componentType, _args);
if (asyncMethod == 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(_componentType, _args);
if (syncMethod == null)
{
throw new InvalidOperationException(
Resources.FormatViewComponent_CannotFindMethod_WithFallback(
ViewComponentMethodSelector.SyncMethodName, ViewComponentMethodSelector.AsyncMethodName));
}
else
{
result = InvokeSyncCore(syncMethod, context.ViewContext);
}
}
else
{
result = await InvokeAsyncCore(asyncMethod, context.ViewContext);
}
await result.ExecuteAsync(context);
}
private object CreateComponent([NotNull] ViewContext context)
{
var activator = _serviceProvider.GetService<ITypeActivator>();
object component = activator.CreateInstance(_serviceProvider, _componentType.AsType());
foreach (var prop in _componentType.AsType().GetRuntimeProperties())
{
if (prop.Name == "ViewContext" &&
typeof(ViewContext).GetTypeInfo().IsAssignableFrom(prop.PropertyType.GetTypeInfo()))
{
prop.SetValue(component, context);
}
else if (prop.Name == "ViewData" &&
typeof(ViewData).GetTypeInfo().IsAssignableFrom(prop.PropertyType.GetTypeInfo()))
{
// We're flowing the viewbag across, but the concept of model doesn't really apply here
var viewData = new ViewData(context.ViewData);
viewData.Model = null;
prop.SetValue(component, viewData);
}
}
var method = _componentType.AsType().GetRuntimeMethods()
.FirstOrDefault(m => m.Name.Equals("Initialize", StringComparison.OrdinalIgnoreCase));
if (method != null)
{
var args = method.GetParameters()
.Select(p => _serviceProvider.GetService(p.ParameterType)).ToArray();
method.Invoke(component, args);
}
return component;
}
private async Task<IViewComponentResult> InvokeAsyncCore([NotNull] MethodInfo method, [NotNull] ViewContext context)
{
var component = CreateComponent(context);
var result = await ReflectedActionExecutor.ExecuteAsync(method, component, _args);
return CoerceToViewComponentResult(result);
}
public IViewComponentResult InvokeSyncCore([NotNull] MethodInfo method, [NotNull] ViewContext context)
{
var component = CreateComponent(context);
object result = null;
try
{
result = method.Invoke(component, _args);
}
catch (TargetInvocationException ex)
{
// Preserve callstack of any user-thrown exceptions.
var exceptionInfo = ExceptionDispatchInfo.Capture(ex.InnerException);
exceptionInfo.Throw();
}
return CoerceToViewComponentResult(result);
}
private static IViewComponentResult CoerceToViewComponentResult(object value)
{
if (value == null)
{
throw new InvalidOperationException(Resources.ViewComponent_MustReturnValue);
}
var componentResult = value as IViewComponentResult;
if (componentResult != null)
{
return componentResult;
}
var stringResult = value as string;
if (stringResult != null)
{
return new ContentViewComponentResult(stringResult);
}
var htmlStringResult = value as HtmlString;
if (htmlStringResult != null)
{
return new ContentViewComponentResult(htmlStringResult);
}
throw new InvalidOperationException(Resources.FormatViewComponent_InvalidReturnValue(
typeof(string).Name,
typeof(HtmlString).Name,
typeof(IViewComponentResult).Name));
}
}
}

View File

@ -0,0 +1,23 @@

using System.Reflection;
using Microsoft.AspNet.DependencyInjection;
namespace Microsoft.AspNet.Mvc
{
public class DefaultViewComponentInvokerFactory : IViewComponentInvokerFactory
{
private readonly INestedProviderManager<ViewComponentInvokerProviderContext> _providerManager;
public DefaultViewComponentInvokerFactory(INestedProviderManager<ViewComponentInvokerProviderContext> providerManager)
{
_providerManager = providerManager;
}
public IViewComponentInvoker CreateInstance([NotNull] TypeInfo componentType, object[] args)
{
var context = new ViewComponentInvokerProviderContext(componentType, args);
_providerManager.Invoke(context);
return context.Result;
}
}
}

View File

@ -0,0 +1,26 @@

using System;
namespace Microsoft.AspNet.Mvc
{
public class DefaultViewComponentInvokerProvider : IViewComponentInvokerProvider
{
private readonly IServiceProvider _serviceProvider;
public DefaultViewComponentInvokerProvider(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public int Order
{
get { return 0; }
}
public void Invoke([NotNull] ViewComponentInvokerProviderContext context, [NotNull] Action callNext)
{
context.Result = new DefaultViewComponentInvoker(_serviceProvider, context.ComponentType, context.Arguments);
callNext();
}
}
}

View File

@ -0,0 +1,31 @@

using Microsoft.AspNet.Mvc.Rendering;
namespace Microsoft.AspNet.Mvc
{
public class DefaultViewComponentResultHelper : IViewComponentResultHelper
{
private readonly IViewEngine _viewEngine;
public DefaultViewComponentResultHelper(IViewEngine viewEngine)
{
_viewEngine = viewEngine;
}
public IViewComponentResult Content([NotNull] string content)
{
return new ContentViewComponentResult(content);
}
public IViewComponentResult Json([NotNull] object value)
{
return new JsonViewComponentResult(value);
}
public IViewComponentResult View([NotNull] string viewName, [NotNull] ViewData viewData)
{
return new ViewViewComponentResult(_viewEngine, viewName, viewData);
}
}
}

View File

@ -0,0 +1,48 @@

using System;
using System.Linq;
using Microsoft.AspNet.Mvc.Core;
namespace Microsoft.AspNet.Mvc
{
public class DefaultViewComponentSelector : IViewComponentSelector
{
private readonly IControllerAssemblyProvider _assemblyProvider;
public DefaultViewComponentSelector(IControllerAssemblyProvider assemblyProvider)
{
_assemblyProvider = assemblyProvider;
}
public Type SelectComponent([NotNull] string componentName)
{
var assemblies = _assemblyProvider.CandidateAssemblies;
var types = assemblies.SelectMany(a => a.DefinedTypes);
var components =
types
.Where(ViewComponentConventions.IsComponent)
.Select(c => new {Name = ViewComponentConventions.GetComponentName(c), Type = c.AsType()});
var matching =
components
.Where(c => string.Equals(c.Name, componentName, StringComparison.OrdinalIgnoreCase))
.ToArray();
if (matching.Length == 0)
{
return null;
}
else if (matching.Length == 1)
{
return matching[0].Type;
}
else
{
var typeNames = string.Join(Environment.NewLine, matching.Select(t => t.Type.FullName));
throw new InvalidOperationException(
Resources.FormatViewComponent_AmbiguousTypeMatch(componentName, typeNames));
}
}
}
}

View File

@ -0,0 +1,12 @@

using System.Threading.Tasks;
namespace Microsoft.AspNet.Mvc
{
public interface IViewComponentInvoker
{
void Invoke([NotNull] ViewComponentContext context);
Task InvokeAsync([NotNull] ViewComponentContext context);
}
}

View File

@ -0,0 +1,10 @@

using System.Reflection;
namespace Microsoft.AspNet.Mvc
{
public interface IViewComponentInvokerFactory
{
IViewComponentInvoker CreateInstance([NotNull] TypeInfo componentType, object[] args);
}
}

View File

@ -0,0 +1,9 @@

using Microsoft.AspNet.DependencyInjection;
namespace Microsoft.AspNet.Mvc
{
public interface IViewComponentInvokerProvider : INestedProvider<ViewComponentInvokerProviderContext>
{
}
}

View File

@ -0,0 +1,14 @@

using Microsoft.AspNet.Mvc.Rendering;
namespace Microsoft.AspNet.Mvc
{
public interface IViewComponentResultHelper
{
IViewComponentResult Content([NotNull] string content);
IViewComponentResult Json([NotNull] object value);
IViewComponentResult View([NotNull] string viewName, [NotNull] ViewData viewData);
}
}

View File

@ -0,0 +1,10 @@

using System;
namespace Microsoft.AspNet.Mvc
{
public interface IViewComponentSelector
{
Type SelectComponent([NotNull] string componentName);
}
}

View File

@ -0,0 +1,88 @@

using System;
using System.IO;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace Microsoft.AspNet.Mvc
{
public class JsonViewComponentResult : IViewComponentResult
{
private readonly object _value;
private JsonSerializerSettings _jsonSerializerSettings;
public JsonViewComponentResult([NotNull] object value)
{
_value = value;
_jsonSerializerSettings = CreateSerializerSettings();
}
public JsonSerializerSettings SerializerSettings
{
get { return _jsonSerializerSettings; }
set
{
if (value == null)
{
throw new ArgumentNullException("value");
}
_jsonSerializerSettings = value;
}
}
/// <summary>
/// Gets or sets a value indicating whether to indent elements when writing data.
/// </summary>
public bool Indent { get; set; }
private JsonSerializerSettings CreateSerializerSettings()
{
return new JsonSerializerSettings()
{
MissingMemberHandling = MissingMemberHandling.Ignore,
// Do not change this setting
// Setting this to None prevents Json.NET from loading malicious, unsafe, or security-sensitive types.
TypeNameHandling = TypeNameHandling.None
};
}
private JsonSerializer CreateJsonSerializer()
{
var jsonSerializer = JsonSerializer.Create(SerializerSettings);
return jsonSerializer;
}
private JsonWriter CreateJsonWriter([NotNull] TextWriter writer)
{
var jsonWriter = new JsonTextWriter(writer);
if (Indent)
{
jsonWriter.Formatting = Formatting.Indented;
}
return jsonWriter;
}
public void Execute([NotNull] ViewComponentContext context)
{
using (var jsonWriter = CreateJsonWriter(context.Writer))
{
jsonWriter.CloseOutput = false;
var jsonSerializer = CreateJsonSerializer();
jsonSerializer.Serialize(jsonWriter, _value);
jsonWriter.Flush();
}
}
public async Task ExecuteAsync([NotNull] ViewComponentContext context)
{
Execute(context);
}
}
}

View File

@ -0,0 +1,52 @@

using Microsoft.AspNet.Abstractions;
using Microsoft.AspNet.Mvc.Rendering;
namespace Microsoft.AspNet.Mvc
{
[ViewComponent]
public abstract class ViewComponent
{
public HttpContext Context
{
get { return ViewContext == null ? null : ViewContext.HttpContext; }
}
public IViewComponentResultHelper Result { get; private set; }
public ViewContext ViewContext { get; set; }
public ViewData ViewData { get; set; }
public void Initialize(IViewComponentResultHelper result)
{
Result = result;
}
public IViewComponentResult View()
{
return View<object>(null, null);
}
public IViewComponentResult View(string viewName)
{
return View<object>(viewName, null);
}
public IViewComponentResult View<TModel>(TModel model)
{
return View(null, model);
}
public IViewComponentResult View<TModel>(string viewName, TModel model)
{
var viewData = new ViewData<TModel>(ViewData);
if (model != null)
{
viewData.Model = model;
}
return Result.View(viewName ?? "Default", viewData);
}
}
}

View File

@ -0,0 +1,11 @@

using System;
namespace Microsoft.AspNet.Mvc
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class ViewComponentAttribute : Attribute
{
public string Name { get; set; }
}
}

View File

@ -0,0 +1,43 @@

using System;
using System.Reflection;
namespace Microsoft.AspNet.Mvc
{
public static class ViewComponentConventions
{
private const string ViewComponentSuffix = "ViewComponent";
public static string GetComponentName([NotNull] TypeInfo componentType)
{
var attribute = componentType.GetCustomAttribute<ViewComponentAttribute>();
if (attribute != null && !string.IsNullOrEmpty(attribute.Name))
{
return attribute.Name;
}
if (componentType.Name.EndsWith(ViewComponentSuffix, StringComparison.OrdinalIgnoreCase))
{
return componentType.Name.Substring(0, componentType.Name.Length - ViewComponentSuffix.Length);
}
else
{
return componentType.Name;
}
}
public static bool IsComponent([NotNull] TypeInfo typeInfo)
{
if (!typeInfo.IsClass ||
typeInfo.IsAbstract ||
typeInfo.ContainsGenericParameters)
{
return false;
}
return
typeInfo.Name.EndsWith(ViewComponentSuffix, StringComparison.OrdinalIgnoreCase) ||
typeInfo.GetCustomAttribute<ViewComponentAttribute>() != null;
}
}
}

View File

@ -0,0 +1,21 @@

using System;
using System.Reflection;
namespace Microsoft.AspNet.Mvc
{
public class ViewComponentInvokerProviderContext
{
public ViewComponentInvokerProviderContext([NotNull] TypeInfo componentType, object[] arguments)
{
ComponentType = componentType;
Arguments = arguments;
}
public object[] Arguments { get; private set; }
public TypeInfo ComponentType { get; private set; }
public IViewComponentInvoker Result { get; set; }
}
}

View File

@ -0,0 +1,75 @@
using System;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.Core.ViewComponents
{
public static class ViewComponentMethodSelector
{
public const string AsyncMethodName = "InvokeAsync";
public const string SyncMethodName = "Invoke";
public static MethodInfo FindAsyncMethod([NotNull] TypeInfo componentType, object[] args)
{
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([NotNull] TypeInfo componentType, object[] args)
{
var method = GetMethod(componentType, args, SyncMethodName);
if (method == null)
{
return null;
}
if (method.ReturnType == typeof(void))
{
throw new InvalidOperationException(Resources.FormatViewComponent_SyncMethod_ShouldReturnValue(SyncMethodName));
}
return method;
}
private static MethodInfo GetMethod(TypeInfo componentType, object[] args, string methodName)
{
args = args ?? new object[0];
var argumentExpressions = new Expression[args.Length];
for (var i = 0; i < args.Length; i++)
{
argumentExpressions[i] = Expression.Constant(args[i], args[i].GetType());
}
try
{
// We're currently using this technique to make a call into a component method that looks like a regular method call.
//
// Ex: @Component.Invoke<Cart>("hello", 5) => cart.Invoke("hello", 5)
//
// This approach has some drawbacks, namely it doesn't account for default parameters, and more noticably, it throws
// if the method is not found.
//
// Unfortunely the overload of Type.GetMethod that we would like to use is not present in CoreCLR. Item #160 in Jira
// tracks these issues.
var expression = Expression.Call(Expression.Constant(null, componentType.AsType()), methodName, null, argumentExpressions);
return expression.Method;
}
catch (InvalidOperationException)
{
return null;
}
}
}
}

View File

@ -0,0 +1,95 @@

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Rendering;
namespace Microsoft.AspNet.Mvc
{
public class ViewViewComponentResult : IViewComponentResult
{
// {0} is the component name, {1} is the view name.
private const string ViewPathFormat = "Components/{0}/{1}";
private readonly IViewEngine _viewEngine;
private readonly string _viewName;
private readonly ViewData _viewData;
public ViewViewComponentResult([NotNull] IViewEngine viewEngine, [NotNull] string viewName, ViewData viewData)
{
_viewEngine = viewEngine;
_viewName = viewName;
_viewData = viewData;
}
public void Execute([NotNull] ViewComponentContext context)
{
throw new NotImplementedException("There's no support for syncronous views right now.");
}
public async Task ExecuteAsync([NotNull] ViewComponentContext context)
{
var childViewContext = new ViewContext(
context.ViewContext.ServiceProvider,
context.ViewContext.HttpContext,
context.ViewContext.ViewEngineContext)
{
Component = context.ViewContext.Component,
Url = context.ViewContext.Url,
ViewData = _viewData ?? context.ViewContext.ViewData,
Writer = context.Writer,
};
string qualifiedViewName;
if (_viewName.Length > 0 && _viewName[0] == '/')
{
// View name that was passed in is already a rooted path, the view engine will handle this.
qualifiedViewName = _viewName;
}
else
{
// This will produce a string like:
//
// Components/Cart/Default
//
// The view engine will combine this with other path info to search paths like:
//
// Views/Shared/Components/Cart/Default.cshtml
// Views/Home/Components/Cart/Default.cshtml
// Areas/Blog/Views/Shared/Components/Cart/Default.cshtml
//
// This supports a controller or area providing an override for component views.
qualifiedViewName = string.Format(
CultureInfo.InvariantCulture,
ViewPathFormat,
ViewComponentConventions.GetComponentName(context.ComponentType),
_viewName);
}
var view = await FindView(context.ViewContext.ViewEngineContext, qualifiedViewName);
using (view as IDisposable)
{
await view.RenderAsync(childViewContext, context.Writer);
}
}
private async Task<IView> FindView([NotNull] IDictionary<string, object> context, [NotNull] string viewName)
{
// Issue #161 in Jira tracks unduping this code.
var result = await _viewEngine.FindView(context, viewName);
if (!result.Success)
{
var locationsText = string.Join(Environment.NewLine, result.SearchedLocations);
const string message = @"The view &apos;{0}&apos; was not found. The following locations were searched:{1}.";
throw new InvalidOperationException(String.Format(
CultureInfo.CurrentCulture,
message,
viewName,
locationsText));
}
return result.View;
}
}
}

View File

@ -21,6 +21,7 @@
"System.Globalization": "4.0.10.0",
"System.IO": "4.0.0.0",
"System.Linq": "4.0.0.0",
"System.Linq.Expressions": "4.0.0.0",
"System.Reflection": "4.0.10.0",
"System.Reflection.Emit.ILGeneration": "4.0.0.0",
"System.Reflection.Emit.Lightweight": "4.0.0.0",
@ -34,4 +35,4 @@
}
}
}
}
}

View File

@ -10,6 +10,11 @@ namespace Microsoft.AspNet.Mvc.Razor
{
public abstract class RazorView : IView
{
public IViewComponentHelper Component
{
get { return Context == null ? null : Context.Component; }
}
public ViewContext Context { get; set; }
public string Layout { get; set; }

View File

@ -59,6 +59,11 @@ namespace Microsoft.AspNet.Mvc.Razor
}
}
public Task<ViewEngineResult> FindComponentView(object actionContext, string viewName)
{
throw new NotImplementedException();
}
private static bool IsSpecificPath(string name)
{
char c = name[0];

View File

@ -0,0 +1,26 @@

using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Rendering;
namespace Microsoft.AspNet.Mvc
{
public interface IViewComponentHelper
{
HtmlString Invoke(string name, params object[] args);
HtmlString Invoke(Type componentType, params object[] args);
void RenderInvoke(string name, params object[] args);
void RenderInvoke(Type componentType, params object[] args);
Task<HtmlString> InvokeAsync(string name, params object[] args);
Task<HtmlString> InvokeAsync(Type componentType, params object[] args);
Task RenderInvokeAsync(string name, params object[] args);
Task RenderInvokeAsync(Type componentType, params object[] args);
}
}

View File

@ -0,0 +1,12 @@

using System.Threading.Tasks;
namespace Microsoft.AspNet.Mvc
{
public interface IViewComponentResult
{
void Execute([NotNull] ViewComponentContext context);
Task ExecuteAsync([NotNull] ViewComponentContext context);
}
}

View File

@ -14,6 +14,8 @@ namespace Microsoft.AspNet.Mvc.Rendering
ViewEngineContext = viewEngineContext;
}
public IViewComponentHelper Component { get; set; }
public HttpContext HttpContext { get; private set; }
public IServiceProvider ServiceProvider { get; private set; }

View File

@ -28,12 +28,12 @@ namespace Microsoft.AspNet.Mvc.Rendering
: this(source.MetadataProvider)
{
_modelMetadata = source.ModelMetadata;
foreach (var entry in source.ModelState)
{
ModelState.Add(entry.Key, entry.Value);
}
foreach (var entry in source)
{
_data.Add(entry.Key, entry.Value);

View File

@ -0,0 +1,23 @@

using System.IO;
using System.Reflection;
using Microsoft.AspNet.Mvc.Rendering;
namespace Microsoft.AspNet.Mvc
{
public class ViewComponentContext
{
public ViewComponentContext([NotNull] TypeInfo componentType, [NotNull] ViewContext viewContext, [NotNull] TextWriter writer)
{
ComponentType = componentType;
ViewContext = viewContext;
Writer = writer;
}
public TypeInfo ComponentType { get; private set; }
public ViewContext ViewContext { get; private set; }
public TextWriter Writer { get; private set; }
}
}

View File

@ -0,0 +1,28 @@

using System.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.Rendering
{
public static class ViewComponentHelperExtensions
{
public static HtmlString Invoke<TComponent>([NotNull] this IViewComponentHelper helper, params object[] args)
{
return helper.Invoke(typeof(TComponent), args);
}
public static void RenderInvoke<TComponent>([NotNull] this IViewComponentHelper helper, params object[] args)
{
helper.RenderInvoke(typeof(TComponent), args);
}
public static async Task<HtmlString> InvokeAsync<TComponent>([NotNull] this IViewComponentHelper helper, params object[] args)
{
return await helper.InvokeAsync(typeof(TComponent), args);
}
public static async Task RenderInvokeAsync<TComponent>([NotNull] this IViewComponentHelper helper, params object[] args)
{
await helper.RenderInvokeAsync(typeof(TComponent), args);
}
}
}

View File

@ -68,6 +68,11 @@ namespace Microsoft.AspNet.Mvc
yield return describe.Transient<IModelValidatorProvider, DataAnnotationsModelValidatorProvider>();
yield return describe.Transient<IModelValidatorProvider, DataMemberModelValidatorProvider>();
yield return describe.Transient<IViewComponentSelector, DefaultViewComponentSelector>();
yield return describe.Transient<IViewComponentInvokerFactory, DefaultViewComponentInvokerFactory>();
yield return describe.Transient<INestedProvider<ViewComponentInvokerProviderContext>, DefaultViewComponentInvokerProvider>();
yield return describe.Transient<IViewComponentResultHelper, DefaultViewComponentResultHelper>();
yield return
describe.Describe(
typeof(INestedProviderManager<>),

View File

@ -36,7 +36,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
var result = await ReflectedActionExecutor.ExecuteAsync(
methodWithVoidReturnType.GetMethodInfo(),
null,
null);
(IDictionary<string, object>)null);
Assert.Same(null, result);
}