Ensure PageContext.ViewData and ViewContext.ViewData are the same instance

Fixes #7675
This commit is contained in:
Pranav K 2018-07-12 17:27:32 -07:00
parent 9d951325b2
commit b62499e02c
No known key found for this signature in database
GPG Key ID: 1963DA6D96C3057A
12 changed files with 263 additions and 10 deletions

View File

@ -0,0 +1,9 @@
using System;
namespace Microsoft.AspNetCore.Mvc.Razor
{
internal interface IModelTypeProvider
{
Type GetModelType();
}
}

View File

@ -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;
}
}
}
}

View File

@ -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

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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]

View File

@ -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();

View File

@ -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";
}
}

View File

@ -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.

View File

@ -0,0 +1,4 @@
@RenderBody()
<span id="valuefromviewstart">@ViewData["ValueFromViewStart"]</span>
<span id="valuefrompage">@ViewData["ValueFromPage"]</span>
<span id="valuefrompagemodel">@ViewData["ValueFromPageModel"]</span>

View File

@ -0,0 +1,4 @@
@{
Layout = "_Layout";
ViewData["ValueFromViewStart"] = "Value from _ViewStart";
}