diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorPagePropertyActivator.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorPagePropertyActivator.cs index 368738c448..e3958d36ce 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorPagePropertyActivator.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorPagePropertyActivator.cs @@ -30,19 +30,21 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal { _metadataProvider = metadataProvider; + // In the absence of a model on the current type, we'll attempt to use ViewDataDictionary on the current type. + var viewDataDictionaryModelType = modelType ?? typeof(object); - if (modelType != null) + if (viewDataDictionaryModelType != null) { - _viewDataDictionaryType = typeof(ViewDataDictionary<>).MakeGenericType(modelType); - _rootFactory = ViewDataDictionaryFactory.CreateFactory(modelType.GetTypeInfo()); - _nestedFactory = ViewDataDictionaryFactory.CreateNestedFactory(modelType.GetTypeInfo()); + _viewDataDictionaryType = typeof(ViewDataDictionary<>).MakeGenericType(viewDataDictionaryModelType); + _rootFactory = ViewDataDictionaryFactory.CreateFactory(viewDataDictionaryModelType.GetTypeInfo()); + _nestedFactory = ViewDataDictionaryFactory.CreateNestedFactory(viewDataDictionaryModelType.GetTypeInfo()); } _propertyActivators = PropertyActivator.GetPropertiesToActivate( - pageType, - typeof(RazorInjectAttribute), - propertyInfo => CreateActivateInfo(propertyInfo, propertyValueAccessors), - includeNonPublic: true); + pageType, + typeof(RazorInjectAttribute), + propertyInfo => CreateActivateInfo(propertyInfo, propertyValueAccessors), + includeNonPublic: true); } public void Activate(object page, ViewContext context) @@ -64,7 +66,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal } } - private ViewDataDictionary CreateViewDataDictionary(ViewContext context) + // Internal for unit testing. + internal ViewDataDictionary CreateViewDataDictionary(ViewContext context) { // Create a ViewDataDictionary if the ViewContext.ViewData is not set or the type of // ViewContext.ViewData is an incompatible type. diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageFactoryProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageFactoryProvider.cs index 876541f9a1..ed068bfd8c 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageFactoryProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageFactoryProvider.cs @@ -42,11 +42,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure public virtual Func CreatePageFactory(CompiledPageActionDescriptor actionDescriptor) { - if (!typeof(Page).GetTypeInfo().IsAssignableFrom(actionDescriptor.PageTypeInfo)) + if (!typeof(PageBase).GetTypeInfo().IsAssignableFrom(actionDescriptor.PageTypeInfo)) { throw new InvalidOperationException(Resources.FormatActivatedInstance_MustBeAnInstanceOf( _pageActivator.GetType().FullName, - typeof(Page).FullName)); + typeof(PageBase).FullName)); } var activatorFactory = _pageActivator.CreateActivator(actionDescriptor); @@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure return (pageContext, viewContext) => { - var page = (Page)activatorFactory(pageContext, viewContext); + var page = (PageBase)activatorFactory(pageContext, viewContext); page.PageContext = pageContext; page.Path = pageContext.ActionDescriptor.RelativePath; page.ViewContext = viewContext; diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs index 8f282e7f37..ca3c962bf3 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal private Dictionary _arguments; private HandlerMethodDescriptor _handler; - private Page _page; + private PageBase _page; private object _pageModel; private ViewContext _viewContext; @@ -124,7 +124,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal _htmlHelperOptions); _viewContext.ExecutingFilePath = _pageContext.ActionDescriptor.RelativePath; - _page = (Page)CacheEntry.PageFactory(_pageContext, _viewContext); + _page = (PageBase)CacheEntry.PageFactory(_pageContext, _viewContext); if (_actionDescriptor.ModelTypeInfo == _actionDescriptor.PageTypeInfo) { @@ -271,7 +271,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal if (_page == null) { - _page = (Page)CacheEntry.PageFactory(_pageContext, _viewContext); + _page = (PageBase)CacheEntry.PageFactory(_pageContext, _viewContext); } pageResult.Page = _page; diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs index c170620079..4e7dc2f02a 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs @@ -1189,6 +1189,26 @@ Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore._InjectedP Assert.StartsWith(expected, responseContent.Trim()); } + [Fact] + public Task InheritsOnViewImportsWorksForPagesWithoutModel() + => InheritsOnViewImportsWorks("Pages/CustomBaseType/Page"); + + [Fact] + public Task InheritsOnViewImportsWorksForPagesWithModel() + => InheritsOnViewImportsWorks("Pages/CustomBaseType/PageWithModel"); + + private async Task InheritsOnViewImportsWorks(string path) + { + // Arrange + var expected = "RazorPagesWebSite.CustomPageBase"; + + // Act + var response = await Client.GetStringAsync(path); + + // Assert + Assert.Equal(expected, response.Trim()); + } + private async Task AddAntiforgeryHeaders(HttpRequestMessage request) { var getResponse = await Client.GetAsync(request.RequestUri); diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorPagePropertyActivatorTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorPagePropertyActivatorTest.cs new file mode 100644 index 0000000000..779fc7df26 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorPagePropertyActivatorTest.cs @@ -0,0 +1,197 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.Internal +{ + public class RazorPagePropertyActivatorTest + { + [Fact] + public void CreateViewDataDictionary_MakesNewInstance_WhenValueOnContextIsNull() + { + // Arrange + var activator = new RazorPagePropertyActivator( + typeof(TestPage), + typeof(TestModel), + new TestModelMetadataProvider(), + propertyValueAccessors: null); + var viewContext = new ViewContext(); + + // Act + var viewDataDictionary = activator.CreateViewDataDictionary(viewContext); + + // Assert + Assert.NotNull(viewDataDictionary); + Assert.IsType>(viewDataDictionary); + } + + [Fact] + public void CreateViewDataDictionary_MakesNewInstanceWithObjectModelType_WhenValueOnContextAndModelTypeAreNull() + { + // Arrange + var activator = new RazorPagePropertyActivator( + typeof(TestPage), + modelType: null, + metadataProvider: new TestModelMetadataProvider(), + propertyValueAccessors: null); + var viewContext = new ViewContext(); + + // Act + var viewDataDictionary = activator.CreateViewDataDictionary(viewContext); + + // Assert + Assert.NotNull(viewDataDictionary); + Assert.IsType>(viewDataDictionary); + } + + [Fact] + public void CreateViewDataDictionary_CreatesNestedViewDataDictionary_WhenContextInstanceIsNonGeneric() + { + // Arrange + var modelMetadataProvider = new TestModelMetadataProvider(); + var activator = new RazorPagePropertyActivator( + typeof(TestPage), + modelType: typeof(TestModel), + metadataProvider: modelMetadataProvider, + propertyValueAccessors: null); + var original = new ViewDataDictionary(modelMetadataProvider, new ModelStateDictionary()) + { + { "test-key", "test-value" }, + }; + var viewContext = new ViewContext + { + ViewData = original, + }; + + // Act + var viewDataDictionary = activator.CreateViewDataDictionary(viewContext); + + // Assert + Assert.NotNull(viewDataDictionary); + Assert.NotSame(original, viewDataDictionary); + Assert.IsType>(viewDataDictionary); + Assert.Equal("test-value", viewDataDictionary["test-key"]); + } + + [Fact] + public void CreateViewDataDictionary_CreatesNestedViewDataDictionary_WhenModelTypeDoesNotMatch() + { + // Arrange + var modelMetadataProvider = new TestModelMetadataProvider(); + var activator = new RazorPagePropertyActivator( + typeof(TestPage), + modelType: typeof(TestModel), + metadataProvider: modelMetadataProvider, + propertyValueAccessors: null); + var original = new ViewDataDictionary(modelMetadataProvider, new ModelStateDictionary()) + { + { "test-key", "test-value" }, + }; + var viewContext = new ViewContext + { + ViewData = original, + }; + + // Act + var viewDataDictionary = activator.CreateViewDataDictionary(viewContext); + + // Assert + Assert.NotNull(viewDataDictionary); + Assert.NotSame(original, viewDataDictionary); + Assert.IsType>(viewDataDictionary); + Assert.Equal("test-value", viewDataDictionary["test-key"]); + } + + [Fact] + public void CreateViewDataDictionary_CreatesNestedViewDataDictionary_WhenNullModelTypeDoesNotMatch() + { + // Arrange + var modelMetadataProvider = new TestModelMetadataProvider(); + var activator = new RazorPagePropertyActivator( + typeof(TestPage), + modelType: null, + metadataProvider: modelMetadataProvider, + propertyValueAccessors: null); + var original = new ViewDataDictionary(modelMetadataProvider, new ModelStateDictionary()) + { + { "test-key", "test-value" }, + }; + var viewContext = new ViewContext + { + ViewData = original, + }; + + // Act + var viewDataDictionary = activator.CreateViewDataDictionary(viewContext); + + // Assert + Assert.NotNull(viewDataDictionary); + Assert.NotSame(original, viewDataDictionary); + Assert.IsType>(viewDataDictionary); + Assert.Equal("test-value", viewDataDictionary["test-key"]); + } + + [Fact] + public void CreateViewDataDictionary_ReturnsInstanceOnContext_IfModelTypeMatches() + { + // Arrange + var modelMetadataProvider = new TestModelMetadataProvider(); + var activator = new RazorPagePropertyActivator( + typeof(TestPage), + modelType: typeof(TestModel), + metadataProvider: modelMetadataProvider, + propertyValueAccessors: null); + var original = new ViewDataDictionary(modelMetadataProvider, new ModelStateDictionary()) + { + { "test-key", "test-value" }, + }; + var viewContext = new ViewContext + { + ViewData = original, + }; + + // Act + var viewDataDictionary = activator.CreateViewDataDictionary(viewContext); + + // Assert + Assert.NotNull(viewDataDictionary); + Assert.Same(original, viewDataDictionary); + } + + [Fact] + public void CreateViewDataDictionary_ReturnsInstanceOnContext_WithNullModelType() + { + // Arrange + var modelMetadataProvider = new TestModelMetadataProvider(); + var activator = new RazorPagePropertyActivator( + typeof(TestPage), + modelType: null, + metadataProvider: modelMetadataProvider, + propertyValueAccessors: null); + var original = new ViewDataDictionary(modelMetadataProvider, new ModelStateDictionary()); + var viewContext = new ViewContext + { + ViewData = original, + }; + + // Act + var viewDataDictionary = activator.CreateViewDataDictionary(viewContext); + + // Assert + Assert.NotNull(viewDataDictionary); + Assert.Same(original, viewDataDictionary); + } + + private class TestPage + { + } + + private class TestModel + { + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageFactoryProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageFactoryProviderTest.cs index e248494282..cc6a78e72d 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageFactoryProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageFactoryProviderTest.cs @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure // Act & Assert var ex = Assert.Throws(() => factoryProvider.CreatePageFactory(descriptor)); Assert.Equal( - $"Page created by '{pageActivator.GetType()}' must be an instance of '{typeof(Page)}'.", + $"Page created by '{pageActivator.GetType()}' must be an instance of '{typeof(PageBase)}'.", ex.Message); } diff --git a/test/WebSites/RazorPagesWebSite/Pages/CustomBaseType/CustomPageBase.cs b/test/WebSites/RazorPagesWebSite/Pages/CustomBaseType/CustomPageBase.cs new file mode 100644 index 0000000000..31d3522032 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/CustomBaseType/CustomPageBase.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorPagesWebSite +{ + public abstract class CustomPageBase : PageBase + { + } +} diff --git a/test/WebSites/RazorPagesWebSite/Pages/CustomBaseType/Page.cshtml b/test/WebSites/RazorPagesWebSite/Pages/CustomBaseType/Page.cshtml new file mode 100644 index 0000000000..5bdc8374e0 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/CustomBaseType/Page.cshtml @@ -0,0 +1,2 @@ +@page +@GetType().BaseType.FullName \ No newline at end of file diff --git a/test/WebSites/RazorPagesWebSite/Pages/CustomBaseType/PageWithModel.cs b/test/WebSites/RazorPagesWebSite/Pages/CustomBaseType/PageWithModel.cs new file mode 100644 index 0000000000..c10d51930e --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/CustomBaseType/PageWithModel.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorPagesWebSite +{ + public class PageWithModel : PageModel + { + } +} diff --git a/test/WebSites/RazorPagesWebSite/Pages/CustomBaseType/PageWithModel.cshtml b/test/WebSites/RazorPagesWebSite/Pages/CustomBaseType/PageWithModel.cshtml new file mode 100644 index 0000000000..9b6bfb0727 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/CustomBaseType/PageWithModel.cshtml @@ -0,0 +1,3 @@ +@page +@model PageWithModel +@GetType().BaseType.FullName \ No newline at end of file diff --git a/test/WebSites/RazorPagesWebSite/Pages/CustomBaseType/_ViewImports.cshtml b/test/WebSites/RazorPagesWebSite/Pages/CustomBaseType/_ViewImports.cshtml new file mode 100644 index 0000000000..8085b8cd3f --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/CustomBaseType/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@using RazorPagesWebSite +@inherits CustomPageBase diff --git a/test/WebSites/RazorPagesWebSite/Pages/CustomBaseType/_ViewStart.cshtml b/test/WebSites/RazorPagesWebSite/Pages/CustomBaseType/_ViewStart.cshtml new file mode 100644 index 0000000000..dd100cf854 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/CustomBaseType/_ViewStart.cshtml @@ -0,0 +1 @@ +@{ Layout = "../Shared/_CustomBaseTypeLayout"; } \ No newline at end of file diff --git a/test/WebSites/RazorPagesWebSite/Pages/Shared/_CustomBaseTypeLayout.cshtml b/test/WebSites/RazorPagesWebSite/Pages/Shared/_CustomBaseTypeLayout.cshtml new file mode 100644 index 0000000000..fd08733813 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Shared/_CustomBaseTypeLayout.cshtml @@ -0,0 +1 @@ +@RenderBody()