Modifying RazorViewEngine to create IRazorView instances from service

container
This commit is contained in:
Pranav K 2014-08-19 18:07:49 -07:00
parent f6547d63e8
commit 2dcbbf70b0
8 changed files with 151 additions and 120 deletions

View File

@ -0,0 +1,22 @@
// 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
{
/// <summary>
/// Represents the contract for an <see cref="IView"/> that executes <see cref="IRazorPage"/> as part of its
/// execution.
/// </summary>
public interface IRazorView : IView
{
/// <summary>
/// Contextualizes the current instance of the <see cref="IRazorView"/> 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);
}
}

View File

@ -234,6 +234,22 @@ namespace Microsoft.AspNet.Mvc.Razor
return string.Format(CultureInfo.CurrentCulture, GetString("ViewContextMustBeSet"), p0, p1);
}
/// <summary>
/// The '{0}' method must be called before '{1}' can be invoked.
/// </summary>
internal static string ViewMustBeContextualized
{
get { return GetString("ViewMustBeContextualized"); }
}
/// <summary>
/// The '{0}' method must be called before '{1}' can be invoked.
/// </summary>
internal static string FormatViewMustBeContextualized(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewMustBeContextualized"), p0, p1);
}
/// <summary>
/// The method '{0}' cannot be invoked by this view.
/// </summary>

View File

@ -3,56 +3,61 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Rendering;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// Represents a <see cref="IView"/> that executes one or more <see cref="RazorPage"/> instances as part of
/// view rendering.
/// Default implementation for <see cref="IRazorView"/> that executes one or more <see cref="RazorPage"/>
/// instances as part of view rendering.
/// </summary>
public class RazorView : IView
public class RazorView : IRazorView
{
private readonly IRazorPageFactory _pageFactory;
private readonly IRazorPageActivator _pageActivator;
private readonly IViewStartProvider _viewStartProvider;
private readonly IRazorPage _page;
private IRazorPage _razorPage;
private bool _isPartial;
/// <summary>
/// Initializes a new instance of RazorView
/// </summary>
/// <param name="page">The page to execute</param>
/// <param name="pageFactory">The view factory used to instantiate layout and _ViewStart pages.</param>
/// <param name="pageActivator">The <see cref="IRazorPageActivator"/> used to activate pages.</param>
public RazorView([NotNull] IRazorPageFactory pageFactory,
[NotNull] IRazorPageActivator pageActivator,
[NotNull] IViewStartProvider viewStartProvider,
[NotNull] IRazorPage page)
/// <param name="viewStartProvider">The <see cref="IViewStartProvider"/> used for discovery of _ViewStart
/// pages</param>
public RazorView(IRazorPageFactory pageFactory,
IRazorPageActivator pageActivator,
IViewStartProvider viewStartProvider)
{
_pageFactory = pageFactory;
_pageActivator = pageActivator;
_viewStartProvider = viewStartProvider;
_page = page;
}
/// <summary>
/// Gets or sets a value that determines if the view hierarchy is executed as part of
/// executing the <see cref="IRazorPage"/> instance. The view hierarchy involves _ViewStart
/// and Layout pages.
/// </summary>
public bool ExecuteViewHierarchy { get; set; }
/// <inheritdoc />
public virtual void Contextualize(IRazorPage razorPage, bool isPartial)
{
_razorPage = razorPage;
_isPartial = isPartial;
}
/// <inheritdoc />
public virtual async Task RenderAsync([NotNull] ViewContext context)
{
if (ExecuteViewHierarchy)
if (_razorPage == null)
{
var bodyWriter = await RenderPageAsync(_page, context, executeViewStart: true);
var message = Resources.FormatViewMustBeContextualized(nameof(Contextualize), nameof(RenderAsync));
throw new InvalidOperationException(message);
}
if (!_isPartial)
{
var bodyWriter = await RenderPageAsync(_razorPage, context, executeViewStart: true);
await RenderLayoutAsync(context, bodyWriter);
}
else
{
await RenderPageCoreAsync(_page, context);
await RenderPageCoreAsync(_razorPage, context);
}
}
@ -93,14 +98,14 @@ namespace Microsoft.AspNet.Mvc.Razor
private async Task RenderViewStartAsync(ViewContext context)
{
var viewStarts = _viewStartProvider.GetViewStartPages(_page.Path);
var viewStarts = _viewStartProvider.GetViewStartPages(_razorPage.Path);
foreach (var viewStart in viewStarts)
{
await RenderPageCoreAsync(viewStart, context);
// Copy over interesting properties from the ViewStart page to the entry page.
_page.Layout = viewStart.Layout;
_razorPage.Layout = viewStart.Layout;
}
}
@ -109,7 +114,7 @@ namespace Microsoft.AspNet.Mvc.Razor
{
// A layout page can specify another layout page. We'll need to continue
// looking for layout pages until they're no longer specified.
var previousPage = _page;
var previousPage = _razorPage;
while (!string.IsNullOrEmpty(previousPage.Layout))
{
var layoutPage = _pageFactory.CreateInstance(previousPage.Layout);

View File

@ -31,23 +31,14 @@ namespace Microsoft.AspNet.Mvc.Razor
};
private readonly IRazorPageFactory _pageFactory;
private readonly ITypeActivator _typeActivator;
private readonly IServiceProvider _serviceProvider;
/// <summary>
/// Initializes a new instance of the RazorViewEngine class.
/// Initializes a new instance of the <see cref="RazorViewEngine" /> class.
/// </summary>
/// <param name="pageFactory">The page factory used for creating <see cref="IRazorPage"/>.</param>
/// <param name="pageActivator">Activator for activated instances of <see cref="IRazorPage"/>.</param>
/// <param name="viewStartProvider">The provider used to provide instances of ViewStarts applicable to the
/// page being rendered.</param>
public RazorViewEngine(IRazorPageFactory pageFactory,
ITypeActivator typeActivator,
IServiceProvider serviceProvider)
/// <param name="pageFactory">The page factory used for creating <see cref="IRazorPage"/> instances.</param>
public RazorViewEngine(IRazorPageFactory pageFactory)
{
_pageFactory = pageFactory;
_typeActivator = typeActivator;
_serviceProvider = serviceProvider;
}
public IEnumerable<string> ViewLocationFormats
@ -83,7 +74,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var page = _pageFactory.CreateInstance(viewName);
if (page != null)
{
return CreateFoundResult(page, viewName, partial);
return CreateFoundResult(context, page, viewName, partial);
}
}
return ViewEngineResult.NotFound(viewName, new[] { viewName });
@ -100,7 +91,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var page = _pageFactory.CreateInstance(path);
if (page != null)
{
return CreateFoundResult(page, path, partial);
return CreateFoundResult(context, page, path, partial);
}
}
@ -108,10 +99,17 @@ namespace Microsoft.AspNet.Mvc.Razor
}
}
private ViewEngineResult CreateFoundResult(IRazorPage page, string viewName, bool partial)
private ViewEngineResult CreateFoundResult(ActionContext actionContext,
IRazorPage page,
string viewName,
bool partial)
{
var view = _typeActivator.CreateInstance<RazorView>(_serviceProvider, page);
view.ExecuteViewHierarchy = !partial;
// A single request could result in creating multiple IRazorView instances (for partials, view components)
// 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.GetService<IRazorView>();
view.Contextualize(page, partial);
return ViewEngineResult.Found(viewName, view);
}

View File

@ -159,6 +159,9 @@
<data name="ViewContextMustBeSet" xml:space="preserve">
<value>'{0} must be set to access '{1}'.</value>
</data>
<data name="ViewMustBeContextualized" xml:space="preserve">
<value>The '{0}' method must be called before '{1}' can be invoked.</value>
</data>
<data name="View_MethodCannotBeCalled" xml:space="preserve">
<value>The method '{0}' cannot be invoked by this view.</value>
</data>

View File

@ -50,6 +50,7 @@ namespace Microsoft.AspNet.Mvc
yield return describe.Singleton<IViewEngineProvider, DefaultViewEngineProvider>();
yield return describe.Scoped<ICompositeViewEngine, CompositeViewEngine>();
yield return describe.Singleton<IViewStartProvider, ViewStartProvider>();
yield return describe.Transient<IRazorView, RazorView>();
yield return describe.Singleton<IRazorPageActivator, RazorPageActivator>();
// Virtual path view factory needs to stay scoped so views can get get scoped services.

View File

@ -3,10 +3,9 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.PipelineCore;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;
using Moq;
using Xunit;
@ -183,17 +182,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
var page = Mock.Of<IRazorPage>();
pageFactory.Setup(p => p.CreateInstance(It.IsAny<string>()))
.Returns(Mock.Of<IRazorPage>());
var serviceProvider = new Mock<IServiceProvider>();
serviceProvider.Setup(p => p.GetService(typeof(IRazorPageFactory)))
.Returns(pageFactory.Object);
serviceProvider.Setup(p => p.GetService(typeof(IRazorPageActivator)))
.Returns(Mock.Of<IRazorPageActivator>());
serviceProvider.Setup(p => p.GetService(typeof(IViewStartProvider)))
.Returns(Mock.Of<IViewStartProvider>());
var viewEngine = new RazorViewEngine(pageFactory.Object,
new TypeActivator(),
serviceProvider.Object);
var viewEngine = new RazorViewEngine(pageFactory.Object);
var context = GetActionContext(_controllerTestContext);
// Act
@ -201,7 +190,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
// Assert
Assert.True(result.Success);
Assert.IsType<RazorView>(result.View);
Assert.IsAssignableFrom<IRazorView>(result.View);
Assert.Equal("/Views/bar/test-view.cshtml", result.ViewName);
}
@ -211,16 +200,20 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
pageFactory.Setup(vpf => vpf.CreateInstance(It.IsAny<string>()))
.Returns<RazorPage>(null);
var viewEngine = new RazorViewEngine(pageFactory.Object,
Mock.Of<ITypeActivator>(),
Mock.Of<IServiceProvider>());
var viewEngine = new RazorViewEngine(pageFactory.Object);
return viewEngine;
}
private static ActionContext GetActionContext(IDictionary<string, object> routeValues)
private static ActionContext GetActionContext(IDictionary<string, object> routeValues,
IRazorView razorView = null)
{
var httpContext = Mock.Of<HttpContext>();
var httpContext = new DefaultHttpContext();
var serviceProvider = new Mock<IServiceProvider>();
serviceProvider.Setup(p => p.GetService(typeof(IRazorView)))
.Returns(razorView ?? Mock.Of<IRazorView>());
httpContext.RequestServices = serviceProvider.Object;
var routeData = new RouteData { Values = routeValues };
return new ActionContext(httpContext, routeData, new ActionDescriptor());
}

View File

@ -16,7 +16,24 @@ namespace Microsoft.AspNet.Mvc.Razor
private const string LayoutPath = "~/Shared/_Layout.cshtml";
[Fact]
public async Task RenderAsync_WithoutHierarchy_DoesNotCreateOutputBuffer()
public async Task RenderAsync_ThrowsIfContextualizeHasNotBeenInvoked()
{
// Arrange
var page = new TestableRazorPage(v => { });
var view = new RazorView(Mock.Of<IRazorPageFactory>(),
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider());
var viewContext = CreateViewContext(view);
var expected = viewContext.Writer;
// Act and Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => view.RenderAsync(viewContext));
Assert.Equal("The 'Contextualize' method must be called before 'RenderAsync' can be invoked.",
ex.Message);
}
[Fact]
public async Task RenderAsync_AsPartial_DoesNotCreateOutputBuffer()
{
// Arrange
TextWriter actual = null;
@ -27,8 +44,8 @@ namespace Microsoft.AspNet.Mvc.Razor
});
var view = new RazorView(Mock.Of<IRazorPageFactory>(),
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
page);
CreateViewStartProvider());
view.Contextualize(page, isPartial: true);
var viewContext = CreateViewContext(view);
var expected = viewContext.Writer;
@ -41,7 +58,7 @@ namespace Microsoft.AspNet.Mvc.Razor
}
[Fact]
public async Task RenderAsync_WithoutHierarchy_ActivatesViews_WithThePassedInViewContext()
public async Task RenderAsync_AsPartial_ActivatesViews_WithThePassedInViewContext()
{
// Arrange
var viewData = new ViewDataDictionary(Mock.Of<IModelMetadataProvider>());
@ -54,8 +71,8 @@ namespace Microsoft.AspNet.Mvc.Razor
var view = new RazorView(Mock.Of<IRazorPageFactory>(),
activator.Object,
CreateViewStartProvider(),
page);
CreateViewStartProvider());
view.Contextualize(page, isPartial: true);
var viewContext = CreateViewContext(view);
var expectedWriter = viewContext.Writer;
activator.Setup(a => a.Activate(page, It.IsAny<ViewContext>()))
@ -75,7 +92,7 @@ namespace Microsoft.AspNet.Mvc.Razor
}
[Fact]
public async Task RenderAsync_WithoutHierarchy_ActivatesViews()
public async Task RenderAsync_AsPartial_ActivatesViews()
{
// Arrange
var page = new TestableRazorPage(v => { });
@ -84,8 +101,8 @@ namespace Microsoft.AspNet.Mvc.Razor
.Verifiable();
var view = new RazorView(Mock.Of<IRazorPageFactory>(),
activator.Object,
CreateViewStartProvider(),
page);
CreateViewStartProvider());
view.Contextualize(page, isPartial: true);
var viewContext = CreateViewContext(view);
// Act
@ -96,7 +113,7 @@ namespace Microsoft.AspNet.Mvc.Razor
}
[Fact]
public async Task RenderAsync_WithoutHierarchy_DoesNotExecuteLayoutOrViewStartPages()
public async Task RenderAsync_AsPartial_DoesNotExecuteLayoutOrViewStartPages()
{
var page = new TestableRazorPage(v =>
{
@ -106,8 +123,8 @@ namespace Microsoft.AspNet.Mvc.Razor
var viewStartProvider = CreateViewStartProvider();
var view = new RazorView(pageFactory.Object,
Mock.Of<IRazorPageActivator>(),
viewStartProvider,
page);
viewStartProvider);
view.Contextualize(page, isPartial: true);
var viewContext = CreateViewContext(view);
// Act
@ -119,7 +136,7 @@ namespace Microsoft.AspNet.Mvc.Razor
}
[Fact]
public async Task RenderAsync_WithHierarchy_CreatesOutputBuffer()
public async Task RenderAsync_CreatesOutputBuffer()
{
// Arrange
TextWriter actual = null;
@ -129,11 +146,8 @@ namespace Microsoft.AspNet.Mvc.Razor
});
var view = new RazorView(Mock.Of<IRazorPageFactory>(),
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
page)
{
ExecuteViewHierarchy = true
};
CreateViewStartProvider());
view.Contextualize(page, isPartial: false);
var viewContext = CreateViewContext(view);
var original = viewContext.Writer;
@ -146,7 +160,7 @@ namespace Microsoft.AspNet.Mvc.Razor
}
[Fact]
public async Task RenderAsync_WithHierarchy_CopiesBufferedContentToOutput()
public async Task RenderAsync_CopiesBufferedContentToOutput()
{
// Arrange
var page = new TestableRazorPage(v =>
@ -155,11 +169,8 @@ namespace Microsoft.AspNet.Mvc.Razor
});
var view = new RazorView(Mock.Of<IRazorPageFactory>(),
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
page)
{
ExecuteViewHierarchy = true
};
CreateViewStartProvider());
view.Contextualize(page, isPartial: false);
var viewContext = CreateViewContext(view);
var original = viewContext.Writer;
@ -171,7 +182,7 @@ namespace Microsoft.AspNet.Mvc.Razor
}
[Fact]
public async Task RenderAsync_WithHierarchy_ActivatesPages()
public async Task RenderAsync_ActivatesPages()
{
// Arrange
var page = new TestableRazorPage(v =>
@ -183,12 +194,9 @@ namespace Microsoft.AspNet.Mvc.Razor
.Verifiable();
var view = new RazorView(Mock.Of<IRazorPageFactory>(),
activator.Object,
CreateViewStartProvider(),
page)
{
ExecuteViewHierarchy = true
};
CreateViewStartProvider());
view.Contextualize(page, isPartial: false);
var viewContext = CreateViewContext(view);
// Act
@ -199,7 +207,7 @@ namespace Microsoft.AspNet.Mvc.Razor
}
[Fact]
public async Task RenderAsync_WithHierarchy_ExecutesViewStart()
public async Task RenderAsync_ExecutesViewStart()
{
// Arrange
var actualLayoutPath = "";
@ -228,11 +236,8 @@ namespace Microsoft.AspNet.Mvc.Razor
.Verifiable();
var view = new RazorView(Mock.Of<IRazorPageFactory>(),
activator.Object,
CreateViewStartProvider(viewStart1, viewStart2),
page)
{
ExecuteViewHierarchy = true
};
CreateViewStartProvider(viewStart1, viewStart2));
view.Contextualize(page, isPartial: false);
var viewContext = CreateViewContext(view);
// Act
@ -243,7 +248,7 @@ namespace Microsoft.AspNet.Mvc.Razor
}
[Fact]
public async Task RenderAsync_WithHierarchy_ExecutesLayoutPages()
public async Task RenderAsync_ExecutesLayoutPages()
{
// Arrange
var expected =
@ -285,11 +290,8 @@ foot-content";
var view = new RazorView(pageFactory.Object,
activator.Object,
CreateViewStartProvider(),
page)
{
ExecuteViewHierarchy = true
};
CreateViewStartProvider());
view.Contextualize(page, isPartial: false);
var viewContext = CreateViewContext(view);
// Act
@ -302,7 +304,7 @@ foot-content";
}
[Fact]
public async Task RenderAsync_WithHierarchy_ThrowsIfSectionsWereDefinedButNotRendered()
public async Task RenderAsync_ThrowsIfSectionsWereDefinedButNotRendered()
{
// Arrange
var page = new TestableRazorPage(v =>
@ -321,11 +323,8 @@ foot-content";
var view = new RazorView(pageFactory.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
page)
{
ExecuteViewHierarchy = true
};
CreateViewStartProvider());
view.Contextualize(page, isPartial: false);
var viewContext = CreateViewContext(view);
// Act and Assert
@ -334,7 +333,7 @@ foot-content";
}
[Fact]
public async Task RenderAsync_WithHierarchy_ThrowsIfBodyWasNotRendered()
public async Task RenderAsync_ThrowsIfBodyWasNotRendered()
{
// Arrange
var page = new TestableRazorPage(v =>
@ -350,11 +349,8 @@ foot-content";
var view = new RazorView(pageFactory.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
page)
{
ExecuteViewHierarchy = true
};
CreateViewStartProvider());
view.Contextualize(page, isPartial: false);
var viewContext = CreateViewContext(view);
// Act and Assert
@ -363,7 +359,7 @@ foot-content";
}
[Fact]
public async Task RenderAsync_WithHierarchy_ExecutesNestedLayoutPages()
public async Task RenderAsync_ExecutesNestedLayoutPages()
{
// Arrange
var expected =
@ -407,11 +403,8 @@ body-content";
var view = new RazorView(pageFactory.Object,
Mock.Of<IRazorPageActivator>(),
CreateViewStartProvider(),
page)
{
ExecuteViewHierarchy = true
};
CreateViewStartProvider());
view.Contextualize(page, isPartial: false);
var viewContext = CreateViewContext(view);
// Act