From 8ce069f56ad76cad5c478b7509b4bb402647eb3d Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 26 Nov 2014 15:56:06 -0800 Subject: [PATCH] [Fixes #393] [Design] Update ViewComponent result apis to be consistent with Controller 1. Updated ViewComponent to exposes similar properties to the existing ones in controller where appropiate. We've left out Resolver for being a bad pattern (just inject the dependency on the constructor or use Context.RequestServices to access it if needed) and Response as although available through the Context property, it shouldn't be used/modified in a ViewComponent. 2. Updated ViewViewComponentResult to follow a similar pattern as ViewResult where the constructor is parameterless and elements like ViewEngine are resolved during execution if the user does not set the associated property on the object. 3. Updated ExecuteAsync in JsonViewComponentResult to remove the unnecessary pragma and async keyword from the signature and to use Task.FromResult(true) instead. 4. Cleaned up ViewViewComponentResult tests. --- .../ContentViewComponentResult.cs | 42 ++++- .../ViewComponents/JsonViewComponentResult.cs | 57 +++++- .../ViewComponents/ViewComponent.cs | 113 +++++++++++- .../ViewComponents/ViewViewComponentResult.cs | 55 +++--- .../DefaultViewComponentActivatorTests.cs | 3 + .../ViewComponentTests.cs | 2 +- .../ContentViewComponentResultTest.cs | 63 +++++++ .../JsonViewComponentResultTest.cs | 103 +++++++++++ .../ViewViewComponentResultTest.cs | 168 ++++++++++++++++-- 9 files changed, 562 insertions(+), 44 deletions(-) create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/ViewComponents/ContentViewComponentResultTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/ViewComponents/JsonViewComponentResultTest.cs diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ContentViewComponentResult.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ContentViewComponentResult.cs index 4b2efddf57..4ef89e3ce8 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ContentViewComponentResult.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ContentViewComponentResult.cs @@ -7,29 +7,67 @@ using Microsoft.AspNet.Mvc.Rendering; namespace Microsoft.AspNet.Mvc { + /// + /// An which writes text when executed. + /// + /// + /// always writes HTML encoded text from the + /// property. + /// + /// When using , the provided content will be HTML + /// encoded and stored in . + /// + /// To write pre-encoded conent, use . + /// public class ContentViewComponentResult : IViewComponentResult { + /// + /// Initializes a new . + /// + /// Content to write. The content be HTML encoded when output. public ContentViewComponentResult([NotNull] string content) { Content = content; EncodedContent = new HtmlString(WebUtility.HtmlEncode(content)); } + /// + /// Initializes a new . + /// + /// + /// Content to write. The content is treated as already HTML encoded, and no further encoding + /// will be performed. + /// public ContentViewComponentResult([NotNull] HtmlString encodedContent) { EncodedContent = encodedContent; Content = WebUtility.HtmlDecode(encodedContent.ToString()); } - public string Content { get; private set; } + /// + /// Gets the content. + /// + public string Content { get; } - public HtmlString EncodedContent { get; private set; } + /// + /// Gets the encoded content. + /// + public HtmlString EncodedContent { get; } + /// + /// Writes the . + /// + /// The . public void Execute([NotNull] ViewComponentContext context) { context.Writer.Write(EncodedContent.ToString()); } + /// + /// Writes the . + /// + /// The . + /// A completed . public async Task ExecuteAsync([NotNull] ViewComponentContext context) { await context.Writer.WriteAsync(EncodedContent.ToString()); diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/JsonViewComponentResult.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/JsonViewComponentResult.cs index be3e34785b..ecf02ec6d5 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/JsonViewComponentResult.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/JsonViewComponentResult.cs @@ -2,29 +2,70 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Threading.Tasks; +using Microsoft.Framework.DependencyInjection; namespace Microsoft.AspNet.Mvc { + /// + /// An which renders JSON text when executed. + /// public class JsonViewComponentResult : IViewComponentResult { - public JsonViewComponentResult([NotNull] object data) + /// + /// Initializes a new . + /// + /// The value to format as JSON text. + public JsonViewComponentResult(object value) { - Data = data; + Value = value; } - public object Data { get; private set; } + /// + /// Initializes a new . + /// + /// The value to format as JSON text. + /// The to use. + public JsonViewComponentResult(object value, JsonOutputFormatter formatter) + { + Value = value; + Formatter = formatter; + } + /// + /// Gets the value. + /// + public object Value { get; } + + /// + /// Gets the formatter. + /// + public JsonOutputFormatter Formatter { get; } + + /// + /// Renders JSON text to the output. + /// + /// The . public void Execute([NotNull] ViewComponentContext context) { - var formatter = new JsonOutputFormatter(); - formatter.WriteObject(context.Writer, Data); + var formatter = Formatter ?? ResolveFormatter(context); + formatter.WriteObject(context.Writer, Value); } -#pragma warning disable 1998 - public async Task ExecuteAsync([NotNull] ViewComponentContext context) + /// + /// Renders JSON text to the output. + /// + /// The . + /// A completed . + public Task ExecuteAsync([NotNull] ViewComponentContext context) { Execute(context); + return Task.FromResult(true); + } + + private static JsonOutputFormatter ResolveFormatter(ViewComponentContext context) + { + var services = context.ViewContext.HttpContext.RequestServices; + return services.GetRequiredService(); } -#pragma warning restore 1998 } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponent.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponent.cs index 01cdbabeb5..3b0f8ff59c 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponent.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewComponent.cs @@ -1,21 +1,69 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Security.Principal; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.Routing; namespace Microsoft.AspNet.Mvc { + /// + /// A base class for view components. + /// [ViewComponent] public abstract class ViewComponent { private dynamic _viewBag; + /// + /// Gets the . + /// public HttpContext Context { - get { return ViewContext == null ? null : ViewContext.HttpContext; } + get + { + return ViewContext?.HttpContext; + } } + /// + /// Gets the . + /// + public HttpRequest Request + { + get + { + return ViewContext?.HttpContext?.Request; + } + } + + /// + /// Gets the for the current user. + /// + public IPrincipal User + { + get + { + return ViewContext?.HttpContext?.User; + } + } + + /// + /// Gets the for the current request. + /// + public RouteData RouteData + { + get + { + return ViewContext?.RouteData; + } + } + + /// + /// Gets the view bag. + /// public dynamic ViewBag { get @@ -29,44 +77,105 @@ namespace Microsoft.AspNet.Mvc } } + /// + /// Gets the . + /// + public ModelStateDictionary ModelState + { + get + { + return ViewData?.ModelState; + } + } + + /// + /// Gets or sets the . + /// + [Activate] + public IUrlHelper Url { get; set; } + + /// + /// Gets or sets the . + /// [Activate] public ViewContext ViewContext { get; set; } + /// + /// Gets or sets the . + /// [Activate] public ViewDataDictionary ViewData { get; set; } + /// + /// Gets or sets the . + /// [Activate] public ICompositeViewEngine ViewEngine { get; set; } + /// + /// Returns a result which will render HTML encoded text. + /// + /// The content, will be HTML encoded before output. + /// A . public ContentViewComponentResult Content([NotNull] string content) { return new ContentViewComponentResult(content); } + /// + /// Returns a result which will render JSON text. + /// + /// The value to output in JSON text. + /// A . public JsonViewComponentResult Json([NotNull] object value) { return new JsonViewComponentResult(value); } + /// + /// Returns a result which will render the partial view with name "Default". + /// + /// A . public ViewViewComponentResult View() { return View(null, null); } + /// + /// Returns a result which will render the partial view with name . + /// + /// The name of the partial view to render. + /// A . public ViewViewComponentResult View(string viewName) { return View(viewName, null); } + /// + /// Returns a result which will render the partial view with name "Default". + /// + /// The model object for the view. + /// A . public ViewViewComponentResult View(TModel model) { return View(null, model); } + /// + /// Returns a result which will render the partial view with name . + /// + /// The name of the partial view to render. + /// The model object for the view. + /// A . public ViewViewComponentResult View(string viewName, TModel model) { var viewData = new ViewDataDictionary(ViewData, model); - return new ViewViewComponentResult(ViewEngine, viewName, viewData); + return new ViewViewComponentResult + { + ViewEngine = ViewEngine, + ViewName = viewName, + ViewData = viewData + }; } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewViewComponentResult.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewViewComponentResult.cs index 5cbf81f539..b4ba3ad77c 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewViewComponentResult.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewViewComponentResult.cs @@ -5,6 +5,7 @@ using System; using System.Globalization; using System.Threading.Tasks; using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.Framework.DependencyInjection; namespace Microsoft.AspNet.Mvc { @@ -15,22 +16,25 @@ namespace Microsoft.AspNet.Mvc { // {0} is the component name, {1} is the view name. private const string ViewPathFormat = "Components/{0}/{1}"; - private readonly IViewEngine _viewEngine; - - public ViewViewComponentResult([NotNull] IViewEngine viewEngine, string viewName, - ViewDataDictionary viewData) - { - _viewEngine = viewEngine; - ViewName = viewName; - ViewData = viewData; - } - - public string ViewName { get; private set; } - - public ViewDataDictionary ViewData { get; private set; } /// - /// Locates and renders a view specified by . + /// Gets or sets the view name. + /// + public string ViewName { get; set; } + + /// + /// Gets or sets the . + /// + public ViewDataDictionary ViewData { get; set; } + + /// + /// Gets or sets the . + /// + public IViewEngine ViewEngine { get; set; } + + /// + /// Locates and renders a view specified by . If is null, + /// then the view name searched for is"Default". /// /// The for the current component execution. /// @@ -42,9 +46,17 @@ namespace Microsoft.AspNet.Mvc TaskHelper.WaitAndThrowIfFaulted(task); } - /// + /// + /// Locates and renders a view specified by . If is null, + /// then the view name searched for is"Default". + /// + /// The for the current component execution. + /// A which will complete when view rendering is completed. public async Task ExecuteAsync([NotNull] ViewComponentContext context) { + var viewEngine = ViewEngine ?? ResolveViewEngine(context); + var viewData = ViewData ?? context.ViewContext.ViewData; + string qualifiedViewName; if (ViewName != null && ViewName.Length > 0 && ViewName[0] == '/') { @@ -71,7 +83,7 @@ namespace Microsoft.AspNet.Mvc ViewName ?? "Default"); } - var view = FindView(context.ViewContext, qualifiedViewName); + var view = FindView(context.ViewContext, viewEngine, qualifiedViewName); var childViewContext = new ViewContext( context.ViewContext, @@ -85,11 +97,14 @@ namespace Microsoft.AspNet.Mvc } } - private IView FindView(ActionContext context, string viewName) + private static IView FindView(ActionContext context, IViewEngine viewEngine, string viewName) { - return _viewEngine.FindPartialView(context, viewName) - .EnsureSuccessful() - .View; + return viewEngine.FindPartialView(context, viewName).EnsureSuccessful().View; + } + + private static IViewEngine ResolveViewEngine(ViewComponentContext context) + { + return context.ViewContext.HttpContext.RequestServices.GetRequiredService(); } } } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultViewComponentActivatorTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultViewComponentActivatorTests.cs index fbfdae17fb..3c51575d2b 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultViewComponentActivatorTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultViewComponentActivatorTests.cs @@ -26,6 +26,7 @@ namespace Microsoft.AspNet.Mvc var serviceProvider = new Mock(); serviceProvider.Setup(p => p.GetService(typeof(IHtmlHelper))).Returns(helper); serviceProvider.Setup(p => p.GetService(typeof(ICompositeViewEngine))).Returns(Mock.Of()); + serviceProvider.Setup(p => p.GetService(typeof(IUrlHelper))).Returns(Mock.Of()); var viewContext = GetViewContext(serviceProvider.Object); // Act @@ -46,6 +47,7 @@ namespace Microsoft.AspNet.Mvc var serviceProvider = new Mock(); serviceProvider.Setup(p => p.GetService(typeof(IHtmlHelper))).Returns(helper); serviceProvider.Setup(p => p.GetService(typeof(ICompositeViewEngine))).Returns(Mock.Of()); + serviceProvider.Setup(p => p.GetService(typeof(IUrlHelper))).Returns(Mock.Of()); var viewContext = GetViewContext(serviceProvider.Object); // Act @@ -66,6 +68,7 @@ namespace Microsoft.AspNet.Mvc serviceProvider.Setup(p => p.GetService(typeof(IHtmlHelper))).Returns(helper); serviceProvider.Setup(p => p.GetService(typeof(MyService))).Returns(myTestService); serviceProvider.Setup(p => p.GetService(typeof(ICompositeViewEngine))).Returns(Mock.Of()); + serviceProvider.Setup(p => p.GetService(typeof(IUrlHelper))).Returns(Mock.Of()); var viewContext = GetViewContext(serviceProvider.Object); var instance = new TestViewComponentWithCustomDataType(); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ViewComponentTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ViewComponentTests.cs index df8a502b7e..74f008fc78 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ViewComponentTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ViewComponentTests.cs @@ -77,7 +77,7 @@ namespace Microsoft.AspNet.Mvc // Assert Assert.IsType(actualResult); - Assert.Same(testData, actualResult.Data); + Assert.Same(testData, actualResult.Value); } [Fact] diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ViewComponents/ContentViewComponentResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ViewComponents/ContentViewComponentResultTest.cs new file mode 100644 index 0000000000..3ba1ca035a --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ViewComponents/ContentViewComponentResultTest.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Reflection; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.PipelineCore; +using Microsoft.AspNet.Routing; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc +{ + public class ContentViewComponentResultTest + { + [Fact] + public void Execute_WritesData_Encoded() + { + // Arrange + var buffer = new MemoryStream(); + var result = new ContentViewComponentResult(""); + + var viewComponentContext = GetViewComponentContext(Mock.Of(), buffer); + + // Act + result.Execute(viewComponentContext); + buffer.Position = 0; + + // Assert + Assert.Equal("<Test />", result.EncodedContent.ToString()); + Assert.Equal("<Test />", new StreamReader(buffer).ReadToEnd()); + } + + [Fact] + public void Execute_WritesData_PreEncoded() + { + // Arrange + var buffer = new MemoryStream(); + var viewComponentContext = GetViewComponentContext(Mock.Of(), buffer); + + var result = new ContentViewComponentResult(new HtmlString("")); + + // Act + result.Execute(viewComponentContext); + buffer.Position = 0; + + // Assert + Assert.Equal("", result.Content); + Assert.Equal("", new StreamReader(buffer).ReadToEnd()); + } + + private static ViewComponentContext GetViewComponentContext(IView view, Stream stream) + { + var actionContext = new ActionContext(new RouteContext(new DefaultHttpContext()), new ActionDescriptor()); + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); + var viewContext = new ViewContext(actionContext, view, viewData, TextWriter.Null); + var writer = new StreamWriter(stream) { AutoFlush = true }; + var viewComponentContext = new ViewComponentContext(typeof(object).GetTypeInfo(), viewContext, writer); + return viewComponentContext; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ViewComponents/JsonViewComponentResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ViewComponents/JsonViewComponentResultTest.cs new file mode 100644 index 0000000000..b4561bf7e4 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ViewComponents/JsonViewComponentResultTest.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.IO; +using System.Reflection; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.PipelineCore; +using Microsoft.AspNet.Routing; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.DependencyInjection.Fallback; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc +{ + public class JsonViewComponentResultTest + { + [Fact] + public void Execute_SerializesData_UsingSpecifiedFormatter() + { + // Arrange + var view = Mock.Of(); + var buffer = new MemoryStream(); + var viewComponentContext = GetViewComponentContext(view, buffer); + + var expectedFormatter = new JsonOutputFormatter(); + var result = new JsonViewComponentResult(1, expectedFormatter); + + // Act + result.Execute(viewComponentContext); + buffer.Position = 0; + + // Assert + Assert.Equal(expectedFormatter, result.Formatter); + Assert.Equal("1", new StreamReader(buffer).ReadToEnd()); + } + + [Fact] + public void Execute_FallsbackToServices_WhenNoJsonFormatterIsProvided() + { + // Arrange + var view = Mock.Of(); + + var serviceProvider = new Mock(); + + serviceProvider + .Setup(p => p.GetService(typeof(JsonOutputFormatter))) + .Returns(new JsonOutputFormatter()) + .Verifiable(); + + var buffer = new MemoryStream(); + + var result = new JsonViewComponentResult(1); + var viewComponentContext = GetViewComponentContext(view, buffer); + viewComponentContext.ViewContext.HttpContext.RequestServices = serviceProvider.Object; + + // Act + result.Execute(viewComponentContext); + buffer.Position = 0; + + // Assert + Assert.Equal("1", new StreamReader(buffer).ReadToEnd()); + serviceProvider.Verify(); + } + + [Fact] + public void Execute_Throws_IfNoFormatterCanBeResolved() + { + // Arrange + var expected = "TODO: No service for type 'Microsoft.AspNet.Mvc.JsonOutputFormatter'" + + " has been registered."; + + var view = Mock.Of(); + + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + + var buffer = new MemoryStream(); + + var result = new JsonViewComponentResult(1); + var viewComponentContext = GetViewComponentContext(view, buffer); + viewComponentContext.ViewContext.HttpContext.RequestServices = serviceProvider; + + // Act + var ex = Assert.Throws(() => result.Execute(viewComponentContext)); + + // Assert + Assert.Equal(expected, ex.Message); + } + + private static ViewComponentContext GetViewComponentContext(IView view, Stream stream) + { + var actionContext = new ActionContext(new RouteContext(new DefaultHttpContext()), new ActionDescriptor()); + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); + var viewContext = new ViewContext(actionContext, view, viewData, TextWriter.Null); + var writer = new StreamWriter(stream) { AutoFlush = true }; + var viewComponentContext = new ViewComponentContext(typeof(object).GetTypeInfo(), viewContext, writer); + return viewComponentContext; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ViewComponents/ViewViewComponentResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ViewComponents/ViewViewComponentResultTest.cs index 716642416e..c673e031f1 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ViewComponents/ViewViewComponentResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ViewComponents/ViewViewComponentResultTest.cs @@ -9,6 +9,8 @@ using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.PipelineCore; using Microsoft.AspNet.Routing; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.DependencyInjection.Fallback; using Moq; using Xunit; @@ -24,12 +26,53 @@ namespace Microsoft.AspNet.Mvc view.Setup(v => v.RenderAsync(It.IsAny())) .Returns(Task.FromResult(result: true)) .Verifiable(); + var viewEngine = new Mock(MockBehavior.Strict); viewEngine.Setup(e => e.FindPartialView(It.IsAny(), It.IsAny())) .Returns(ViewEngineResult.Found("some-view", view.Object)) .Verifiable(); + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); - var result = new ViewViewComponentResult(viewEngine.Object, "some-view", viewData); + + var result = new ViewViewComponentResult + { + ViewEngine = viewEngine.Object, + ViewName = "some-view", + ViewData = viewData + }; + + var viewComponentContext = GetViewComponentContext(view.Object, viewData); + + // Act + result.Execute(viewComponentContext); + + // Assert + viewEngine.Verify(); + view.Verify(); + } + + [Fact] + public void Execute_ResolvesView_WithDefaultAsViewName() + { + // Arrange + var view = new Mock(MockBehavior.Strict); + view.Setup(v => v.RenderAsync(It.IsAny())) + .Returns(Task.FromResult(result: true)) + .Verifiable(); + + var viewEngine = new Mock(MockBehavior.Strict); + viewEngine.Setup(e => e.FindPartialView(It.IsAny(), It.IsAny())) + .Returns(ViewEngineResult.Found("Default", view.Object)) + .Verifiable(); + + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); + + var result = new ViewViewComponentResult + { + ViewEngine = viewEngine.Object, + ViewData = viewData + }; + var viewComponentContext = GetViewComponentContext(view.Object, viewData); // Act @@ -48,13 +91,23 @@ namespace Microsoft.AspNet.Mvc "The view 'Components/Object/some-view' was not found. The following locations were searched:", "location1", "location2."); + var view = Mock.Of(); + var viewEngine = new Mock(MockBehavior.Strict); viewEngine.Setup(e => e.FindPartialView(It.IsAny(), It.IsAny())) .Returns(ViewEngineResult.NotFound("Components/Object/some-view", new[] { "location1", "location2" })) .Verifiable(); + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); - var result = new ViewViewComponentResult(viewEngine.Object, "some-view", viewData); + + var result = new ViewViewComponentResult + { + ViewEngine = viewEngine.Object, + ViewName = "some-view", + ViewData = viewData + }; + var viewComponentContext = GetViewComponentContext(view, viewData); // Act and Assert @@ -67,16 +120,26 @@ namespace Microsoft.AspNet.Mvc { // Arrange var expected = new IndexOutOfRangeException(); + var view = new Mock(); view.Setup(v => v.RenderAsync(It.IsAny())) .Throws(expected) .Verifiable(); + var viewEngine = new Mock(MockBehavior.Strict); viewEngine.Setup(e => e.FindPartialView(It.IsAny(), It.IsAny())) .Returns(ViewEngineResult.Found("some-view", view.Object)) .Verifiable(); + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); - var result = new ViewViewComponentResult(viewEngine.Object, "some-view", viewData); + + var result = new ViewViewComponentResult + { + ViewEngine = viewEngine.Object, + ViewName = "some-view", + ViewData = viewData + }; + var viewComponentContext = GetViewComponentContext(view.Object, viewData); // Act @@ -92,12 +155,21 @@ namespace Microsoft.AspNet.Mvc { // Arrange var view = Mock.Of(); + var viewEngine = new Mock(MockBehavior.Strict); viewEngine.Setup(e => e.FindPartialView(It.IsAny(), It.IsAny())) .Returns(ViewEngineResult.Found("some-view", view)) .Verifiable(); + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); - var result = new ViewViewComponentResult(viewEngine.Object, "some-view", viewData); + + var result = new ViewViewComponentResult + { + ViewEngine = viewEngine.Object, + ViewName = "some-view", + ViewData = viewData + }; + var viewComponentContext = GetViewComponentContext(view, viewData); // Act @@ -107,26 +179,100 @@ namespace Microsoft.AspNet.Mvc viewEngine.Verify(); } + [Fact] + public async Task ExecuteAsync_ResolvesViewEngineFromServiceProvider_IfNoViewEngineIsExplicitlyProvided() + { + // Arrange + var view = Mock.Of(); + + var viewEngine = new Mock(MockBehavior.Strict); + viewEngine.Setup(e => e.FindPartialView(It.IsAny(), It.IsAny())) + .Returns(ViewEngineResult.Found("some-view", view)) + .Verifiable(); + + var serviceProvider = new Mock(); + serviceProvider.Setup(p => p.GetService(typeof(ICompositeViewEngine))) + .Returns(viewEngine.Object); + + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); + + var result = new ViewViewComponentResult + { + ViewName = "some-view", + ViewData = viewData + }; + + var viewComponentContext = GetViewComponentContext(view, viewData); + viewComponentContext.ViewContext.HttpContext.RequestServices = serviceProvider.Object; + + // Act + await result.ExecuteAsync(viewComponentContext); + + // Assert + viewEngine.Verify(); + serviceProvider.Verify(); + } + [Fact] public async Task ExecuteAsync_ThrowsIfPartialViewCannotBeFound() { // Arrange - var expected = -@"The view 'Components/Object/some-view' was not found. The following locations were searched: -foo -bar."; + var expected = string.Join(Environment.NewLine, + "The view 'Components/Object/some-view' was not found. The following locations were searched:", + "view-location1", + "view-location2."); + var view = Mock.Of(); + var viewEngine = new Mock(MockBehavior.Strict); viewEngine.Setup(e => e.FindPartialView(It.IsAny(), It.IsAny())) - .Returns(ViewEngineResult.NotFound("Components/Object/some-view", new[] { "foo", "bar" })) + .Returns(ViewEngineResult.NotFound( + "Components/Object/some-view", + new[] { "view-location1", "view-location2" })) .Verifiable(); + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); - var result = new ViewViewComponentResult(viewEngine.Object, "some-view", viewData); + + var result = new ViewViewComponentResult + { + ViewEngine = viewEngine.Object, + ViewName = "some-view", + ViewData = viewData + }; + var viewComponentContext = GetViewComponentContext(view, viewData); // Act and Assert var ex = await Assert.ThrowsAsync( - async () => await result.ExecuteAsync(viewComponentContext)); + () => result.ExecuteAsync(viewComponentContext)); + Assert.Equal(expected, ex.Message); + } + + [Fact] + public async Task ExecuteAsync_Throws_IfNoViewEngineCanBeResolved() + { + // Arrange + var expected = "TODO: No service for type 'Microsoft.AspNet.Mvc.Rendering.ICompositeViewEngine'" + + " has been registered."; + + var view = Mock.Of(); + + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); + + var result = new ViewViewComponentResult + { + ViewName = "some-view", + ViewData = viewData + }; + + var viewComponentContext = GetViewComponentContext(view, viewData); + viewComponentContext.ViewContext.HttpContext.RequestServices = serviceProvider; + + // Act and Assert + var ex = await Assert.ThrowsAsync( + () => result.ExecuteAsync(viewComponentContext)); Assert.Equal(expected, ex.Message); }