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.Routing;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.Extensions.Internal;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Razor
|
||||
{
|
||||
|
|
@ -19,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
{
|
||||
// Name of the "public TModel Model" property on RazorPage<TModel>
|
||||
private const string ModelPropertyName = "Model";
|
||||
private readonly ConcurrentDictionary<Type, RazorPagePropertyActivator> _activationInfo;
|
||||
private readonly ConcurrentDictionary<CacheKey, RazorPagePropertyActivator> _activationInfo;
|
||||
private readonly IModelMetadataProvider _metadataProvider;
|
||||
|
||||
// Value accessors for common singleton properties activated in a RazorPage.
|
||||
|
|
@ -36,7 +37,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
HtmlEncoder htmlEncoder,
|
||||
IModelExpressionProvider modelExpressionProvider)
|
||||
{
|
||||
_activationInfo = new ConcurrentDictionary<Type, RazorPagePropertyActivator>();
|
||||
_activationInfo = new ConcurrentDictionary<CacheKey, RazorPagePropertyActivator>();
|
||||
_metadataProvider = metadataProvider;
|
||||
|
||||
_propertyAccessors = new RazorPagePropertyActivator.PropertyValueAccessors
|
||||
|
|
@ -62,26 +63,76 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var propertyActivator = GetOrAddCacheEntry(page);
|
||||
propertyActivator.Activate(page, context);
|
||||
}
|
||||
|
||||
internal RazorPagePropertyActivator GetOrAddCacheEntry(IRazorPage page)
|
||||
{
|
||||
var pageType = page.GetType();
|
||||
RazorPagePropertyActivator propertyActivator;
|
||||
if (!_activationInfo.TryGetValue(pageType, out propertyActivator))
|
||||
Type providedModelType = null;
|
||||
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
|
||||
// the equivalent of TModel Model property on RazorPage<TModel>.
|
||||
//
|
||||
// Otherwise if we don't have a model property the activator will just skip setting
|
||||
// the view data.
|
||||
var modelType = pageType.GetRuntimeProperty(ModelPropertyName)?.PropertyType;
|
||||
var modelType = providedModelType;
|
||||
if (modelType == null)
|
||||
{
|
||||
modelType = pageType.GetRuntimeProperty(ModelPropertyName)?.PropertyType;
|
||||
}
|
||||
|
||||
propertyActivator = new RazorPagePropertyActivator(
|
||||
pageType,
|
||||
modelType,
|
||||
_metadataProvider,
|
||||
_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>
|
||||
public IReadOnlyList<IRazorPage> ViewStartPages { get; }
|
||||
|
||||
internal Action<IRazorPage, ViewContext> OnAfterPageActivated { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task RenderAsync(ViewContext context)
|
||||
{
|
||||
|
|
@ -167,6 +169,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
page.ViewContext = context;
|
||||
_pageActivator.Activate(page, context);
|
||||
|
||||
OnAfterPageActivated?.Invoke(page, context);
|
||||
|
||||
_diagnosticSource.BeforeViewPage(page, context);
|
||||
|
||||
try
|
||||
|
|
|
|||
|
|
@ -76,13 +76,29 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
}
|
||||
|
||||
var viewContext = result.Page.ViewContext;
|
||||
var pageAdapter = new RazorPageAdapter(result.Page, pageContext.ActionDescriptor.DeclaredModelTypeInfo);
|
||||
|
||||
viewContext.View = new RazorView(
|
||||
_razorViewEngine,
|
||||
_razorPageActivator,
|
||||
viewStarts,
|
||||
new RazorPageAdapter(result.Page),
|
||||
pageAdapter,
|
||||
_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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
//
|
||||
// The page gets activated before handler methods run, but the RazorView will also activate
|
||||
// each page.
|
||||
public class RazorPageAdapter : IRazorPage
|
||||
public class RazorPageAdapter : IRazorPage, IModelTypeProvider
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (page == null)
|
||||
|
|
@ -28,6 +30,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
_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
|
||||
{
|
||||
get { return _page.ViewContext; }
|
||||
|
|
@ -75,5 +83,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
{
|
||||
return _page.ExecuteAsync();
|
||||
}
|
||||
|
||||
Type IModelTypeProvider.GetModelType() => _modelType;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -607,6 +607,22 @@ Hello from /Pages/Shared/";
|
|||
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)
|
||||
{
|
||||
var response = await Client.GetAsync(request.RequestUri);
|
||||
|
|
|
|||
|
|
@ -165,6 +165,69 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
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()
|
||||
{
|
||||
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
|
||||
{
|
||||
[RazorInject]
|
||||
|
|
|
|||
|
|
@ -1747,6 +1747,49 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
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)
|
||||
{
|
||||
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