diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorView.cs new file mode 100644 index 0000000000..7f8f411a7e --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorView.cs @@ -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 +{ + /// + /// Represents the contract for an that executes as part of its + /// execution. + /// + public interface IRazorView : IView + { + /// + /// Contextualizes the current instance of the providing it with the + /// to execute. + /// + /// The instance to execute. + /// Determines if the view is to be executed as a partial. + void Contextualize(IRazorPage razorPage, bool isPartial); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs index 9235868bbc..dd8c6568fb 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs @@ -234,6 +234,22 @@ namespace Microsoft.AspNet.Mvc.Razor return string.Format(CultureInfo.CurrentCulture, GetString("ViewContextMustBeSet"), p0, p1); } + /// + /// The '{0}' method must be called before '{1}' can be invoked. + /// + internal static string ViewMustBeContextualized + { + get { return GetString("ViewMustBeContextualized"); } + } + + /// + /// The '{0}' method must be called before '{1}' can be invoked. + /// + internal static string FormatViewMustBeContextualized(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ViewMustBeContextualized"), p0, p1); + } + /// /// The method '{0}' cannot be invoked by this view. /// diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs index 712bf61b34..d2587b5bd7 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs @@ -3,56 +3,61 @@ using System; using System.Threading.Tasks; -using Microsoft.AspNet.Mvc.Rendering; namespace Microsoft.AspNet.Mvc.Razor { /// - /// Represents a that executes one or more instances as part of - /// view rendering. + /// Default implementation for that executes one or more + /// instances as part of view rendering. /// - 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; /// /// Initializes a new instance of RazorView /// - /// The page to execute /// The view factory used to instantiate layout and _ViewStart pages. /// The used to activate pages. - public RazorView([NotNull] IRazorPageFactory pageFactory, - [NotNull] IRazorPageActivator pageActivator, - [NotNull] IViewStartProvider viewStartProvider, - [NotNull] IRazorPage page) + /// The used for discovery of _ViewStart + /// pages + public RazorView(IRazorPageFactory pageFactory, + IRazorPageActivator pageActivator, + IViewStartProvider viewStartProvider) { _pageFactory = pageFactory; _pageActivator = pageActivator; _viewStartProvider = viewStartProvider; - _page = page; } - /// - /// Gets or sets a value that determines if the view hierarchy is executed as part of - /// executing the instance. The view hierarchy involves _ViewStart - /// and Layout pages. - /// - public bool ExecuteViewHierarchy { get; set; } + /// + public virtual void Contextualize(IRazorPage razorPage, bool isPartial) + { + _razorPage = razorPage; + _isPartial = isPartial; + } /// 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); diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs index a9ff53396f..6b476b4d50 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs @@ -31,23 +31,14 @@ namespace Microsoft.AspNet.Mvc.Razor }; private readonly IRazorPageFactory _pageFactory; - private readonly ITypeActivator _typeActivator; - private readonly IServiceProvider _serviceProvider; /// - /// Initializes a new instance of the RazorViewEngine class. + /// Initializes a new instance of the class. /// - /// The page factory used for creating . - /// Activator for activated instances of . - /// The provider used to provide instances of ViewStarts applicable to the - /// page being rendered. - public RazorViewEngine(IRazorPageFactory pageFactory, - ITypeActivator typeActivator, - IServiceProvider serviceProvider) + /// The page factory used for creating instances. + public RazorViewEngine(IRazorPageFactory pageFactory) { _pageFactory = pageFactory; - _typeActivator = typeActivator; - _serviceProvider = serviceProvider; } public IEnumerable 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(_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(); + view.Contextualize(page, partial); return ViewEngineResult.Found(viewName, view); } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx index 44185c3b9c..39439205d0 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx @@ -159,6 +159,9 @@ '{0} must be set to access '{1}'. + + The '{0}' method must be called before '{1}' can be invoked. + The method '{0}' cannot be invoked by this view. diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index beeb20bc60..d91082535e 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -50,6 +50,7 @@ namespace Microsoft.AspNet.Mvc yield return describe.Singleton(); yield return describe.Scoped(); yield return describe.Singleton(); + yield return describe.Transient(); yield return describe.Singleton(); // Virtual path view factory needs to stay scoped so views can get get scoped services. diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs index 6fd7bbe963..9fc9744e0c 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs @@ -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(); pageFactory.Setup(p => p.CreateInstance(It.IsAny())) .Returns(Mock.Of()); - - var serviceProvider = new Mock(); - serviceProvider.Setup(p => p.GetService(typeof(IRazorPageFactory))) - .Returns(pageFactory.Object); - serviceProvider.Setup(p => p.GetService(typeof(IRazorPageActivator))) - .Returns(Mock.Of()); - serviceProvider.Setup(p => p.GetService(typeof(IViewStartProvider))) - .Returns(Mock.Of()); - 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(result.View); + Assert.IsAssignableFrom(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())) .Returns(null); - var viewEngine = new RazorViewEngine(pageFactory.Object, - Mock.Of(), - Mock.Of()); + var viewEngine = new RazorViewEngine(pageFactory.Object); return viewEngine; } - private static ActionContext GetActionContext(IDictionary routeValues) + private static ActionContext GetActionContext(IDictionary routeValues, + IRazorView razorView = null) { - var httpContext = Mock.Of(); + var httpContext = new DefaultHttpContext(); + var serviceProvider = new Mock(); + serviceProvider.Setup(p => p.GetService(typeof(IRazorView))) + .Returns(razorView ?? Mock.Of()); + + httpContext.RequestServices = serviceProvider.Object; var routeData = new RouteData { Values = routeValues }; return new ActionContext(httpContext, routeData, new ActionDescriptor()); } diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs index 0e0ede1156..00ea8e44ed 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewTest.cs @@ -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(), + Mock.Of(), + CreateViewStartProvider()); + var viewContext = CreateViewContext(view); + var expected = viewContext.Writer; + + // Act and Assert + var ex = await Assert.ThrowsAsync(() => 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(), Mock.Of(), - 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()); @@ -54,8 +71,8 @@ namespace Microsoft.AspNet.Mvc.Razor var view = new RazorView(Mock.Of(), 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())) @@ -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(), 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(), - 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(), Mock.Of(), - 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(), Mock.Of(), - 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(), 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(), 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(), - 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(), - 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(), - CreateViewStartProvider(), - page) - { - ExecuteViewHierarchy = true - }; + CreateViewStartProvider()); + view.Contextualize(page, isPartial: false); var viewContext = CreateViewContext(view); // Act