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
{
/// <summary>
/// Represents the contract for an <see cref="IView"/> that executes <see cref="IRazorPage"/> as part of its
/// execution.
/// Defines methods to create <see cref="RazorView"/> instances with a given <see cref="IRazorPage"/>.
/// </summary>
public interface IRazorView : IView
public interface IRazorViewFactory
{
/// <summary>
/// Contextualizes the current instance of the <see cref="IRazorView"/> providing it with the
/// <see cref="IRazorPage"/> to execute.
/// Creates a <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>
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.IO;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.PageExecutionInstrumentation;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <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.
/// </summary>
public class RazorView : IRazorView
public class RazorView : IView
{
private readonly IRazorPageFactory _pageFactory;
private readonly IRazorPageActivator _pageActivator;
@ -24,7 +25,7 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <summary>
/// Initializes a new instance of RazorView
/// </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="viewStartProvider">The <see cref="IViewStartProvider"/> used for discovery of _ViewStart
/// pages</param>
@ -42,7 +43,12 @@ namespace Microsoft.AspNet.Mvc.Razor
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,
bool isPartial)
{

View File

@ -34,6 +34,7 @@ namespace Microsoft.AspNet.Mvc.Razor
};
private readonly IRazorPageFactory _pageFactory;
private readonly IRazorViewFactory _pageViewFactory;
private readonly IReadOnlyList<IViewLocationExpander> _viewLocationExpanders;
private readonly IViewLocationCache _viewLocationCache;
@ -42,10 +43,12 @@ namespace Microsoft.AspNet.Mvc.Razor
/// </summary>
/// <param name="pageFactory">The page factory used for creating <see cref="IRazorPage"/> instances.</param>
public RazorViewEngine(IRazorPageFactory pageFactory,
IRazorViewFactory pageViewFactory,
IViewLocationExpanderProvider viewLocationExpanderProvider,
IViewLocationCache viewLocationCache)
{
_pageFactory = pageFactory;
_pageViewFactory = pageViewFactory;
_viewLocationExpanders = viewLocationExpanderProvider.ViewLocationExpanders;
_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.
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);
}

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
yield return describe.Scoped<IViewStartProvider, ViewStartProvider>();
yield return describe.Transient<IRazorView, RazorView>();
yield return describe.Transient<IRazorViewFactory, RazorViewFactory>();
yield return describe.Singleton<IRazorPageActivator, RazorPageActivator>();
// Virtual path view factory needs to stay scoped so views can get get scoped services.
yield return describe.Scoped<IRazorPageFactory, VirtualPathRazorPageFactory>();

View File

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