diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/PartialViewResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/PartialViewResult.cs
index 96dfc7b77b..efcac52297 100644
--- a/src/Microsoft.AspNet.Mvc.Core/ActionResults/PartialViewResult.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/PartialViewResult.cs
@@ -1,21 +1,53 @@
// 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.Threading.Tasks;
using Microsoft.AspNet.Mvc.Rendering;
+using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc
{
///
- /// Represents a that renders a partial view.
+ /// Represents an that renders a partial view to the response.
///
- public class PartialViewResult : ViewResultBase
+ public class PartialViewResult : ActionResult
{
+ ///
+ /// Gets or sets the name of the partial view to render.
+ ///
+ ///
+ /// When null, defaults to .
+ ///
+ public string ViewName { get; set; }
+
+ ///
+ /// Gets or sets the used for rendering the view for this result.
+ ///
+ public ViewDataDictionary ViewData { get; set; }
+
+ ///
+ /// Gets or sets the used to locate views.
+ ///
+ /// When null, an instance of from
+ /// ActionContext.HttpContext.RequestServices is used.
+ public IViewEngine ViewEngine { get; set; }
+
///
- protected override ViewEngineResult FindView([NotNull] IViewEngine viewEngine,
- [NotNull] ActionContext context,
- [NotNull] string viewName)
+ public override async Task ExecuteResultAsync([NotNull] ActionContext context)
{
- return viewEngine.FindPartialView(context, viewName);
+ var viewEngine = ViewEngine ??
+ context.HttpContext.RequestServices.GetRequiredService();
+
+ var viewName = ViewName ?? context.ActionDescriptor.Name;
+ var view = viewEngine.FindPartialView(context, viewName)
+ .EnsureSuccessful()
+ .View;
+
+ using (view as IDisposable)
+ {
+ await ViewExecutor.ExecuteAsync(view, context, ViewData, contentType: null);
+ }
}
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewExecutor.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewExecutor.cs
new file mode 100644
index 0000000000..e47569ce9a
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewExecutor.cs
@@ -0,0 +1,131 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNet.Mvc.Rendering;
+
+namespace Microsoft.AspNet.Mvc
+{
+ ///
+ /// Utility type for rendering a to the response.
+ ///
+ public static class ViewExecutor
+ {
+ private const int BufferSize = 1024;
+ private const string ContentType = "text/html; charset=utf-8";
+
+ ///
+ /// Asynchronously renders the specified to the response body.
+ ///
+ /// The to render.
+ /// The for the current executing action.
+ /// The for the view being rendered.
+ /// A that represents the asychronous rendering.
+ public static async Task ExecuteAsync([NotNull] IView view,
+ [NotNull] ActionContext actionContext,
+ [NotNull] ViewDataDictionary viewData,
+ string contentType)
+ {
+ if (string.IsNullOrEmpty(contentType))
+ {
+ contentType = ContentType;
+ }
+
+ actionContext.HttpContext.Response.ContentType = contentType;
+ var wrappedStream = new StreamWrapper(actionContext.HttpContext.Response.Body);
+ var encoding = Encodings.UTF8EncodingWithoutBOM;
+ using (var writer = new StreamWriter(wrappedStream, encoding, BufferSize, leaveOpen: true))
+ {
+ try
+ {
+ var viewContext = new ViewContext(actionContext, 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 class StreamWrapper : Stream
+ {
+ private readonly Stream _wrappedStream;
+
+ public StreamWrapper(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);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs
index efc9d002e9..f4e951f12e 100644
--- a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs
@@ -1,21 +1,53 @@
// 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.Threading.Tasks;
using Microsoft.AspNet.Mvc.Rendering;
+using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc
{
///
- /// Represents a that renders a view to the response.
+ /// Represents an that renders a view to the response.
///
- public class ViewResult : ViewResultBase
+ public class ViewResult : ActionResult
{
+ ///
+ /// /// Gets or sets the name of the view to render.
+ ///
+ ///
+ /// When null, defaults to .
+ ///
+ public string ViewName { get; set; }
+
+ ///
+ /// Gets or sets the for this result.
+ ///
+ public ViewDataDictionary ViewData { get; set; }
+
+ ///
+ /// Gets or sets the used to locate views.
+ ///
+ /// When null, an instance of from
+ /// ActionContext.HttpContext.RequestServices is used.
+ public IViewEngine ViewEngine { get; set; }
+
///
- protected override ViewEngineResult FindView([NotNull] IViewEngine viewEngine,
- [NotNull] ActionContext context,
- [NotNull] string viewName)
+ public override async Task ExecuteResultAsync([NotNull] ActionContext context)
{
- return viewEngine.FindView(context, viewName);
+ var viewEngine = ViewEngine ??
+ context.HttpContext.RequestServices.GetRequiredService();
+
+ var viewName = ViewName ?? context.ActionDescriptor.Name;
+ var view = viewEngine.FindView(context, viewName)
+ .EnsureSuccessful()
+ .View;
+
+ using (view as IDisposable)
+ {
+ await ViewExecutor.ExecuteAsync(view, context, ViewData, contentType: null);
+ }
}
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResultBase.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResultBase.cs
deleted file mode 100644
index 83d3c9e997..0000000000
--- a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResultBase.cs
+++ /dev/null
@@ -1,163 +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.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.GetRequiredService();
-
- 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/Rendering/ViewEngine/ViewEngineResult.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngine/ViewEngineResult.cs
index 54a2a3b618..c72ca74372 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngine/ViewEngineResult.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/ViewEngine/ViewEngineResult.cs
@@ -1,7 +1,9 @@
// 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 Microsoft.AspNet.Mvc.Core;
namespace Microsoft.AspNet.Mvc.Rendering
{
@@ -40,5 +42,21 @@ namespace Microsoft.AspNet.Mvc.Rendering
ViewName = viewName,
};
}
+
+ public ViewEngineResult EnsureSuccessful()
+ {
+ if (!Success)
+ {
+ var locations = string.Empty;
+ if (SearchedLocations != null)
+ {
+ locations = Environment.NewLine + string.Join(Environment.NewLine, SearchedLocations);
+ }
+
+ throw new InvalidOperationException(Resources.FormatViewEngine_ViewNotFound(ViewName, locations));
+ }
+
+ return this;
+ }
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewViewComponentResult.cs b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewViewComponentResult.cs
index dc587b091a..8a03ab2a7b 100644
--- a/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewViewComponentResult.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/ViewComponents/ViewViewComponentResult.cs
@@ -88,20 +88,9 @@ namespace Microsoft.AspNet.Mvc
private IView FindView(ActionContext context, string viewName)
{
- var result = _viewEngine.FindPartialView(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;
+ return _viewEngine.FindPartialView(context, viewName)
+ .EnsureSuccessful()
+ .View;
}
}
}
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/PartialViewResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/PartialViewResultTest.cs
index c2ca9d11ea..427f37f400 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/PartialViewResultTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/PartialViewResultTest.cs
@@ -1,6 +1,7 @@
// 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.Threading.Tasks;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.PipelineCore;
@@ -8,26 +9,110 @@ using Microsoft.AspNet.Routing;
using Moq;
using Xunit;
-namespace Microsoft.AspNet.Mvc.Core
+namespace Microsoft.AspNet.Mvc
{
public class PartialViewResultTest
{
[Fact]
- public async Task PartialViewResult_UsesFindViewOnSpecifiedViewEngineToLocateViews()
+ public async Task ExecuteResultAsync_ReturnsError_IfViewCouldNotBeFound()
{
// Arrange
- var viewName = "mypartialview";
- var context = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
+ 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 = new Mock();
- var view = Mock.Of();
-
- viewEngine.Setup(e => e.FindPartialView(context, viewName))
- .Returns(ViewEngineResult.Found(viewName, view))
- .Verifiable();
+ viewEngine.Setup(v => v.FindPartialView(It.IsAny(), It.IsAny()))
+ .Returns(ViewEngineResult.NotFound("MyView", new[] { "Location1", "Location2" }))
+ .Verifiable();
var viewResult = new PartialViewResult
{
ViewEngine = viewEngine.Object,
+ ViewName = "MyView"
+ };
+
+ // Act and Assert
+ var ex = await Assert.ThrowsAsync(
+ () => viewResult.ExecuteResultAsync(actionContext));
+ Assert.Equal(expected, ex.Message);
+ viewEngine.Verify();
+ }
+
+ [Fact]
+ public async Task ViewResult_UsesFindPartialViewOnSpecifiedViewEngineToLocateViews()
+ {
+ // 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.FindPartialView(context, "myview"))
+ .Returns(ViewEngineResult.Found("myview", view))
+ .Verifiable();
+
+ var viewResult = new PartialViewResult
+ {
+ ViewName = viewName,
+ ViewEngine = viewEngine.Object
+ };
+
+ // Act
+ await viewResult.ExecuteResultAsync(context);
+
+ // Assert
+ viewEngine.Verify();
+ }
+
+ [Fact]
+ public async Task ExecuteResultAsync_UsesActionDescriptorName_IfViewNameIsNull()
+ {
+ // Arrange
+ var viewName = "some-view-name";
+ var context = new ActionContext(new DefaultHttpContext(),
+ new RouteData(),
+ new ActionDescriptor { Name = viewName });
+ var viewEngine = new Mock();
+ viewEngine.Setup(e => e.FindPartialView(context, viewName))
+ .Returns(ViewEngineResult.Found(viewName, Mock.Of()))
+ .Verifiable();
+
+ var viewResult = new PartialViewResult
+ {
+ ViewEngine = viewEngine.Object
+ };
+
+ // Act
+ await viewResult.ExecuteResultAsync(context);
+
+ // Assert
+ viewEngine.Verify();
+ }
+
+ [Fact]
+ public async Task ExecuteResultAsync_UsesCompositeViewEngineFromServices_IfViewEngineIsNotSpecified()
+ {
+ // Arrange
+ var viewName = "partial-view-name";
+ var context = new ActionContext(new DefaultHttpContext(),
+ new RouteData(),
+ new ActionDescriptor { Name = viewName });
+ var viewEngine = new Mock();
+ viewEngine.Setup(e => e.FindPartialView(It.IsAny(), viewName))
+ .Returns(ViewEngineResult.Found(viewName, Mock.Of()))
+ .Verifiable();
+
+ var serviceProvider = new Mock();
+ serviceProvider.Setup(p => p.GetService(typeof(ICompositeViewEngine)))
+ .Returns(viewEngine.Object);
+ context.HttpContext.RequestServices = serviceProvider.Object;
+
+ var viewResult = new PartialViewResult
+ {
ViewName = viewName
};
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ViewExecutorTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ViewExecutorTest.cs
new file mode 100644
index 0000000000..b0ff83d53e
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ViewExecutorTest.cs
@@ -0,0 +1,129 @@
+// 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.ModelBinding;
+using Microsoft.AspNet.Mvc.Rendering;
+using Microsoft.AspNet.PipelineCore;
+using Microsoft.AspNet.Routing;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc
+{
+ public class ViewExecutorTest
+ {
+ // The buffer size of the StreamWriter used in ViewResult.
+ private const int ViewResultStreamWriterBufferSize = 1024;
+
+ [Fact]
+ public async Task ExecuteAsync_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 context = new DefaultHttpContext();
+ var memoryStream = new MemoryStream();
+ context.Response.Body = memoryStream;
+
+ var actionContext = new ActionContext(context,
+ new RouteData(),
+ new ActionDescriptor());
+ var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider());
+
+ // Act
+ await ViewExecutor.ExecuteAsync(view.Object, actionContext, viewData, contentType: null);
+
+ // Assert
+ Assert.Equal(expected, memoryStream.ToArray());
+ Assert.Equal("text/html; charset=utf-8", context.Response.ContentType);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_UsesSpecifiedContentType()
+ {
+ // Arrange
+ var contentType = "some-content-type";
+ var view = Mock.Of();
+ var context = new DefaultHttpContext();
+ var memoryStream = new MemoryStream();
+ context.Response.Body = memoryStream;
+
+ var actionContext = new ActionContext(context,
+ new RouteData(),
+ new ActionDescriptor());
+ var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider());
+
+ // Act
+ await ViewExecutor.ExecuteAsync(view, actionContext, viewData, contentType);
+
+ // Assert
+ Assert.Equal(contentType, context.Response.ContentType);
+ }
+
+ public static IEnumerable