diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs index cb69697095..dc4be24ae1 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs @@ -20,6 +20,9 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers [HtmlTargetElement("partial", Attributes = "name", TagStructure = TagStructure.WithoutEndTag)] public class PartialTagHelper : TagHelper { + private const string ForAttributeName = "for"; + private const string ModelAttributeName = "model"; + private readonly ICompositeViewEngine _viewEngine; private readonly IViewBufferScope _viewBufferScope; @@ -37,10 +40,17 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers public string Name { get; set; } /// - /// An expression to be evaluated against the current model. + /// An expression to be evaluated against the current model. Cannot be used together with . /// + [HtmlAttributeName(ForAttributeName)] public ModelExpression For { get; set; } + /// + /// The model to pass into the partial view. Cannot be used together with . + /// + [HtmlAttributeName(ModelAttributeName)] + public object Model { get; set; } + /// /// A to pass into the partial view. /// @@ -63,10 +73,11 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers throw new ArgumentNullException(nameof(context)); } + var model = ResolveModel(); var viewBuffer = new ViewBuffer(_viewBufferScope, Name, ViewBuffer.PartialViewPageSize); using (var writer = new ViewBufferTextWriter(viewBuffer, Encoding.UTF8)) { - await RenderPartialViewAsync(writer); + await RenderPartialViewAsync(writer, model); // Reset the TagName. We don't want `partial` to render. output.TagName = null; @@ -74,7 +85,33 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } } - private async Task RenderPartialViewAsync(TextWriter writer) + // Internal for testing + internal object ResolveModel() + { + if (Model != null & For != null) + { + throw new InvalidOperationException( + Resources.FormatPartialTagHelper_InvalidModelAttributes( + typeof(PartialTagHelper).FullName, + ForAttributeName, + ModelAttributeName)); + } + + if (Model != null) + { + return Model; + } + + if (For != null) + { + return For.Model; + } + + // Model and For are null, fallback to the ViewContext's ViewData model. + return ViewContext.ViewData.Model; + } + + private async Task RenderPartialViewAsync(TextWriter writer, object model) { var viewEngineResult = _viewEngine.GetView(ViewContext.ExecutingFilePath, Name, isMainPage: false); var getViewLocations = viewEngineResult.SearchedLocations; @@ -99,9 +136,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers var view = viewEngineResult.View; // Determine which ViewData we should use to construct a new ViewData var baseViewData = ViewData ?? ViewContext.ViewData; - - // Use the rendering View's model only if an for expression does not exist - var model = For != null ? For.Model : ViewContext.ViewData.Model; var newViewData = new ViewDataDictionary(baseViewData, model); var partialViewContext = new ViewContext(ViewContext, view, newViewData, writer); diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs index c590669745..93ac7fa60e 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs @@ -192,6 +192,20 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers internal static string FormatViewEngine_PartialViewNotFound(object p0, object p1) => string.Format(CultureInfo.CurrentCulture, GetString("ViewEngine_PartialViewNotFound"), p0, p1); + /// + /// Cannot use '{0}' with both '{1}' and '{2}' attributes. + /// + internal static string PartialTagHelper_InvalidModelAttributes + { + get => GetString("PartialTagHelper_InvalidModelAttributes"); + } + + /// + /// Cannot use '{0}' with both '{1}' and '{2}' attributes. + /// + internal static string FormatPartialTagHelper_InvalidModelAttributes(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("PartialTagHelper_InvalidModelAttributes"), p0, p1, p2); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Resources.resx b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Resources.resx index e93c480395..1d6166e955 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Resources.resx @@ -156,4 +156,7 @@ The partial view '{0}' was not found. The following locations were searched:{1} + + Cannot use '{0}' with both '{1}' and '{2}' attributes. + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpers.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpers.html index 861d7b33a4..09796d5ddd 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpers.html +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpers.html @@ -62,5 +62,13 @@ Product_2 description
HtmlFieldPrefix =
+ + + +
+

You might also like these products!

+ \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpersWithNullModel.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpersWithNullModel.html index 2fcbfd25fa..20aabb80f3 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpersWithNullModel.html +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpersWithNullModel.html @@ -26,5 +26,13 @@
HtmlFieldPrefix =
+ + + +
+

You might also like these products!

+ \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs index c7a08e299f..8fa8c4f502 100644 --- a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs @@ -23,6 +23,90 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers { public class PartialTagHelperTest { + [Fact] + public void ResolveModel_ReturnsModelWhenProvided() + { + // Arrange + var expectedModel = new object(); + var tagHelper = new PartialTagHelper(Mock.Of(), Mock.Of()) + { + Model = expectedModel, + }; + + // Act + var model = tagHelper.ResolveModel(); + + // Assert + Assert.Same(expectedModel, model); + } + + [Fact] + public void ResolveModel_ReturnsForModelWhenProvided() + { + // Arrange + var expectedModel = new PropertyModel(); + var modelMetadataProvider = new TestModelMetadataProvider(); + var containerModel = new TestModel() + { + Property = expectedModel + }; + var containerModelExplorer = modelMetadataProvider.GetModelExplorerForType( + typeof(TestModel), + containerModel); + var propertyModelExplorer = containerModelExplorer.GetExplorerForProperty(nameof(TestModel.Property)); + var tagHelper = new PartialTagHelper(Mock.Of(), Mock.Of()) + { + For = new ModelExpression("Property", propertyModelExplorer), + }; + + // Act + var model = tagHelper.ResolveModel(); + + // Assert + Assert.Same(expectedModel, model); + } + + [Fact] + public void ResolveModel_ReturnsViewContextsViewDataModelWhenModelAndForAreNull() + { + // Arrange + var expectedModel = new object(); + var viewContext = GetViewContext(); + viewContext.ViewData.Model = expectedModel; + var tagHelper = new PartialTagHelper(Mock.Of(), Mock.Of()) + { + ViewContext = viewContext + }; + + // Act + var model = tagHelper.ResolveModel(); + + // Assert + Assert.Same(expectedModel, model); + } + + [Fact] + public void ResolveModel_ThrowsWhenModelAndForProvided() + { + // Arrange + var modelMetadataProvider = new TestModelMetadataProvider(); + var containerModel = new TestModel(); + var containerModelExplorer = modelMetadataProvider.GetModelExplorerForType( + typeof(TestModel), + containerModel); + var propertyModelExplorer = containerModelExplorer.GetExplorerForProperty(nameof(TestModel.Property)); + var tagHelper = new PartialTagHelper(Mock.Of(), Mock.Of()) + { + Model = new object(), + For = new ModelExpression("Property", propertyModelExplorer), + }; + var expectedMessage = Resources.FormatPartialTagHelper_InvalidModelAttributes(typeof(PartialTagHelper).FullName, "for", "model"); + + // Act & Assert + var exception = Assert.Throws(() => tagHelper.ResolveModel()); + Assert.Equal(expectedMessage, exception.Message); + } + [Fact] public async Task ProcessAsync_RendersPartialView_IfGetViewReturnsView() { diff --git a/test/WebSites/HtmlGenerationWebSite/Models/ProductRecommendations.cs b/test/WebSites/HtmlGenerationWebSite/Models/ProductRecommendations.cs new file mode 100644 index 0000000000..648a541751 --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Models/ProductRecommendations.cs @@ -0,0 +1,23 @@ +// 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; + +namespace HtmlGenerationWebSite.Models +{ + public class ProductRecommendations + { + public ProductRecommendations(params Product[] products) + { + if (products == null) + { + throw new ArgumentNullException(nameof(products)); + } + + Products = products; + } + + public IEnumerable Products { get; } + } +} diff --git a/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/ProductListUsingTagHelpers.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/ProductListUsingTagHelpers.cshtml index 4f78aabfb1..c0f2d7b10d 100644 --- a/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/ProductListUsingTagHelpers.cshtml +++ b/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/ProductListUsingTagHelpers.cshtml @@ -21,5 +21,7 @@
HtmlFieldPrefix = @ViewData.TemplateInfo.HtmlFieldPrefix
+ + diff --git a/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/_ProductRecommendations.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/_ProductRecommendations.cshtml new file mode 100644 index 0000000000..62427670ea --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/_ProductRecommendations.cshtml @@ -0,0 +1,13 @@ +@using HtmlGenerationWebSite.Models +@model ProductRecommendations + +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + +
+

You might also like these products!

+ \ No newline at end of file