Ensure PageContext.ViewData and ViewContext.ViewData are the same instance
Fixes #7675
This commit is contained in:
parent
9d951325b2
commit
b62499e02c
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
|
{
|
||||||
|
internal interface IModelTypeProvider
|
||||||
|
{
|
||||||
|
Type GetModelType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Mvc.Razor.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||||
|
using Microsoft.Extensions.Internal;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Razor
|
namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
{
|
{
|
||||||
|
|
@ -19,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
{
|
{
|
||||||
// Name of the "public TModel Model" property on RazorPage<TModel>
|
// Name of the "public TModel Model" property on RazorPage<TModel>
|
||||||
private const string ModelPropertyName = "Model";
|
private const string ModelPropertyName = "Model";
|
||||||
private readonly ConcurrentDictionary<Type, RazorPagePropertyActivator> _activationInfo;
|
private readonly ConcurrentDictionary<CacheKey, RazorPagePropertyActivator> _activationInfo;
|
||||||
private readonly IModelMetadataProvider _metadataProvider;
|
private readonly IModelMetadataProvider _metadataProvider;
|
||||||
|
|
||||||
// Value accessors for common singleton properties activated in a RazorPage.
|
// Value accessors for common singleton properties activated in a RazorPage.
|
||||||
|
|
@ -36,7 +37,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
HtmlEncoder htmlEncoder,
|
HtmlEncoder htmlEncoder,
|
||||||
IModelExpressionProvider modelExpressionProvider)
|
IModelExpressionProvider modelExpressionProvider)
|
||||||
{
|
{
|
||||||
_activationInfo = new ConcurrentDictionary<Type, RazorPagePropertyActivator>();
|
_activationInfo = new ConcurrentDictionary<CacheKey, RazorPagePropertyActivator>();
|
||||||
_metadataProvider = metadataProvider;
|
_metadataProvider = metadataProvider;
|
||||||
|
|
||||||
_propertyAccessors = new RazorPagePropertyActivator.PropertyValueAccessors
|
_propertyAccessors = new RazorPagePropertyActivator.PropertyValueAccessors
|
||||||
|
|
@ -62,26 +63,76 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
throw new ArgumentNullException(nameof(context));
|
throw new ArgumentNullException(nameof(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var propertyActivator = GetOrAddCacheEntry(page);
|
||||||
|
propertyActivator.Activate(page, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal RazorPagePropertyActivator GetOrAddCacheEntry(IRazorPage page)
|
||||||
|
{
|
||||||
var pageType = page.GetType();
|
var pageType = page.GetType();
|
||||||
RazorPagePropertyActivator propertyActivator;
|
Type providedModelType = null;
|
||||||
if (!_activationInfo.TryGetValue(pageType, out propertyActivator))
|
if (page is IModelTypeProvider modelTypeProvider)
|
||||||
|
{
|
||||||
|
providedModelType = modelTypeProvider.GetModelType();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only need to vary by providedModelType since it varies at runtime. Defined model type
|
||||||
|
// is synonymous with the pageType and consequently does not need to be accounted for in the cache key.
|
||||||
|
var cacheKey = new CacheKey(pageType, providedModelType);
|
||||||
|
if (!_activationInfo.TryGetValue(cacheKey, out var propertyActivator))
|
||||||
{
|
{
|
||||||
// Look for a property named "Model". If it is non-null, we'll assume this is
|
// Look for a property named "Model". If it is non-null, we'll assume this is
|
||||||
// the equivalent of TModel Model property on RazorPage<TModel>.
|
// the equivalent of TModel Model property on RazorPage<TModel>.
|
||||||
//
|
//
|
||||||
// Otherwise if we don't have a model property the activator will just skip setting
|
// Otherwise if we don't have a model property the activator will just skip setting
|
||||||
// the view data.
|
// the view data.
|
||||||
var modelType = pageType.GetRuntimeProperty(ModelPropertyName)?.PropertyType;
|
var modelType = providedModelType;
|
||||||
|
if (modelType == null)
|
||||||
|
{
|
||||||
|
modelType = pageType.GetRuntimeProperty(ModelPropertyName)?.PropertyType;
|
||||||
|
}
|
||||||
|
|
||||||
propertyActivator = new RazorPagePropertyActivator(
|
propertyActivator = new RazorPagePropertyActivator(
|
||||||
pageType,
|
pageType,
|
||||||
modelType,
|
modelType,
|
||||||
_metadataProvider,
|
_metadataProvider,
|
||||||
_propertyAccessors);
|
_propertyAccessors);
|
||||||
|
|
||||||
propertyActivator = _activationInfo.GetOrAdd(pageType, propertyActivator);
|
propertyActivator = _activationInfo.GetOrAdd(cacheKey, propertyActivator);
|
||||||
}
|
}
|
||||||
|
|
||||||
propertyActivator.Activate(page, context);
|
return propertyActivator;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly struct CacheKey : IEquatable<CacheKey>
|
||||||
|
{
|
||||||
|
public CacheKey(Type pageType, Type providedModelType)
|
||||||
|
{
|
||||||
|
PageType = pageType;
|
||||||
|
ProvidedModelType = providedModelType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type PageType { get; }
|
||||||
|
|
||||||
|
public Type ProvidedModelType { get; }
|
||||||
|
|
||||||
|
public bool Equals(CacheKey other)
|
||||||
|
{
|
||||||
|
return PageType == other.PageType &&
|
||||||
|
ProvidedModelType == other.ProvidedModelType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
var hashCodeCombiner = HashCodeCombiner.Start();
|
||||||
|
hashCodeCombiner.Add(PageType);
|
||||||
|
if (ProvidedModelType != null)
|
||||||
|
{
|
||||||
|
hashCodeCombiner.Add(ProvidedModelType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashCodeCombiner.CombinedHash;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -96,6 +96,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<IRazorPage> ViewStartPages { get; }
|
public IReadOnlyList<IRazorPage> ViewStartPages { get; }
|
||||||
|
|
||||||
|
internal Action<IRazorPage, ViewContext> OnAfterPageActivated { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual async Task RenderAsync(ViewContext context)
|
public virtual async Task RenderAsync(ViewContext context)
|
||||||
{
|
{
|
||||||
|
|
@ -167,6 +169,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
page.ViewContext = context;
|
page.ViewContext = context;
|
||||||
_pageActivator.Activate(page, context);
|
_pageActivator.Activate(page, context);
|
||||||
|
|
||||||
|
OnAfterPageActivated?.Invoke(page, context);
|
||||||
|
|
||||||
_diagnosticSource.BeforeViewPage(page, context);
|
_diagnosticSource.BeforeViewPage(page, context);
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -76,13 +76,29 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
}
|
}
|
||||||
|
|
||||||
var viewContext = result.Page.ViewContext;
|
var viewContext = result.Page.ViewContext;
|
||||||
|
var pageAdapter = new RazorPageAdapter(result.Page, pageContext.ActionDescriptor.DeclaredModelTypeInfo);
|
||||||
|
|
||||||
viewContext.View = new RazorView(
|
viewContext.View = new RazorView(
|
||||||
_razorViewEngine,
|
_razorViewEngine,
|
||||||
_razorPageActivator,
|
_razorPageActivator,
|
||||||
viewStarts,
|
viewStarts,
|
||||||
new RazorPageAdapter(result.Page),
|
pageAdapter,
|
||||||
_htmlEncoder,
|
_htmlEncoder,
|
||||||
_diagnosticSource);
|
_diagnosticSource)
|
||||||
|
{
|
||||||
|
OnAfterPageActivated = (page, currentViewContext) =>
|
||||||
|
{
|
||||||
|
if (page != pageAdapter)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewContext is always activated with the "right" ViewData<T> type.
|
||||||
|
// Copy that over to the PageContext since PageContext.ViewData is exposed
|
||||||
|
// as the ViewData property on the Page that the user works with.
|
||||||
|
pageContext.ViewData = currentViewContext.ViewData;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return ExecuteAsync(viewContext, result.ContentType, result.StatusCode);
|
return ExecuteAsync(viewContext, result.ContentType, result.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
//
|
//
|
||||||
// The page gets activated before handler methods run, but the RazorView will also activate
|
// The page gets activated before handler methods run, but the RazorView will also activate
|
||||||
// each page.
|
// each page.
|
||||||
public class RazorPageAdapter : IRazorPage
|
public class RazorPageAdapter : IRazorPage, IModelTypeProvider
|
||||||
{
|
{
|
||||||
private readonly RazorPageBase _page;
|
private readonly RazorPageBase _page;
|
||||||
|
private readonly Type _modelType;
|
||||||
|
|
||||||
|
[Obsolete("This constructor is obsolete and will be removed in a future version.")]
|
||||||
public RazorPageAdapter(RazorPageBase page)
|
public RazorPageAdapter(RazorPageBase page)
|
||||||
{
|
{
|
||||||
if (page == null)
|
if (page == null)
|
||||||
|
|
@ -28,6 +30,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
_page = page;
|
_page = page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RazorPageAdapter(RazorPageBase page, Type modelType)
|
||||||
|
{
|
||||||
|
_page = page ?? throw new ArgumentNullException(nameof(page));
|
||||||
|
_modelType = modelType ?? throw new ArgumentNullException(nameof(modelType));
|
||||||
|
}
|
||||||
|
|
||||||
public ViewContext ViewContext
|
public ViewContext ViewContext
|
||||||
{
|
{
|
||||||
get { return _page.ViewContext; }
|
get { return _page.ViewContext; }
|
||||||
|
|
@ -75,5 +83,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
{
|
{
|
||||||
return _page.ExecuteAsync();
|
return _page.ExecuteAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Type IModelTypeProvider.GetModelType() => _modelType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -607,6 +607,22 @@ Hello from /Pages/Shared/";
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ViewDataSetInViewStart_IsAvailableToPage()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var document = await Client.GetHtmlDocumentAsync("/ViewData/ViewDataSetInViewStart");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var valueSetInViewStart = document.RequiredQuerySelector("#valuefromviewstart").TextContent;
|
||||||
|
var valueSetInPageModel = document.RequiredQuerySelector("#valuefrompagemodel").TextContent;
|
||||||
|
var valueSetInPage = document.RequiredQuerySelector("#valuefrompage").TextContent;
|
||||||
|
|
||||||
|
Assert.Equal("Value from _ViewStart", valueSetInViewStart);
|
||||||
|
Assert.Equal("Value from Page Model", valueSetInPageModel);
|
||||||
|
Assert.Equal("Value from Page", valueSetInPage);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task AddAntiforgeryHeadersAsync(HttpRequestMessage request)
|
private async Task AddAntiforgeryHeadersAsync(HttpRequestMessage request)
|
||||||
{
|
{
|
||||||
var response = await Client.GetAsync(request.RequestUri);
|
var response = await Client.GetAsync(request.RequestUri);
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,69 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
Assert.Throws<InvalidCastException>(() => activator.Activate(instance, viewContext));
|
Assert.Throws<InvalidCastException>(() => activator.Activate(instance, viewContext));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Activate_UsesModelFromModelTypeProvider()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var activator = CreateActivator();
|
||||||
|
|
||||||
|
var viewData = new ViewDataDictionary<object>(MetadataProvider, new ModelStateDictionary())
|
||||||
|
{
|
||||||
|
{ "key", "value" },
|
||||||
|
};
|
||||||
|
var viewContext = CreateViewContext(viewData);
|
||||||
|
var page = new ModelTypeProviderRazorPage();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
activator.Activate(page, viewContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Same(viewContext.ViewData, page.ViewData);
|
||||||
|
Assert.NotSame(viewData, viewContext.ViewData);
|
||||||
|
|
||||||
|
Assert.IsType<ViewDataDictionary<Guid>>(viewContext.ViewData);
|
||||||
|
Assert.Equal("value", viewContext.ViewData["key"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetOrAddCacheEntry_CachesPages()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var activator = CreateActivator();
|
||||||
|
var page = new TestRazorPage();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result1 = activator.GetOrAddCacheEntry(page);
|
||||||
|
var result2 = activator.GetOrAddCacheEntry(page);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Same(result1, result2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetOrAddCacheEntry_VariesByModelType_IfPageIsModelTypeProvider()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var activator = CreateActivator();
|
||||||
|
var page = new ModelTypeProviderRazorPage();
|
||||||
|
|
||||||
|
// Act - 1
|
||||||
|
var result1 = activator.GetOrAddCacheEntry(page);
|
||||||
|
var result2 = activator.GetOrAddCacheEntry(page);
|
||||||
|
|
||||||
|
// Assert - 1
|
||||||
|
Assert.Same(result1, result2);
|
||||||
|
|
||||||
|
// Act - 2
|
||||||
|
page.ModelType = typeof(string);
|
||||||
|
var result3 = activator.GetOrAddCacheEntry(page);
|
||||||
|
var result4 = activator.GetOrAddCacheEntry(page);
|
||||||
|
|
||||||
|
// Assert - 2
|
||||||
|
Assert.Same(result3, result4);
|
||||||
|
Assert.NotSame(result1, result3);
|
||||||
|
}
|
||||||
|
|
||||||
private RazorPageActivator CreateActivator()
|
private RazorPageActivator CreateActivator()
|
||||||
{
|
{
|
||||||
return new RazorPageActivator(MetadataProvider, UrlHelperFactory, JsonHelper, DiagnosticSource, HtmlEncoder, ModelExpressionProvider);
|
return new RazorPageActivator(MetadataProvider, UrlHelperFactory, JsonHelper, DiagnosticSource, HtmlEncoder, ModelExpressionProvider);
|
||||||
|
|
@ -225,6 +288,21 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ModelTypeProviderRazorPage : RazorPage, IModelTypeProvider
|
||||||
|
{
|
||||||
|
[RazorInject]
|
||||||
|
public ViewDataDictionary ViewData { get; set; }
|
||||||
|
|
||||||
|
public Type ModelType { get; set; } = typeof(Guid);
|
||||||
|
|
||||||
|
public override Task ExecuteAsync()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type GetModelType() => ModelType;
|
||||||
|
}
|
||||||
|
|
||||||
private abstract class NoModelPropertyBase<TModel> : RazorPage
|
private abstract class NoModelPropertyBase<TModel> : RazorPage
|
||||||
{
|
{
|
||||||
[RazorInject]
|
[RazorInject]
|
||||||
|
|
|
||||||
|
|
@ -1747,6 +1747,49 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
Assert.Equal(expected, viewContext.Writer.ToString());
|
Assert.Equal(expected, viewContext.Writer.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RenderAsync_InvokesOnAfterPageActivated()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var viewStart = new TestableRazorPage(_ => { });
|
||||||
|
var page = new TestableRazorPage(p => { p.Layout = LayoutPath; });
|
||||||
|
var layout = new TestableRazorPage(p => { p.RenderBodyPublic(); });
|
||||||
|
var expected = new HashSet<IRazorPage>();
|
||||||
|
var onAfterPageActivatedCalled = 0;
|
||||||
|
|
||||||
|
var activated = new HashSet<IRazorPage>();
|
||||||
|
var pageActivator = new Mock<IRazorPageActivator>();
|
||||||
|
pageActivator.Setup(p => p.Activate(It.IsAny<IRazorPage>(), It.IsAny<ViewContext>()))
|
||||||
|
.Callback((IRazorPage p, ViewContext v) => activated.Add(p));
|
||||||
|
|
||||||
|
var viewEngine = new Mock<IRazorViewEngine>();
|
||||||
|
viewEngine.Setup(v => v.FindPage(It.IsAny<ActionContext>(), LayoutPath))
|
||||||
|
.Returns(new RazorPageResult(LayoutPath, layout));
|
||||||
|
|
||||||
|
var view = new RazorView(
|
||||||
|
viewEngine.Object,
|
||||||
|
pageActivator.Object,
|
||||||
|
new[] { viewStart },
|
||||||
|
page,
|
||||||
|
new HtmlTestEncoder(),
|
||||||
|
new DiagnosticListener("Microsoft.AspNetCore.Mvc.Razor"))
|
||||||
|
{
|
||||||
|
OnAfterPageActivated = AssertActivated,
|
||||||
|
};
|
||||||
|
var viewContext = CreateViewContext(view);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await view.RenderAsync(viewContext);
|
||||||
|
Assert.Equal(3, onAfterPageActivatedCalled);
|
||||||
|
|
||||||
|
void AssertActivated(IRazorPage p, ViewContext v)
|
||||||
|
{
|
||||||
|
onAfterPageActivatedCalled++;
|
||||||
|
expected.Add(p);
|
||||||
|
Assert.Equal(expected, activated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static ViewContext CreateViewContext(RazorView view)
|
private static ViewContext CreateViewContext(RazorView view)
|
||||||
{
|
{
|
||||||
var httpContext = new DefaultHttpContext();
|
var httpContext = new DefaultHttpContext();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace RazorPagesWebSite.ViewDataSetInViewStart
|
||||||
|
{
|
||||||
|
public class Index : PageModel
|
||||||
|
{
|
||||||
|
[ViewData]
|
||||||
|
public string ValueFromPageModel => "Value from Page Model";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
@page
|
||||||
|
@namespace RazorPagesWebSite.ViewDataSetInViewStart
|
||||||
|
@model Index
|
||||||
|
@{
|
||||||
|
ViewData["ValueFromPage"] = "Value from Page";
|
||||||
|
}
|
||||||
|
Sample that shows ViewData attributes being set in a PageModel.
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
@RenderBody()
|
||||||
|
<span id="valuefromviewstart">@ViewData["ValueFromViewStart"]</span>
|
||||||
|
<span id="valuefrompage">@ViewData["ValueFromPage"]</span>
|
||||||
|
<span id="valuefrompagemodel">@ViewData["ValueFromPageModel"]</span>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
@{
|
||||||
|
Layout = "_Layout";
|
||||||
|
ViewData["ValueFromViewStart"] = "Value from _ViewStart";
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue