From de77c92a0a13f1ca27e5a6a7c6365b2679102fd6 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 8 Oct 2014 17:19:33 -0700 Subject: [PATCH] Reviving PartialViewResult and associated methods on Controller Fixes #824 --- .../ActionResults/PartialViewResult.cs | 21 ++ .../ActionResults/ViewResult.cs | 144 +---------- .../ActionResults/ViewResultBase.cs | 162 ++++++++++++ src/Microsoft.AspNet.Mvc.Core/Controller.cs | 56 ++++ .../ActionResults/PartialViewResultTest.cs | 41 +++ .../ActionResults/ViewResultBaseTest.cs | 243 ++++++++++++++++++ .../ActionResults/ViewResultTest.cs | 41 +++ .../ViewResultTest.cs | 196 -------------- .../ViewEngineTests.cs | 55 ++++ .../PartialViewEngineController.cs | 46 ++++ .../Controllers/ViewEngineController.cs | 6 + .../PartialViewEngine/PartialWithModel.cshtml | 3 + .../ViewWithDataFromController.cshtml | 1 + .../PartialViewEngine/ViewWithFullPath.cshtml | 4 + .../PartialViewEngine/ViewWithLayout.cshtml | 4 + .../ViewWithNestedLayout.cshtml | 4 + .../ViewWithoutLayout.cshtml | 1 + .../ViewWithDataFromController.cshtml | 1 + 18 files changed, 698 insertions(+), 331 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/ActionResults/PartialViewResult.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResultBase.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/PartialViewResultTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ViewResultBaseTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ViewResultTest.cs delete mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/ViewResultTest.cs create mode 100644 test/WebSites/RazorWebSite/Controllers/PartialViewEngineController.cs create mode 100644 test/WebSites/RazorWebSite/Views/PartialViewEngine/PartialWithModel.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/PartialViewEngine/ViewWithDataFromController.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/PartialViewEngine/ViewWithFullPath.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/PartialViewEngine/ViewWithLayout.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/PartialViewEngine/ViewWithNestedLayout.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/PartialViewEngine/ViewWithoutLayout.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithDataFromController.cshtml diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/PartialViewResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/PartialViewResult.cs new file mode 100644 index 0000000000..96dfc7b77b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/PartialViewResult.cs @@ -0,0 +1,21 @@ +// 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 Microsoft.AspNet.Mvc.Rendering; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Represents a that renders a partial view. + /// + public class PartialViewResult : ViewResultBase + { + /// + protected override ViewEngineResult FindView([NotNull] IViewEngine viewEngine, + [NotNull] ActionContext context, + [NotNull] string viewName) + { + return viewEngine.FindPartialView(context, viewName); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs index cca67cabf4..efc9d002e9 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs @@ -1,147 +1,21 @@ // 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.IO; -using System.Threading.Tasks; -using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Rendering; -using Microsoft.Framework.DependencyInjection; namespace Microsoft.AspNet.Mvc { - public class ViewResult : ActionResult + /// + /// Represents a that renders a view to the response. + /// + public class ViewResult : ViewResultBase { - private const int BufferSize = 1024; - - public string ViewName { get; set; } - - public ViewDataDictionary ViewData { get; set; } - - public IViewEngine ViewEngine { get; set; } - - public override async Task ExecuteResultAsync([NotNull] ActionContext context) + /// + protected override ViewEngineResult FindView([NotNull] IViewEngine viewEngine, + [NotNull] ActionContext context, + [NotNull] string viewName) { - var viewEngine = ViewEngine ?? context.HttpContext.RequestServices.GetService(); - - var viewName = ViewName ?? context.ActionDescriptor.Name; - var view = FindView(viewEngine, context, viewName); - - using (view as IDisposable) - { - context.HttpContext.Response.ContentType = "text/html; charset=utf-8"; - var wrappedStream = new StreamWrapper(context.HttpContext.Response.Body); - var encoding = Encodings.UTF8EncodingWithoutBOM; - using (var writer = new StreamWriter(wrappedStream, encoding, BufferSize, leaveOpen: true)) - { - try - { - var viewContext = new ViewContext(context, view, ViewData, writer); - await view.RenderAsync(viewContext); - } - catch - { - // Need to prevent writes/flushes on dispose because the StreamWriter will flush even if - // nothing got written. This leads to a response going out on the wire prematurely in case an - // exception is being thrown inside the try catch block. - wrappedStream.BlockWrites = true; - throw; - } - } - } - } - - private static IView FindView(IViewEngine viewEngine, ActionContext context, string viewName) - { - var result = viewEngine.FindView(context, viewName); - if (!result.Success) - { - var locations = string.Empty; - if (result.SearchedLocations != null) - { - locations = Environment.NewLine + - string.Join(Environment.NewLine, result.SearchedLocations); - } - - throw new InvalidOperationException(Resources.FormatViewEngine_ViewNotFound(viewName, locations)); - } - - return result.View; - } - - private class StreamWrapper : Stream - { - private readonly Stream _wrappedStream; - - public StreamWrapper([NotNull] Stream stream) - { - _wrappedStream = stream; - } - - public bool BlockWrites { get; set; } - - public override bool CanRead - { - get { return _wrappedStream.CanRead; } - } - - public override bool CanSeek - { - get { return _wrappedStream.CanSeek; } - } - - public override bool CanWrite - { - get { return _wrappedStream.CanWrite; } - } - - public override void Flush() - { - if (!BlockWrites) - { - _wrappedStream.Flush(); - } - } - - public override long Length - { - get { return _wrappedStream.Length; } - } - - public override long Position - { - get - { - return _wrappedStream.Position; - } - set - { - _wrappedStream.Position = value; - } - } - - public override int Read(byte[] buffer, int offset, int count) - { - return _wrappedStream.Read(buffer, offset, count); - } - - public override long Seek(long offset, SeekOrigin origin) - { - return Seek(offset, origin); - } - - public override void SetLength(long value) - { - SetLength(value); - } - - public override void Write(byte[] buffer, int offset, int count) - { - if (!BlockWrites) - { - _wrappedStream.Write(buffer, offset, count); - } - } + return viewEngine.FindView(context, viewName); } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResultBase.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResultBase.cs new file mode 100644 index 0000000000..6aa04ead36 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResultBase.cs @@ -0,0 +1,162 @@ +// 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.IO; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.Core; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.Framework.DependencyInjection; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Represents the base type for an that renders a view to the response. + /// + public abstract class ViewResultBase : ActionResult + { + private const int BufferSize = 1024; + + public string ViewName { get; set; } + + public ViewDataDictionary ViewData { get; set; } + + public IViewEngine ViewEngine { get; set; } + + public override async Task ExecuteResultAsync([NotNull] ActionContext context) + { + var viewEngine = ViewEngine ?? context.HttpContext.RequestServices.GetService(); + + var viewName = ViewName ?? context.ActionDescriptor.Name; + var view = FindViewInternal(viewEngine, context, viewName); + + using (view as IDisposable) + { + context.HttpContext.Response.ContentType = "text/html; charset=utf-8"; + var wrappedStream = new StreamWrapper(context.HttpContext.Response.Body); + var encoding = Encodings.UTF8EncodingWithoutBOM; + using (var writer = new StreamWriter(wrappedStream, encoding, BufferSize, leaveOpen: true)) + { + try + { + var viewContext = new ViewContext(context, view, ViewData, writer); + await view.RenderAsync(viewContext); + } + catch + { + // Need to prevent writes/flushes on dispose because the StreamWriter will flush even if + // nothing got written. This leads to a response going out on the wire prematurely in case an + // exception is being thrown inside the try catch block. + wrappedStream.BlockWrites = true; + throw; + } + } + } + } + + /// + /// Attempts to locate the view named using the specified + /// . + /// + /// The used to locate the view. + /// The for the executing action. + /// The view to find. + /// + protected abstract ViewEngineResult FindView(IViewEngine viewEngine, + ActionContext context, + string viewName); + + private IView FindViewInternal(IViewEngine viewEngine, ActionContext context, string viewName) + { + var result = FindView(viewEngine, context, viewName); + if (!result.Success) + { + var locations = string.Empty; + if (result.SearchedLocations != null) + { + locations = Environment.NewLine + + string.Join(Environment.NewLine, result.SearchedLocations); + } + + throw new InvalidOperationException(Resources.FormatViewEngine_ViewNotFound(viewName, locations)); + } + + return result.View; + } + + private class StreamWrapper : Stream + { + private readonly Stream _wrappedStream; + + public StreamWrapper([NotNull] Stream stream) + { + _wrappedStream = stream; + } + + public bool BlockWrites { get; set; } + + public override bool CanRead + { + get { return _wrappedStream.CanRead; } + } + + public override bool CanSeek + { + get { return _wrappedStream.CanSeek; } + } + + public override bool CanWrite + { + get { return _wrappedStream.CanWrite; } + } + + public override void Flush() + { + if (!BlockWrites) + { + _wrappedStream.Flush(); + } + } + + public override long Length + { + get { return _wrappedStream.Length; } + } + + public override long Position + { + get + { + return _wrappedStream.Position; + } + set + { + _wrappedStream.Position = value; + } + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _wrappedStream.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return Seek(offset, origin); + } + + public override void SetLength(long value) + { + SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (!BlockWrites) + { + _wrappedStream.Write(buffer, offset, count); + } + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Controller.cs b/src/Microsoft.AspNet.Mvc.Core/Controller.cs index e7179c9d25..ca9e280a1c 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Controller.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Controller.cs @@ -187,6 +187,62 @@ namespace Microsoft.AspNet.Mvc }; } + /// + /// Creates a object that renders a partial view to the response. + /// + /// The created object for the response. + [NonAction] + public virtual PartialViewResult PartialView() + { + return PartialView(viewName: null); + } + + /// + /// Creates a object by specifying a . + /// + /// The name of the view that is rendered to the response. + /// The created object for the response. + [NonAction] + public virtual PartialViewResult PartialView(string viewName) + { + return PartialView(viewName, model: null); + } + + /// + /// Creates a object by specifying a + /// to be rendered by the partial view. + /// + /// The model that is rendered by the partial view. + /// The created object for the response. + [NonAction] + public virtual PartialViewResult PartialView(object model) + { + return PartialView(viewName: null, model: model); + } + + /// + /// Creates a object by specifying a + /// and the to be rendered by the partial view. + /// + /// The name of the partial view that is rendered to the response. + /// The model that is rendered by the partial view. + /// The created object for the response. + [NonAction] + public virtual PartialViewResult PartialView(string viewName, object model) + { + // Do not override ViewData.Model unless passed a non-null value. + if (model != null) + { + ViewData.Model = model; + } + + return new PartialViewResult() + { + ViewName = viewName, + ViewData = ViewData, + }; + } + /// /// Creates a object by specifying a string. /// diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/PartialViewResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/PartialViewResultTest.cs new file mode 100644 index 0000000000..c2ca9d11ea --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/PartialViewResultTest.cs @@ -0,0 +1,41 @@ +// 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.Threading.Tasks; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.PipelineCore; +using Microsoft.AspNet.Routing; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Core +{ + public class PartialViewResultTest + { + [Fact] + public async Task PartialViewResult_UsesFindViewOnSpecifiedViewEngineToLocateViews() + { + // Arrange + var viewName = "mypartialview"; + var context = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); + var viewEngine = new Mock(); + var view = Mock.Of(); + + viewEngine.Setup(e => e.FindPartialView(context, viewName)) + .Returns(ViewEngineResult.Found(viewName, view)) + .Verifiable(); + + var viewResult = new PartialViewResult + { + ViewEngine = viewEngine.Object, + ViewName = viewName + }; + + // Act + await viewResult.ExecuteResultAsync(context); + + // Assert + viewEngine.Verify(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ViewResultBaseTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ViewResultBaseTest.cs new file mode 100644 index 0000000000..1755e1ee45 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ViewResultBaseTest.cs @@ -0,0 +1,243 @@ +// 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.Threading.Tasks; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.PipelineCore; +using Microsoft.AspNet.Routing; +using Moq; +using Moq.Protected; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Core +{ + public class ViewResultBaseTest + { + // The buffer size of the StreamWriter used in ViewResult. + private const int ViewResultStreamWriterBufferSize = 1024; + + [Fact] + public async Task ExecuteResultAsync_ReturnsError_IfViewCouldNotBeFound() + { + // Arrange + var expected = string.Join(Environment.NewLine, + "The view 'MyView' was not found. The following locations were searched:", + "Location1", + "Location2."); + var actionContext = new ActionContext(new DefaultHttpContext(), + new RouteData(), + new ActionDescriptor()); + var viewEngine = Mock.Of(); + + var mockResult = new Mock { CallBase = true }; + mockResult.Protected() + .Setup("FindView", viewEngine, actionContext, ItExpr.IsAny()) + .Returns(ViewEngineResult.NotFound("MyView", new[] { "Location1", "Location2" })) + .Verifiable(); + + var viewResult = mockResult.Object; + viewResult.ViewEngine = viewEngine; + viewResult.ViewName = "MyView"; + + // Act and Assert + var ex = await Assert.ThrowsAsync( + () => viewResult.ExecuteResultAsync(actionContext)); + Assert.Equal(expected, ex.Message); + mockResult.Verify(); + } + + [Fact] + public async Task ExecuteResultAsync_UsesActionDescriptorName_IfViewNameIsNullOrEmpty() + { + // Arrange + var viewName = "some-view-name"; + var actionContext = new ActionContext(new DefaultHttpContext(), + new RouteData(), + new ActionDescriptor { Name = viewName }); + var viewEngine = Mock.Of(); + + var mockResult = new Mock { CallBase = true }; + mockResult.Protected() + .Setup("FindView", viewEngine, actionContext, viewName) + .Returns(ViewEngineResult.Found(viewName, Mock.Of())) + .Verifiable(); + + var viewResult = mockResult.Object; + viewResult.ViewEngine = viewEngine; + + // Act + await viewResult.ExecuteResultAsync(actionContext); + + // Assert + mockResult.Verify(); + } + + [Fact] + public async Task ExecuteResultAsync_WritesOutputWithoutBOM() + { + // Arrange + var expected = new byte[] { 97, 98, 99, 100 }; + + var view = new Mock(); + view.Setup(v => v.RenderAsync(It.IsAny())) + .Callback((ViewContext v) => + { + view.ToString(); + v.Writer.Write("abcd"); + }) + .Returns(Task.FromResult(0)); + + var viewEngine = Mock.Of(); + var routeDictionary = new Dictionary(); + var serviceProvider = new Mock(); + serviceProvider.Setup(sp => sp.GetService(typeof(ICompositeViewEngine))) + .Returns(viewEngine); + + var context = new DefaultHttpContext + { + RequestServices = serviceProvider.Object, + }; + var memoryStream = new MemoryStream(); + context.Response.Body = memoryStream; + + var actionContext = new ActionContext(context, + new RouteData { Values = routeDictionary }, + new ActionDescriptor()); + + var viewResult = new Mock { CallBase = true }; + + + var result = new Mock { CallBase = true }; + result.Protected() + .Setup("FindView", viewEngine, actionContext, ItExpr.IsAny()) + .Returns(ViewEngineResult.Found("MyView", view.Object)); + + // Act + await result.Object.ExecuteResultAsync(actionContext); + + // Assert + Assert.Equal(expected, memoryStream.ToArray()); + } + + [Fact] + public async Task ExecuteResultAsync_UsesProvidedViewEngine() + { + // Arrange + var expected = new byte[] { 97, 98, 99, 100 }; + + var view = new Mock(); + view.Setup(v => v.RenderAsync(It.IsAny())) + .Callback((ViewContext v) => + { + view.ToString(); + v.Writer.Write("abcd"); + }) + .Returns(Task.FromResult(0)); + + var routeDictionary = new Dictionary(); + + var goodViewEngine = Mock.Of(); + var badViewEngine = new Mock(MockBehavior.Strict); + + var serviceProvider = new Mock(); + serviceProvider.Setup(sp => sp.GetService(typeof(ICompositeViewEngine))) + .Returns(badViewEngine.Object); + + var context = new DefaultHttpContext + { + RequestServices = serviceProvider.Object, + }; + var memoryStream = new MemoryStream(); + context.Response.Body = memoryStream; + + var actionContext = new ActionContext(context, + new RouteData { Values = routeDictionary }, + new ActionDescriptor()); + + var result = new Mock { CallBase = true }; + result.Protected() + .Setup("FindView", goodViewEngine, actionContext, ItExpr.IsAny()) + .Returns(ViewEngineResult.Found("MyView", view.Object)) + .Verifiable(); + + result.Object.ViewEngine = goodViewEngine; + + // Act + await result.Object.ExecuteResultAsync(actionContext); + + // Assert + Assert.Equal(expected, memoryStream.ToArray()); + result.Verify(); + } + + public static IEnumerable ExecuteResultAsync_DoesNotWriteToResponse_OnceExceptionIsThrownData + { + get + { + yield return new object[] { 30, 0 }; + + if (PlatformHelper.IsMono) + { + // The StreamWriter in Mono buffers 2x the buffer size before flushing. + yield return new object[] { ViewResultStreamWriterBufferSize * 2 + 30, ViewResultStreamWriterBufferSize }; + } + else + { + yield return new object[] { ViewResultStreamWriterBufferSize + 30, ViewResultStreamWriterBufferSize }; + } + } + } + + // The StreamWriter used by ViewResult an internal buffer and consequently anything written to this buffer + // prior to it filling up will not be written to the underlying stream once an exception is thrown. + [Theory] + [MemberData(nameof(ExecuteResultAsync_DoesNotWriteToResponse_OnceExceptionIsThrownData))] + public async Task ExecuteResultAsync_DoesNotWriteToResponse_OnceExceptionIsThrown(int writtenLength, int expectedLength) + { + // Arrange + var longString = new string('a', writtenLength); + + var routeDictionary = new Dictionary(); + + var view = new Mock(); + view.Setup(v => v.RenderAsync(It.IsAny())) + .Callback((ViewContext v) => + { + view.ToString(); + v.Writer.Write(longString); + throw new Exception(); + }); + + var viewEngine = Mock.Of(); + var serviceProvider = new Mock(); + serviceProvider.Setup(sp => sp.GetService(typeof(ICompositeViewEngine))) + .Returns(viewEngine); + + var context = new DefaultHttpContext + { + RequestServices = serviceProvider.Object, + }; + var memoryStream = new MemoryStream(); + context.Response.Body = memoryStream; + + var actionContext = new ActionContext(context, + new RouteData { Values = routeDictionary }, + new ActionDescriptor()); + + var result = new Mock { CallBase = true }; + result.Protected() + .Setup("FindView", viewEngine, actionContext, ItExpr.IsAny()) + .Returns(ViewEngineResult.Found("MyView", view.Object)) + .Verifiable(); + + // Act + await Record.ExceptionAsync(() => result.Object.ExecuteResultAsync(actionContext)); + + // Assert + Assert.Equal(expectedLength, memoryStream.Length); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ViewResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ViewResultTest.cs new file mode 100644 index 0000000000..cb19408a96 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ViewResultTest.cs @@ -0,0 +1,41 @@ +// 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.Threading.Tasks; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.PipelineCore; +using Microsoft.AspNet.Routing; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Core +{ + public class ViewResultTest + { + [Fact] + public async Task ViewResult_UsesFindViewOnSpecifiedViewEngineToLocateViews() + { + // Arrange + var viewName = "myview"; + var context = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); + var viewEngine = new Mock(); + var view = Mock.Of(); + + viewEngine.Setup(e => e.FindView(context, "myview")) + .Returns(ViewEngineResult.Found("myview", view)) + .Verifiable(); + + var viewResult = new ViewResult + { + ViewName = viewName, + ViewEngine = viewEngine.Object + }; + + // Act + await viewResult.ExecuteResultAsync(context); + + // Assert + viewEngine.Verify(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ViewResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ViewResultTest.cs deleted file mode 100644 index 79ca3f9d17..0000000000 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ViewResultTest.cs +++ /dev/null @@ -1,196 +0,0 @@ -// 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.Threading.Tasks; -using Microsoft.AspNet.Http; -using Microsoft.AspNet.Mvc.Rendering; -using Microsoft.AspNet.Routing; -using Moq; -using Xunit; - -namespace Microsoft.AspNet.Mvc.Core.Test -{ - public class ViewResultTest - { - // The buffer size of the StreamWriter used in ViewResult. - private const int ViewResultStreamWriterBufferSize = 1024; - - [Fact] - public async Task ExecuteResultAsync_WritesOutputWithoutBOM() - { - // Arrange - var expected = new byte[] { 97, 98, 99, 100 }; - - var view = new Mock(); - view.Setup(v => v.RenderAsync(It.IsAny())) - .Callback((ViewContext v) => - { - view.ToString(); - v.Writer.Write("abcd"); - }) - .Returns(Task.FromResult(0)); - - var routeDictionary = new Dictionary(); - - var viewEngine = new Mock(); - - var serviceProvider = new Mock(); - serviceProvider.Setup(sp => sp.GetService(typeof(ICompositeViewEngine))) - .Returns(viewEngine.Object); - - var memoryStream = new MemoryStream(); - var response = new Mock(); - response.SetupGet(r => r.Body) - .Returns(memoryStream); - - var context = new Mock(); - context.SetupGet(c => c.Response) - .Returns(response.Object); - context.SetupGet(c => c.RequestServices) - .Returns(serviceProvider.Object); - - var actionContext = new ActionContext(context.Object, - new RouteData() { Values = routeDictionary }, - new ActionDescriptor()); - - viewEngine.Setup(v => v.FindView(actionContext, It.IsAny())) - .Returns(ViewEngineResult.Found("MyView", view.Object)); - - - var viewResult = new ViewResult(); - - // Act - await viewResult.ExecuteResultAsync(actionContext); - - // Assert - Assert.Equal(expected, memoryStream.ToArray()); - } - - [Fact] - public async Task ExecuteResultAsync_UsesProvidedViewEngine() - { - // Arrange - var expected = new byte[] { 97, 98, 99, 100 }; - - var view = new Mock(); - view.Setup(v => v.RenderAsync(It.IsAny())) - .Callback((ViewContext v) => - { - view.ToString(); - v.Writer.Write("abcd"); - }) - .Returns(Task.FromResult(0)); - - var routeDictionary = new Dictionary(); - - var goodViewEngine = new Mock(); - - var badViewEngine = new Mock(MockBehavior.Strict); - - var serviceProvider = new Mock(); - serviceProvider.Setup(sp => sp.GetService(typeof(ICompositeViewEngine))) - .Returns(badViewEngine.Object); - - var memoryStream = new MemoryStream(); - var response = new Mock(); - response.SetupGet(r => r.Body) - .Returns(memoryStream); - - var context = new Mock(); - context.SetupGet(c => c.Response) - .Returns(response.Object); - context.SetupGet(c => c.RequestServices) - .Returns(serviceProvider.Object); - - var actionContext = new ActionContext(context.Object, - new RouteData() { Values = routeDictionary }, - new ActionDescriptor()); - - goodViewEngine.Setup(v => v.FindView(actionContext, It.IsAny())) - .Returns(ViewEngineResult.Found("MyView", view.Object)); - - - var viewResult = new ViewResult() - { - ViewEngine = goodViewEngine.Object, - }; - - // Act - await viewResult.ExecuteResultAsync(actionContext); - - // Assert - Assert.Equal(expected, memoryStream.ToArray()); - } - - public static IEnumerable ExecuteResultAsync_DoesNotWriteToResponse_OnceExceptionIsThrownData - { - get - { - yield return new object[] { 30, 0 }; - - if (PlatformHelper.IsMono) - { - // The StreamWriter in Mono buffers 2x the buffer size before flushing. - yield return new object[] { ViewResultStreamWriterBufferSize * 2 + 30, ViewResultStreamWriterBufferSize }; - } - else - { - yield return new object[] { ViewResultStreamWriterBufferSize + 30, ViewResultStreamWriterBufferSize }; - } - } - } - - // The StreamWriter used by ViewResult an internal buffer and consequently anything written to this buffer - // prior to it filling up will not be written to the underlying stream once an exception is thrown. - [Theory] - [MemberData(nameof(ExecuteResultAsync_DoesNotWriteToResponse_OnceExceptionIsThrownData))] - public async Task ExecuteResultAsync_DoesNotWriteToResponse_OnceExceptionIsThrown(int writtenLength, int expectedLength) - { - // Arrange - var longString = new string('a', writtenLength); - - var routeDictionary = new Dictionary(); - - var view = new Mock(); - view.Setup(v => v.RenderAsync(It.IsAny())) - .Callback((ViewContext v) => - { - view.ToString(); - v.Writer.Write(longString); - throw new Exception(); - }); - - var viewEngine = new Mock(); - viewEngine.Setup(v => v.FindView(It.IsAny(), It.IsAny())) - .Returns(ViewEngineResult.Found("MyView", view.Object)); - - var serviceProvider = new Mock(); - serviceProvider.Setup(sp => sp.GetService(typeof(ICompositeViewEngine))) - .Returns(viewEngine.Object); - - var memoryStream = new MemoryStream(); - var response = new Mock(); - response.SetupGet(r => r.Body) - .Returns(memoryStream); - var context = new Mock(); - context.SetupGet(c => c.Response) - .Returns(response.Object); - context.SetupGet(c => c.RequestServices).Returns(serviceProvider.Object); - - var actionContext = new ActionContext(context.Object, - new RouteData() { Values = routeDictionary }, - new ActionDescriptor()); - - var viewResult = new ViewResult(); - - // Act - await Record.ExceptionAsync(() => viewResult.ExecuteResultAsync(actionContext)); - - // Assert - Assert.Equal(expectedLength, memoryStream.Length); - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs index af0d9623f4..142f6acd6b 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs @@ -49,6 +49,12 @@ ViewWithNestedLayout-Content " }; + + yield return new[] + { + "ViewWithDataFromController", + "

hello from controller

" + }; } } @@ -141,5 +147,54 @@ component-content"; // Assert Assert.Equal(expected, body.Trim()); } + + public static IEnumerable PartialRazorViews_DoNotRenderLayoutData + { + get + { + yield return new[] + { + "ViewWithoutLayout", @"ViewWithoutLayout-Content" + }; + yield return new[] + { + "PartialViewWithNamePassedIn", @"ViewWithLayout-Content" + }; + yield return new[] + { + "ViewWithFullPath", "ViewWithFullPath-content" + }; + yield return new[] + { + "ViewWithNestedLayout", "ViewWithNestedLayout-Content" + }; + yield return new[] + { + "PartialWithDataFromController", "

hello from controller

" + }; + yield return new[] + { + "PartialWithModel", string.Join(Environment.NewLine, + "my name is judge", + "98052", + "") + }; + } + } + + [Theory] + [MemberData(nameof(PartialRazorViews_DoNotRenderLayoutData))] + public async Task PartialRazorViews_DoNotRenderLayout(string actionName, string expected) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var body = await client.GetStringAsync("http://localhost/PartialViewEngine/" + actionName); + + // Assert + Assert.Equal(expected, body.Trim()); + } } } \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Controllers/PartialViewEngineController.cs b/test/WebSites/RazorWebSite/Controllers/PartialViewEngineController.cs new file mode 100644 index 0000000000..89f222ea46 --- /dev/null +++ b/test/WebSites/RazorWebSite/Controllers/PartialViewEngineController.cs @@ -0,0 +1,46 @@ +// 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 Microsoft.AspNet.Mvc; + +namespace RazorWebSite.Controllers +{ + public class PartialViewEngineController : Controller + { + public IActionResult ViewWithoutLayout() + { + return PartialView(); + } + + public IActionResult ViewWithFullPath() + { + return PartialView(@"/Views/ViewEngine/ViewWithFullPath.cshtml"); + } + + public IActionResult PartialViewWithNamePassedIn() + { + return PartialView("ViewWithLayout"); + } + + public IActionResult ViewWithNestedLayout() + { + return PartialView(); + } + + public IActionResult PartialWithDataFromController() + { + ViewData["data-from-controller"] = "hello from controller"; + return PartialView("ViewWithDataFromController"); + } + + public IActionResult PartialWithModel() + { + var model = new Person + { + Name = "my name is judge", + Address = new Address { ZipCode = "98052"} + }; + return PartialView(model); + } + } +} \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Controllers/ViewEngineController.cs b/test/WebSites/RazorWebSite/Controllers/ViewEngineController.cs index 29a10087bd..42efaf2227 100644 --- a/test/WebSites/RazorWebSite/Controllers/ViewEngineController.cs +++ b/test/WebSites/RazorWebSite/Controllers/ViewEngineController.cs @@ -42,5 +42,11 @@ namespace RazorWebSite.Controllers ViewData["Title"] = "Controller title"; return View("ViewWithTitle"); } + + public IActionResult ViewWithDataFromController() + { + ViewData["data-from-controller"] = "hello from controller"; + return View("ViewWithDataFromController"); + } } } \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/PartialViewEngine/PartialWithModel.cshtml b/test/WebSites/RazorWebSite/Views/PartialViewEngine/PartialWithModel.cshtml new file mode 100644 index 0000000000..981fbb5c34 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/PartialViewEngine/PartialWithModel.cshtml @@ -0,0 +1,3 @@ +@model Person +@Model.Name +@Html.Partial("_Partial", Model.Address) \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/PartialViewEngine/ViewWithDataFromController.cshtml b/test/WebSites/RazorWebSite/Views/PartialViewEngine/ViewWithDataFromController.cshtml new file mode 100644 index 0000000000..d188489534 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/PartialViewEngine/ViewWithDataFromController.cshtml @@ -0,0 +1 @@ +

@ViewData["data-from-controller"]

\ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/PartialViewEngine/ViewWithFullPath.cshtml b/test/WebSites/RazorWebSite/Views/PartialViewEngine/ViewWithFullPath.cshtml new file mode 100644 index 0000000000..00b84b6ee8 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/PartialViewEngine/ViewWithFullPath.cshtml @@ -0,0 +1,4 @@ +@{ + Layout = "/Views/Shared/_Layout.cshtml"; +} +ViewWithFullPath-content \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/PartialViewEngine/ViewWithLayout.cshtml b/test/WebSites/RazorWebSite/Views/PartialViewEngine/ViewWithLayout.cshtml new file mode 100644 index 0000000000..917dcb1283 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/PartialViewEngine/ViewWithLayout.cshtml @@ -0,0 +1,4 @@ +@{ + Layout = "/Views/Shared/_Layout.cshtml"; +} +ViewWithLayout-Content \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/PartialViewEngine/ViewWithNestedLayout.cshtml b/test/WebSites/RazorWebSite/Views/PartialViewEngine/ViewWithNestedLayout.cshtml new file mode 100644 index 0000000000..476c051ead --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/PartialViewEngine/ViewWithNestedLayout.cshtml @@ -0,0 +1,4 @@ +@{ + Layout = "/Views/ViewEngine/_NestedLayout.cshtml"; +} +ViewWithNestedLayout-Content \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/PartialViewEngine/ViewWithoutLayout.cshtml b/test/WebSites/RazorWebSite/Views/PartialViewEngine/ViewWithoutLayout.cshtml new file mode 100644 index 0000000000..1838444da1 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/PartialViewEngine/ViewWithoutLayout.cshtml @@ -0,0 +1 @@ +ViewWithoutLayout-Content \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithDataFromController.cshtml b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithDataFromController.cshtml new file mode 100644 index 0000000000..d188489534 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithDataFromController.cshtml @@ -0,0 +1 @@ +

@ViewData["data-from-controller"]

\ No newline at end of file