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