diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelperOfT.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelperOfT.cs index bb61b5867d..833a271740 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelperOfT.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelperOfT.cs @@ -63,6 +63,22 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures } ViewData = viewContext.ViewData as ViewDataDictionary; + + if (ViewData == null) + { + // The view data that we have at this point might be of a more derived type than the one defined at compile time. + // For example ViewDataDictionary where our TModel is Base and Derived : Base. + // This can't happen for regular views, but it can happen in razor pages if someone modified the model type through + // the page application model. + // In that case, we check if the type of the current view data, 'ViewDataDictionary' is "covariant" with the + // one defined at compile time 'ViewDataDictionary' + var runtimeType = viewContext.ViewData.ModelMetadata.ModelType; + if (runtimeType != null && typeof(TModel) != runtimeType && typeof(TModel).IsAssignableFrom(runtimeType)) + { + ViewData = new ViewDataDictionary(viewContext.ViewData, viewContext.ViewData.Model); + } + } + if (ViewData == null) { // viewContext may contain a base ViewDataDictionary instance. So complain about that type, not TModel. diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperTest.cs index 180dea3415..29ffb589d8 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperTest.cs @@ -1,6 +1,7 @@ // 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 System; using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Xunit; @@ -286,6 +287,81 @@ namespace Microsoft.AspNetCore.Mvc.Rendering Assert.Equal(expectedString, result.ToString()); } + [Fact] + public void Contextualize_WorksWithCovariantViewDataDictionary() + { + // Arrange + var helperToContextualize = DefaultTemplatesUtilities + .GetHtmlHelper(model: null); + + var viewContext = DefaultTemplatesUtilities + .GetHtmlHelper(model: null) + .ViewContext; + + // Act + helperToContextualize.Contextualize(viewContext); + + // Assert + Assert.IsType>( + helperToContextualize.ViewData); + + Assert.Same(helperToContextualize.ViewContext, viewContext); + } + + [Fact] + public void Contextualize_ThrowsIfViewDataDictionariesAreNotCompatible() + { + // Arrange + var helperToContextualize = DefaultTemplatesUtilities + .GetHtmlHelper(model: null); + + var viewContext = DefaultTemplatesUtilities + .GetHtmlHelper(model: null) + .ViewContext; + + var expectedMessage = $"Property '{nameof(ViewContext.ViewData)}' is of type " + + $"'{typeof(ViewDataDictionary).FullName}'," + + $" but this method requires a value of type '{typeof(ViewDataDictionary).FullName}'."; + + // Act & Assert + var exception = Assert.Throws("viewContext", () => helperToContextualize.Contextualize(viewContext)); + Assert.Contains(expectedMessage, exception.Message); + } + + [Fact] + public void Contextualize_ThrowsForNonGenericViewDataDictionaries() + { + // Arrange + var helperToContextualize = DefaultTemplatesUtilities + .GetHtmlHelper(model: null); + + var viewContext = DefaultTemplatesUtilities + .GetHtmlHelper(model: null) + .ViewContext; + viewContext.ViewData = new ViewDataDictionary(viewContext.ViewData); + + var expectedMessage = $"Property '{nameof(ViewContext.ViewData)}' is of type " + + $"'{typeof(ViewDataDictionary).FullName}'," + + $" but this method requires a value of type '{typeof(ViewDataDictionary).FullName}'."; + + // Act & Assert + var exception = Assert.Throws("viewContext", () => helperToContextualize.Contextualize(viewContext)); + Assert.Contains(expectedMessage, exception.Message); + } + + private class BaseModel + { + public string Name { get; set; } + } + + private class DerivedModel : BaseModel + { + } + + private class NonDerivedModel + { + } + [Theory] [InlineData("SomeName", "SomeName")] [InlineData("Obj1.Prop1", "Obj1_Prop1")]