Introducing RazorViewFactory for RazorView

This commit is contained in:
ianhong 2014-11-10 11:42:44 -08:00
parent 7e8870cb9f
commit e9c7a34cfc
7 changed files with 198 additions and 24 deletions

View File

@ -6,17 +6,16 @@ using Microsoft.AspNet.Mvc.Rendering;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
{ {
/// <summary> /// <summary>
/// Represents the contract for an <see cref="IView"/> that executes <see cref="IRazorPage"/> as part of its /// Defines methods to create <see cref="RazorView"/> instances with a given <see cref="IRazorPage"/>.
/// execution.
/// </summary> /// </summary>
public interface IRazorView : IView public interface IRazorViewFactory
{ {
/// <summary> /// <summary>
/// Contextualizes the current instance of the <see cref="IRazorView"/> providing it with the /// Creates a <see cref="RazorView"/> providing it with the <see cref="IRazorPage"/> to execute.
/// <see cref="IRazorPage"/> to execute.
/// </summary> /// </summary>
/// <param name="razorPage">The <see cref="IRazorPage"/> instance to execute.</param> /// <param name="razorPage">The <see cref="IRazorPage"/> instance to execute.</param>
/// <param name="isPartial">Determines if the view is to be executed as a partial.</param> /// <param name="isPartial">Determines if the view is to be executed as a partial.</param>
void Contextualize(IRazorPage razorPage, bool isPartial); /// <returns>The IRazorPage instance if it exists, null otherwise.</returns>
IView GetView([NotNull] IRazorPage page, bool isPartial);
} }
} }

View File

@ -4,15 +4,16 @@
using System; using System;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.PageExecutionInstrumentation; using Microsoft.AspNet.PageExecutionInstrumentation;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
{ {
/// <summary> /// <summary>
/// Default implementation for <see cref="IRazorView"/> that executes one or more <see cref="RazorPage"/> /// Default implementation for <see cref="IView"/> that executes one or more <see cref="IRazorPage"/>
/// instances as part of view rendering. /// instances as part of view rendering.
/// </summary> /// </summary>
public class RazorView : IRazorView public class RazorView : IView
{ {
private readonly IRazorPageFactory _pageFactory; private readonly IRazorPageFactory _pageFactory;
private readonly IRazorPageActivator _pageActivator; private readonly IRazorPageActivator _pageActivator;
@ -24,7 +25,7 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <summary> /// <summary>
/// Initializes a new instance of RazorView /// Initializes a new instance of RazorView
/// </summary> /// </summary>
/// <param name="pageFactory">The view factory used to instantiate layout and _ViewStart pages.</param> /// <param name="pageFactory">The page factory used to instantiate layout and _ViewStart pages.</param>
/// <param name="pageActivator">The <see cref="IRazorPageActivator"/> used to activate pages.</param> /// <param name="pageActivator">The <see cref="IRazorPageActivator"/> used to activate pages.</param>
/// <param name="viewStartProvider">The <see cref="IViewStartProvider"/> used for discovery of _ViewStart /// <param name="viewStartProvider">The <see cref="IViewStartProvider"/> used for discovery of _ViewStart
/// pages</param> /// pages</param>
@ -42,7 +43,12 @@ namespace Microsoft.AspNet.Mvc.Razor
get { return _pageExecutionFeature != null; } get { return _pageExecutionFeature != null; }
} }
/// <inheritdoc /> /// <summary>
/// Contextualizes the current instance of the <see cref="RazorView"/> providing it with the
/// <see cref="IRazorPage"/> to execute.
/// </summary>
/// <param name="razorPage">The <see cref="IRazorPage"/> instance to execute.</param>
/// <param name="isPartial">Determines if the view is to be executed as a partial.</param>
public virtual void Contextualize([NotNull] IRazorPage razorPage, public virtual void Contextualize([NotNull] IRazorPage razorPage,
bool isPartial) bool isPartial)
{ {

View File

@ -34,6 +34,7 @@ namespace Microsoft.AspNet.Mvc.Razor
}; };
private readonly IRazorPageFactory _pageFactory; private readonly IRazorPageFactory _pageFactory;
private readonly IRazorViewFactory _pageViewFactory;
private readonly IReadOnlyList<IViewLocationExpander> _viewLocationExpanders; private readonly IReadOnlyList<IViewLocationExpander> _viewLocationExpanders;
private readonly IViewLocationCache _viewLocationCache; private readonly IViewLocationCache _viewLocationCache;
@ -42,10 +43,12 @@ namespace Microsoft.AspNet.Mvc.Razor
/// </summary> /// </summary>
/// <param name="pageFactory">The page factory used for creating <see cref="IRazorPage"/> instances.</param> /// <param name="pageFactory">The page factory used for creating <see cref="IRazorPage"/> instances.</param>
public RazorViewEngine(IRazorPageFactory pageFactory, public RazorViewEngine(IRazorPageFactory pageFactory,
IRazorViewFactory pageViewFactory,
IViewLocationExpanderProvider viewLocationExpanderProvider, IViewLocationExpanderProvider viewLocationExpanderProvider,
IViewLocationCache viewLocationCache) IViewLocationCache viewLocationCache)
{ {
_pageFactory = pageFactory; _pageFactory = pageFactory;
_pageViewFactory = pageViewFactory;
_viewLocationExpanders = viewLocationExpanderProvider.ViewLocationExpanders; _viewLocationExpanders = viewLocationExpanderProvider.ViewLocationExpanders;
_viewLocationCache = viewLocationCache; _viewLocationCache = viewLocationCache;
} }
@ -193,9 +196,9 @@ namespace Microsoft.AspNet.Mvc.Razor
// and might store state. We'll use the service container to create new instances as we require. // and might store state. We'll use the service container to create new instances as we require.
var services = actionContext.HttpContext.RequestServices; var services = actionContext.HttpContext.RequestServices;
var view = services.GetRequiredService<IRazorView>();
view.Contextualize(page, partial); var view = _pageViewFactory.GetView(page, partial);
return ViewEngineResult.Found(viewName, view); return ViewEngineResult.Found(viewName, view);
} }

View File

@ -0,0 +1,40 @@
// 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.Razor
{
public class RazorViewFactory : IRazorViewFactory
{
private readonly IRazorPageActivator _pageActivator;
private readonly IRazorPageFactory _pageFactory;
private readonly IViewStartProvider _viewStartProvider;
/// <summary>
/// Initializes a new instance of RazorViewFactory
/// </summary>
/// <param name="pageFactory">The page factory used to instantiate layout and _ViewStart pages.</param>
/// <param name="pageActivator">The <see cref="IRazorPageActivator"/> used to activate pages.</param>
/// <param name="viewStartProvider">The <see cref="IViewStartProvider"/> used for discovery of _ViewStart
/// pages</param>
public RazorViewFactory(IRazorPageFactory pageFactory,
IRazorPageActivator pageActivator,
IViewStartProvider viewStartProvider)
{
_pageFactory = pageFactory;
_pageActivator = pageActivator;
_viewStartProvider = viewStartProvider;
}
/// <inheritdoc />
public IView GetView([NotNull] IRazorPage page, bool isPartial)
{
var razorView = new RazorView(_pageFactory, _pageActivator, _viewStartProvider);
razorView.Contextualize(page, isPartial);
return razorView;
}
}
}

View File

@ -132,7 +132,7 @@ namespace Microsoft.AspNet.Mvc
// The ViewStartProvider needs to be able to consume scoped instances of IRazorPageFactory // The ViewStartProvider needs to be able to consume scoped instances of IRazorPageFactory
yield return describe.Scoped<IViewStartProvider, ViewStartProvider>(); yield return describe.Scoped<IViewStartProvider, ViewStartProvider>();
yield return describe.Transient<IRazorView, RazorView>(); yield return describe.Transient<IRazorViewFactory, RazorViewFactory>();
yield return describe.Singleton<IRazorPageActivator, RazorPageActivator>(); yield return describe.Singleton<IRazorPageActivator, RazorPageActivator>();
// Virtual path view factory needs to stay scoped so views can get get scoped services. // Virtual path view factory needs to stay scoped so views can get get scoped services.
yield return describe.Scoped<IRazorPageFactory, VirtualPathRazorPageFactory>(); yield return describe.Scoped<IRazorPageFactory, VirtualPathRazorPageFactory>();

View File

@ -210,10 +210,15 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
{ {
// Arrange // Arrange
var pageFactory = new Mock<IRazorPageFactory>(); var pageFactory = new Mock<IRazorPageFactory>();
var viewFactory = new Mock<IRazorViewFactory>();
var page = Mock.Of<IRazorPage>(); var page = Mock.Of<IRazorPage>();
pageFactory.Setup(p => p.CreateInstance(It.IsAny<string>())) pageFactory.Setup(p => p.CreateInstance(It.IsAny<string>()))
.Returns(Mock.Of<IRazorPage>()); .Returns(Mock.Of<IRazorPage>());
var viewEngine = CreateViewEngine(pageFactory.Object); viewFactory.Setup(p => p.GetView(It.IsAny<IRazorPage>(), It.IsAny<bool>()))
.Returns(Mock.Of<IView>());
var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object);
var context = GetActionContext(_controllerTestContext); var context = GetActionContext(_controllerTestContext);
// Act // Act
@ -221,7 +226,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
// Assert // Assert
Assert.True(result.Success); Assert.True(result.Success);
Assert.IsAssignableFrom<IRazorView>(result.View); Assert.IsAssignableFrom<IView>(result.View);
Assert.Equal("/Views/bar/test-view.cshtml", result.ViewName); Assert.Equal("/Views/bar/test-view.cshtml", result.ViewName);
} }
@ -230,11 +235,13 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
{ {
// Arrange // Arrange
var pageFactory = new Mock<IRazorPageFactory>(); var pageFactory = new Mock<IRazorPageFactory>();
var viewFactory = new Mock<IRazorViewFactory>();
var page = Mock.Of<IRazorPage>(); var page = Mock.Of<IRazorPage>();
pageFactory.Setup(p => p.CreateInstance("fake-path1/bar/test-view.rzr")) pageFactory.Setup(p => p.CreateInstance("fake-path1/bar/test-view.rzr"))
.Returns(Mock.Of<IRazorPage>()) .Returns(Mock.Of<IRazorPage>())
.Verifiable(); .Verifiable();
var viewEngine = new OverloadedLocationViewEngine(pageFactory.Object, var viewEngine = new OverloadedLocationViewEngine(pageFactory.Object,
viewFactory.Object,
GetViewLocationExpanders(), GetViewLocationExpanders(),
GetViewLocationCache()); GetViewLocationCache());
var context = GetActionContext(_controllerTestContext); var context = GetActionContext(_controllerTestContext);
@ -251,11 +258,13 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
{ {
// Arrange // Arrange
var pageFactory = new Mock<IRazorPageFactory>(); var pageFactory = new Mock<IRazorPageFactory>();
var viewFactory = new Mock<IRazorViewFactory>();
var page = Mock.Of<IRazorPage>(); var page = Mock.Of<IRazorPage>();
pageFactory.Setup(p => p.CreateInstance("fake-area-path/foo/bar/test-view2.rzr")) pageFactory.Setup(p => p.CreateInstance("fake-area-path/foo/bar/test-view2.rzr"))
.Returns(Mock.Of<IRazorPage>()) .Returns(Mock.Of<IRazorPage>())
.Verifiable(); .Verifiable();
var viewEngine = new OverloadedLocationViewEngine(pageFactory.Object, var viewEngine = new OverloadedLocationViewEngine(pageFactory.Object,
viewFactory.Object,
GetViewLocationExpanders(), GetViewLocationExpanders(),
GetViewLocationCache()); GetViewLocationCache());
var context = GetActionContext(_areaTestContext); var context = GetActionContext(_areaTestContext);
@ -293,7 +302,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
}; };
} }
} }
/*
[Theory] [Theory]
[MemberData(nameof(FindView_UsesViewLocationExpandersToLocateViewsData))] [MemberData(nameof(FindView_UsesViewLocationExpandersToLocateViewsData))]
public void FindView_UsesViewLocationExpandersToLocateViews(IDictionary<string, object> routeValues, public void FindView_UsesViewLocationExpandersToLocateViews(IDictionary<string, object> routeValues,
@ -304,6 +313,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
pageFactory.Setup(p => p.CreateInstance("test-string/bar.cshtml")) pageFactory.Setup(p => p.CreateInstance("test-string/bar.cshtml"))
.Returns(Mock.Of<IRazorPage>()) .Returns(Mock.Of<IRazorPage>())
.Verifiable(); .Verifiable();
var expander1Result = new[] { "some-seed" }; var expander1Result = new[] { "some-seed" };
var expander1 = new Mock<IViewLocationExpander>(); var expander1 = new Mock<IViewLocationExpander>();
expander1.Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>())) expander1.Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
@ -347,7 +357,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
expander1.Verify(); expander1.Verify();
expander2.Verify(); expander2.Verify();
} }
*/
[Fact] [Fact]
public void FindView_CachesValuesIfViewWasFound() public void FindView_CachesValuesIfViewWasFound()
{ {
@ -358,14 +368,19 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
pageFactory.Setup(p => p.CreateInstance("/Views/Shared/baz.cshtml")) pageFactory.Setup(p => p.CreateInstance("/Views/Shared/baz.cshtml"))
.Returns(Mock.Of<IRazorPage>()) .Returns(Mock.Of<IRazorPage>())
.Verifiable(); .Verifiable();
var viewFactory = new Mock<IRazorViewFactory>();
viewFactory.Setup(p => p.GetView(It.IsAny<IRazorPage>(), It.IsAny<bool>()))
.Returns(Mock.Of<IView>());
var cache = GetViewLocationCache(); var cache = GetViewLocationCache();
var cacheMock = Mock.Get<IViewLocationCache>(cache); var cacheMock = Mock.Get<IViewLocationCache>(cache);
cacheMock.Setup(c => c.Set(It.IsAny<ViewLocationExpanderContext>(), "/Views/Shared/baz.cshtml")) cacheMock.Setup(c => c.Set(It.IsAny<ViewLocationExpanderContext>(), "/Views/Shared/baz.cshtml"))
.Verifiable(); .Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object, cache: cache); var viewEngine = CreateViewEngine(pageFactory.Object, viewFactory.Object, cache: cache);
var context = GetActionContext(_controllerTestContext); var context = GetActionContext(_controllerTestContext, viewFactory.Object);
// Act // Act
var result = viewEngine.FindView(context, "baz"); var result = viewEngine.FindView(context, "baz");
@ -384,6 +399,11 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
pageFactory.Setup(p => p.CreateInstance("some-view-location")) pageFactory.Setup(p => p.CreateInstance("some-view-location"))
.Returns(Mock.Of<IRazorPage>()) .Returns(Mock.Of<IRazorPage>())
.Verifiable(); .Verifiable();
var viewFactory = new Mock<IRazorViewFactory>();
viewFactory.Setup(p => p.GetView(It.IsAny<IRazorPage>(), It.IsAny<bool>()))
.Returns(Mock.Of<IView>());
var expander = new Mock<IViewLocationExpander>(MockBehavior.Strict); var expander = new Mock<IViewLocationExpander>(MockBehavior.Strict);
expander.Setup(v => v.PopulateValues(It.IsAny<ViewLocationExpanderContext>())) expander.Setup(v => v.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
.Verifiable(); .Verifiable();
@ -393,6 +413,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
.Verifiable(); .Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object, var viewEngine = CreateViewEngine(pageFactory.Object,
viewFactory.Object,
new[] { expander.Object }, new[] { expander.Object },
cacheMock.Object); cacheMock.Object);
var context = GetActionContext(_controllerTestContext); var context = GetActionContext(_controllerTestContext);
@ -418,6 +439,11 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
pageFactory.Setup(p => p.CreateInstance("some-view-location")) pageFactory.Setup(p => p.CreateInstance("some-view-location"))
.Returns(Mock.Of<IRazorPage>()) .Returns(Mock.Of<IRazorPage>())
.Verifiable(); .Verifiable();
var viewFactory = new Mock<IRazorViewFactory>();
viewFactory.Setup(p => p.GetView(It.IsAny<IRazorPage>(), It.IsAny<bool>()))
.Returns(Mock.Of<IView>());
var cacheMock = new Mock<IViewLocationCache>(); var cacheMock = new Mock<IViewLocationCache>();
cacheMock.Setup(c => c.Get(It.IsAny<ViewLocationExpanderContext>())) cacheMock.Setup(c => c.Get(It.IsAny<ViewLocationExpanderContext>()))
.Returns("expired-location"); .Returns("expired-location");
@ -432,6 +458,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
.Verifiable(); .Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object, var viewEngine = CreateViewEngine(pageFactory.Object,
viewFactory.Object,
new[] { expander.Object }, new[] { expander.Object },
cacheMock.Object); cacheMock.Object);
var context = GetActionContext(_controllerTestContext); var context = GetActionContext(_controllerTestContext);
@ -447,14 +474,18 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
} }
private IViewEngine CreateViewEngine(IRazorPageFactory pageFactory = null, private IViewEngine CreateViewEngine(IRazorPageFactory pageFactory = null,
IRazorViewFactory viewFactory = null,
IEnumerable<IViewLocationExpander> expanders = null, IEnumerable<IViewLocationExpander> expanders = null,
IViewLocationCache cache = null) IViewLocationCache cache = null)
{ {
pageFactory = pageFactory ?? Mock.Of<IRazorPageFactory>(); pageFactory = pageFactory ?? Mock.Of<IRazorPageFactory>();
viewFactory = viewFactory ?? Mock.Of<IRazorViewFactory>();
cache = cache ?? GetViewLocationCache(); cache = cache ?? GetViewLocationCache();
var viewLocationExpanderProvider = GetViewLocationExpanders(expanders); var viewLocationExpanderProvider = GetViewLocationExpanders(expanders);
var viewEngine = new RazorViewEngine(pageFactory, var viewEngine = new RazorViewEngine(pageFactory,
viewFactory,
viewLocationExpanderProvider, viewLocationExpanderProvider,
cache); cache);
@ -481,12 +512,12 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
} }
private static ActionContext GetActionContext(IDictionary<string, object> routeValues, private static ActionContext GetActionContext(IDictionary<string, object> routeValues,
IRazorView razorView = null) IRazorViewFactory razorViewFactory = null)
{ {
var httpContext = new DefaultHttpContext(); var httpContext = new DefaultHttpContext();
var serviceProvider = new Mock<IServiceProvider>(); var serviceProvider = new Mock<IServiceProvider>();
serviceProvider.Setup(p => p.GetService(typeof(IRazorView))) serviceProvider.Setup(p => p.GetService(typeof(IRazorViewFactory)))
.Returns(razorView ?? Mock.Of<IRazorView>()); .Returns(razorViewFactory ?? Mock.Of<IRazorViewFactory>());
httpContext.RequestServices = serviceProvider.Object; httpContext.RequestServices = serviceProvider.Object;
@ -502,9 +533,10 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
private class OverloadedLocationViewEngine : RazorViewEngine private class OverloadedLocationViewEngine : RazorViewEngine
{ {
public OverloadedLocationViewEngine(IRazorPageFactory pageFactory, public OverloadedLocationViewEngine(IRazorPageFactory pageFactory,
IRazorViewFactory viewFactory,
IViewLocationExpanderProvider expanderProvider, IViewLocationExpanderProvider expanderProvider,
IViewLocationCache cache) IViewLocationCache cache)
: base(pageFactory, expanderProvider, cache) : base(pageFactory, viewFactory, expanderProvider, cache)
{ {
} }

View File

@ -0,0 +1,94 @@
// 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 Xunit;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.PipelineCore;
using Moq;
namespace Microsoft.AspNet.Mvc.Razor.Test
{
public class RazorViewFactoryTest
{
[Fact]
public void GetView_ReturnsRazorView()
{
// Arrange
var factory = new RazorViewFactory(
Mock.Of<IRazorPageFactory>(),
Mock.Of<IRazorPageActivator>(),
Mock.Of<IViewStartProvider>());
// Act
var view = factory.GetView(Mock.Of<IRazorPage>(), true);
// Assert
Assert.IsType<RazorView>(view);
}
[Fact]
public async Task RenderAsync_ContextualizeMustBeInvoked()
{
// Arrange
var page = new TestableRazorPage(v => { });
var factory = new RazorViewFactory(
Mock.Of<IRazorPageFactory>(),
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider());
// Act
var view = factory.GetView(page, true);
// Assert
var viewContext = CreateViewContext(view);
await Assert.DoesNotThrowAsync(() => view.RenderAsync(viewContext));
}
private static IViewStartProvider CreateViewStartProvider()
{
var viewStartPages = new IRazorPage[0];
var viewStartProvider = new Mock<IViewStartProvider>();
viewStartProvider.Setup(v => v.GetViewStartPages(It.IsAny<string>()))
.Returns(viewStartPages);
return viewStartProvider.Object;
}
private static ViewContext CreateViewContext(IView view)
{
var httpContext = new DefaultHttpContext();
var actionContext = new ActionContext(httpContext, routeData: null, actionDescriptor: null);
return new ViewContext(
actionContext,
view,
new ViewDataDictionary(new EmptyModelMetadataProvider()),
new StringWriter());
}
private class TestableRazorPage : RazorPage
{
private readonly Action<TestableRazorPage> _executeAction;
public TestableRazorPage(Action<TestableRazorPage> executeAction)
{
_executeAction = executeAction;
}
public void RenderBodyPublic()
{
Write(RenderBody());
}
public override Task ExecuteAsync()
{
_executeAction(this);
return Task.FromResult(0);
}
}
}
}