diff --git a/samples/MvcSample.Web/Components/TagCloud.cs b/samples/MvcSample.Web/Components/TagCloud.cs new file mode 100644 index 0000000000..874cd8fcce --- /dev/null +++ b/samples/MvcSample.Web/Components/TagCloud.cs @@ -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 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 GetTagsAsync(int count) + { + return Task.FromResult(GetTags(count)); + } + + private string[] GetTags(int count) + { + return Tags.Take(count).ToArray(); + } + } +} \ No newline at end of file diff --git a/samples/MvcSample.Web/Views/Shared/Components/Tags/Default.cshtml b/samples/MvcSample.Web/Views/Shared/Components/Tags/Default.cshtml new file mode 100644 index 0000000000..be64295b8c --- /dev/null +++ b/samples/MvcSample.Web/Views/Shared/Components/Tags/Default.cshtml @@ -0,0 +1,8 @@ +@model string[] + +
+ @foreach (var tag in Model) + { + @tag + } +
\ No newline at end of file diff --git a/samples/MvcSample.Web/Views/Shared/MyView.cshtml b/samples/MvcSample.Web/Views/Shared/MyView.cshtml index bcf9e01768..3a37319daf 100644 --- a/samples/MvcSample.Web/Views/Shared/MyView.cshtml +++ b/samples/MvcSample.Web/Views/Shared/MyView.cshtml @@ -41,4 +41,7 @@

You can easily find a web hosting company that offers the right mix of features and price for your applications.

Learn more »

+
+ @await Component.InvokeAsync("Tags", 15) +
diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs index 4363711e24..f3e91d50fe 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs @@ -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(), + _serviceProvider.GetService(), + viewContext); + + return viewContext; + } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs index 51ea1e10f7..d072c945e9 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -58,6 +58,150 @@ namespace Microsoft.AspNet.Mvc.Core return GetString("ReflectedActionFilterEndPoint_UnexpectedActionDescriptor"); } + /// + /// The view component name '{0}' matched multiple types: {1} + /// + internal static string ViewComponent_AmbiguousTypeMatch + { + get { return GetString("ViewComponent_AmbiguousTypeMatch"); } + } + + /// + /// The view component name '{0}' matched multiple types: {1} + /// + internal static string FormatViewComponent_AmbiguousTypeMatch(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AmbiguousTypeMatch"), p0, p1); + } + + /// + /// The async view component method '{0}' should be declared to return Task<T>. + /// + internal static string ViewComponent_AsyncMethod_ShouldReturnTask + { + get { return GetString("ViewComponent_AsyncMethod_ShouldReturnTask"); } + } + + /// + /// The async view component method '{0}' should be declared to return Task<T>. + /// + internal static string FormatViewComponent_AsyncMethod_ShouldReturnTask(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AsyncMethod_ShouldReturnTask"), p0); + } + + /// + /// A view component must return a non-null value. + /// + internal static string ViewComponent_MustReturnValue + { + get { return GetString("ViewComponent_MustReturnValue"); } + } + + /// + /// A view component must return a non-null value. + /// + internal static string FormatViewComponent_MustReturnValue() + { + return GetString("ViewComponent_MustReturnValue"); + } + + /// + /// The view component method '{0}' should be declared to return a value. + /// + internal static string ViewComponent_SyncMethod_ShouldReturnValue + { + get { return GetString("ViewComponent_SyncMethod_ShouldReturnValue"); } + } + + /// + /// The view component method '{0}' should be declared to return a value. + /// + internal static string FormatViewComponent_SyncMethod_ShouldReturnValue(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_SyncMethod_ShouldReturnValue"), p0); + } + + /// + /// A view component named '{0}' could not be found. + /// + internal static string ViewComponent_CannotFindComponent + { + get { return GetString("ViewComponent_CannotFindComponent"); } + } + + /// + /// A view component named '{0}' could not be found. + /// + internal static string FormatViewComponent_CannotFindComponent(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_CannotFindComponent"), p0); + } + + /// + /// An invoker could not be created for the view component '{0}'. + /// + internal static string ViewComponent_IViewComponentFactory_ReturnedNull + { + get { return GetString("ViewComponent_IViewComponentFactory_ReturnedNull"); } + } + + /// + /// An invoker could not be created for the view component '{0}'. + /// + internal static string FormatViewComponent_IViewComponentFactory_ReturnedNull(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_IViewComponentFactory_ReturnedNull"), p0); + } + + /// + /// Could not find an '{0}' method matching the parameters. + /// + internal static string ViewComponent_CannotFindMethod + { + get { return GetString("ViewComponent_CannotFindMethod"); } + } + + /// + /// Could not find an '{0}' method matching the parameters. + /// + internal static string FormatViewComponent_CannotFindMethod(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_CannotFindMethod"), p0); + } + + /// + /// Could not find an '{0}' or '{1}' method matching the parameters. + /// + internal static string ViewComponent_CannotFindMethod_WithFallback + { + get { return GetString("ViewComponent_CannotFindMethod_WithFallback"); } + } + + /// + /// Could not find an '{0}' or '{1}' method matching the parameters. + /// + internal static string FormatViewComponent_CannotFindMethod_WithFallback(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_CannotFindMethod_WithFallback"), p0, p1); + } + + /// + /// View components only support returning {0}, {1} or {2}. + /// + internal static string ViewComponent_InvalidReturnValue + { + get { return GetString("ViewComponent_InvalidReturnValue"); } + } + + /// + /// View components only support returning {0}, {1} or {2}. + /// + 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); diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionExecutor.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionExecutor.cs index dac59c3835..66671dade2 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionExecutor.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionExecutor.cs @@ -24,11 +24,16 @@ namespace Microsoft.AspNet.Mvc public static async Task ExecuteAsync(MethodInfo actionMethodInfo, object instance, IDictionary actionArguments) { - var methodArguments = PrepareArguments(actionArguments, actionMethodInfo.GetParameters()); + var orderedArguments = PrepareArguments(actionArguments, actionMethodInfo.GetParameters()); + return await ExecuteAsync(actionMethodInfo, instance, orderedArguments); + } + + public static async Task ExecuteAsync(MethodInfo actionMethodInfo, object instance, object[] orderedActionArguments) + { object invocationResult = null; try { - invocationResult = actionMethodInfo.Invoke(instance, methodArguments); + invocationResult = actionMethodInfo.Invoke(instance, orderedActionArguments); } catch (TargetInvocationException targetInvocationException) { diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx index 8f3b0948bb..c3d3b06696 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -120,10 +120,37 @@ 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. - + The method '{0}' on type '{1}' returned a Task instance even though it is not an asynchronous method. The class ReflectedActionFilterEndPoint only supports ReflectedActionDescriptors. + + The view component name '{0}' matched multiple types: {1} + + + The async view component method '{0}' should be declared to return Task<T>. + + + A view component must return a non-null value. + + + The view component method '{0}' should be declared to return a value. + + + A view component named '{0}' could not be found. + + + An invoker could not be created for the view component '{0}'. + + + Could not find an '{0}' method matching the parameters. + + + Could not find an '{0}' or '{1}' method matching the parameters. + + + View components only support returning {0}, {1} or {2}. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ContentViewComponentResult.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ContentViewComponentResult.cs new file mode 100644 index 0000000000..919119ca44 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ContentViewComponentResult.cs @@ -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()); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentHelper.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentHelper.cs new file mode 100644 index 0000000000..365364f009 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentHelper.cs @@ -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 InvokeAsync([NotNull] string name, params object[] args) + { + var componentType = SelectComponent(name); + return await InvokeAsync(componentType, args); + } + + public async Task 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); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentInvoker.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentInvoker.cs new file mode 100644 index 0000000000..b9c485731c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentInvoker.cs @@ -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(); + 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 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)); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentInvokerFactory.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentInvokerFactory.cs new file mode 100644 index 0000000000..4b0dcdfb1c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentInvokerFactory.cs @@ -0,0 +1,23 @@ + +using System.Reflection; +using Microsoft.AspNet.DependencyInjection; + +namespace Microsoft.AspNet.Mvc +{ + public class DefaultViewComponentInvokerFactory : IViewComponentInvokerFactory + { + private readonly INestedProviderManager _providerManager; + + public DefaultViewComponentInvokerFactory(INestedProviderManager providerManager) + { + _providerManager = providerManager; + } + + public IViewComponentInvoker CreateInstance([NotNull] TypeInfo componentType, object[] args) + { + var context = new ViewComponentInvokerProviderContext(componentType, args); + _providerManager.Invoke(context); + return context.Result; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentInvokerProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentInvokerProvider.cs new file mode 100644 index 0000000000..e9f3f8e2c6 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentInvokerProvider.cs @@ -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(); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentResultHelper.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentResultHelper.cs new file mode 100644 index 0000000000..45db71b0f0 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentResultHelper.cs @@ -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); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentSelector.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentSelector.cs new file mode 100644 index 0000000000..6c6a577265 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/DefaultViewComponentSelector.cs @@ -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)); + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/IViewComponentInvoker.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/IViewComponentInvoker.cs new file mode 100644 index 0000000000..544a678464 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/IViewComponentInvoker.cs @@ -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); + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/IViewComponentInvokerFactory.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/IViewComponentInvokerFactory.cs new file mode 100644 index 0000000000..92198bcbb1 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/IViewComponentInvokerFactory.cs @@ -0,0 +1,10 @@ + +using System.Reflection; + +namespace Microsoft.AspNet.Mvc +{ + public interface IViewComponentInvokerFactory + { + IViewComponentInvoker CreateInstance([NotNull] TypeInfo componentType, object[] args); + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/IViewComponentInvokerProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/IViewComponentInvokerProvider.cs new file mode 100644 index 0000000000..201b004ed9 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/IViewComponentInvokerProvider.cs @@ -0,0 +1,9 @@ + +using Microsoft.AspNet.DependencyInjection; + +namespace Microsoft.AspNet.Mvc +{ + public interface IViewComponentInvokerProvider : INestedProvider + { + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/IViewComponentResultHelper.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/IViewComponentResultHelper.cs new file mode 100644 index 0000000000..df283d33c3 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/IViewComponentResultHelper.cs @@ -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); + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/IViewComponentSelector.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/IViewComponentSelector.cs new file mode 100644 index 0000000000..a02ae010c6 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/IViewComponentSelector.cs @@ -0,0 +1,10 @@ + +using System; + +namespace Microsoft.AspNet.Mvc +{ + public interface IViewComponentSelector + { + Type SelectComponent([NotNull] string componentName); + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/JsonViewComponentResult.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/JsonViewComponentResult.cs new file mode 100644 index 0000000000..122d9d7890 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/JsonViewComponentResult.cs @@ -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; + } + } + + /// + /// Gets or sets a value indicating whether to indent elements when writing data. + /// + 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); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponent.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponent.cs new file mode 100644 index 0000000000..f971a42f90 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponent.cs @@ -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(null, null); + } + + public IViewComponentResult View(string viewName) + { + return View(viewName, null); + } + + public IViewComponentResult View(TModel model) + { + return View(null, model); + } + + public IViewComponentResult View(string viewName, TModel model) + { + var viewData = new ViewData(ViewData); + if (model != null) + { + viewData.Model = model; + } + + return Result.View(viewName ?? "Default", viewData); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponentAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponentAttribute.cs new file mode 100644 index 0000000000..85f834d9e6 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponentAttribute.cs @@ -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; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponentConventions.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponentConventions.cs new file mode 100644 index 0000000000..e475c65e42 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponentConventions.cs @@ -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(); + 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() != null; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponentInvokerProviderContext.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponentInvokerProviderContext.cs new file mode 100644 index 0000000000..4d650ec678 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponentInvokerProviderContext.cs @@ -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; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponentMethodSelector.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponentMethodSelector.cs new file mode 100644 index 0000000000..3455912ed0 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponentMethodSelector.cs @@ -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("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; + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewViewComponentResult.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewViewComponentResult.cs new file mode 100644 index 0000000000..2063adb4a7 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewViewComponentResult.cs @@ -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 FindView([NotNull] IDictionary 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 '{0}' was not found. The following locations were searched:{1}."; + throw new InvalidOperationException(String.Format( + CultureInfo.CurrentCulture, + message, + viewName, + locationsText)); + } + + return result.View; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/project.json b/src/Microsoft.AspNet.Mvc.Core/project.json index 5c7f38a4b0..c984e9ed72 100644 --- a/src/Microsoft.AspNet.Mvc.Core/project.json +++ b/src/Microsoft.AspNet.Mvc.Core/project.json @@ -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 @@ } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs index 92833890a2..90cf64f13c 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs @@ -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; } diff --git a/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/RazorViewEngine.cs b/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/RazorViewEngine.cs index a70e9103eb..c3c7a88b4e 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/RazorViewEngine.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/RazorViewEngine.cs @@ -59,6 +59,11 @@ namespace Microsoft.AspNet.Mvc.Razor } } + public Task FindComponentView(object actionContext, string viewName) + { + throw new NotImplementedException(); + } + private static bool IsSpecificPath(string name) { char c = name[0]; diff --git a/src/Microsoft.AspNet.Mvc.Rendering/IViewComponentHelper.cs b/src/Microsoft.AspNet.Mvc.Rendering/IViewComponentHelper.cs new file mode 100644 index 0000000000..107344e39c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Rendering/IViewComponentHelper.cs @@ -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 InvokeAsync(string name, params object[] args); + + Task InvokeAsync(Type componentType, params object[] args); + + Task RenderInvokeAsync(string name, params object[] args); + + Task RenderInvokeAsync(Type componentType, params object[] args); + } +} diff --git a/src/Microsoft.AspNet.Mvc.Rendering/IViewComponentResult.cs b/src/Microsoft.AspNet.Mvc.Rendering/IViewComponentResult.cs new file mode 100644 index 0000000000..57e2ccd897 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Rendering/IViewComponentResult.cs @@ -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); + } +} diff --git a/src/Microsoft.AspNet.Mvc.Rendering/View/ViewContext.cs b/src/Microsoft.AspNet.Mvc.Rendering/View/ViewContext.cs index f0dea94c1d..7939753976 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/View/ViewContext.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/View/ViewContext.cs @@ -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; } diff --git a/src/Microsoft.AspNet.Mvc.Rendering/View/ViewData.cs b/src/Microsoft.AspNet.Mvc.Rendering/View/ViewData.cs index a02fbe762a..4752b008a5 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/View/ViewData.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/View/ViewData.cs @@ -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); diff --git a/src/Microsoft.AspNet.Mvc.Rendering/ViewComponentContext.cs b/src/Microsoft.AspNet.Mvc.Rendering/ViewComponentContext.cs new file mode 100644 index 0000000000..5fcaa1cd67 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Rendering/ViewComponentContext.cs @@ -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; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Rendering/ViewComponentHelperExtensions.cs b/src/Microsoft.AspNet.Mvc.Rendering/ViewComponentHelperExtensions.cs new file mode 100644 index 0000000000..a65ce85cf0 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Rendering/ViewComponentHelperExtensions.cs @@ -0,0 +1,28 @@ + +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + public static class ViewComponentHelperExtensions + { + public static HtmlString Invoke([NotNull] this IViewComponentHelper helper, params object[] args) + { + return helper.Invoke(typeof(TComponent), args); + } + + public static void RenderInvoke([NotNull] this IViewComponentHelper helper, params object[] args) + { + helper.RenderInvoke(typeof(TComponent), args); + } + + public static async Task InvokeAsync([NotNull] this IViewComponentHelper helper, params object[] args) + { + return await helper.InvokeAsync(typeof(TComponent), args); + } + + public static async Task RenderInvokeAsync([NotNull] this IViewComponentHelper helper, params object[] args) + { + await helper.RenderInvokeAsync(typeof(TComponent), args); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index 8a3d185e73..3779e4f65c 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -68,6 +68,11 @@ namespace Microsoft.AspNet.Mvc yield return describe.Transient(); yield return describe.Transient(); + yield return describe.Transient(); + yield return describe.Transient(); + yield return describe.Transient, DefaultViewComponentInvokerProvider>(); + yield return describe.Transient(); + yield return describe.Describe( typeof(INestedProviderManager<>), diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionExecutorTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionExecutorTests.cs index 8eb5501458..217a963c33 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ActionExecutorTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionExecutorTests.cs @@ -36,7 +36,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test var result = await ReflectedActionExecutor.ExecuteAsync( methodWithVoidReturnType.GetMethodInfo(), null, - null); + (IDictionary)null); Assert.Same(null, result); }