diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/IRazorViewEngine.cs b/src/Microsoft.AspNetCore.Mvc.Razor/IRazorViewEngine.cs index 76e146d3a4..6af2cafef8 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/IRazorViewEngine.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/IRazorViewEngine.cs @@ -15,9 +15,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// . /// /// The . - /// The name of the page. + /// The name or path of the page. /// The of locating the page. - /// . + /// + /// Use when the absolute or relative + /// path of the page is known. + /// . + /// RazorPageResult FindPage(ActionContext context, string pageName); /// diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs new file mode 100644 index 0000000000..8d26b511ff --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs @@ -0,0 +1,119 @@ +// 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.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewEngines; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace Microsoft.AspNetCore.Mvc.TagHelpers +{ + /// + /// Renders a partial view. + /// + [HtmlTargetElement("partial", Attributes = "name", TagStructure = TagStructure.WithoutEndTag)] + public class PartialTagHelper : TagHelper + { + private const string ForAttributeName = "asp-for"; + private readonly ICompositeViewEngine _viewEngine; + private readonly IViewBufferScope _viewBufferScope; + + public PartialTagHelper( + ICompositeViewEngine viewEngine, + IViewBufferScope viewBufferScope) + { + _viewEngine = viewEngine ?? throw new ArgumentNullException(nameof(viewEngine)); + _viewBufferScope = viewBufferScope ?? throw new ArgumentNullException(nameof(viewBufferScope)); + } + + /// + /// The name or path of the partial view that is rendered to the response. + /// + public string Name { get; set; } + + /// + /// An expression to be evaluated against the current model. + /// + [HtmlAttributeName(ForAttributeName)] + public ModelExpression For { get; set; } + + /// + /// A to pass into the partial view. + /// + public ViewDataDictionary ViewData { get; set; } + + [HtmlAttributeNotBound] + [ViewContext] + public ViewContext ViewContext { get; set; } + + /// + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (output == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var viewBuffer = new ViewBuffer(_viewBufferScope, Name, ViewBuffer.PartialViewPageSize); + using (var writer = new ViewBufferTextWriter(viewBuffer, Encoding.UTF8)) + { + await RenderPartialViewAsync(writer); + + // Reset the TagName. We don't want `partial` to render. + output.TagName = null; + output.Content.SetHtmlContent(viewBuffer); + } + } + + private async Task RenderPartialViewAsync(TextWriter writer) + { + var viewEngineResult = _viewEngine.GetView(ViewContext.ExecutingFilePath, Name, isMainPage: false); + var getViewLocations = viewEngineResult.SearchedLocations; + if (!viewEngineResult.Success) + { + viewEngineResult = _viewEngine.FindView(ViewContext, Name, isMainPage: false); + } + + if (!viewEngineResult.Success) + { + var searchedLocations = Enumerable.Concat(getViewLocations, viewEngineResult.SearchedLocations); + var locations = string.Empty; + if (searchedLocations.Any()) + { + locations += Environment.NewLine + string.Join(Environment.NewLine, searchedLocations); + } + + throw new InvalidOperationException( + Resources.FormatViewEngine_PartialViewNotFound(Name, locations)); + } + + var view = viewEngineResult.View; + // Determine which ViewData we should use to construct a new ViewData + var baseViewData = ViewData ?? ViewContext.ViewData; + var model = For?.Model ?? ViewContext.ViewData.Model; + var newViewData = new ViewDataDictionary(baseViewData, model); + var partialViewContext = new ViewContext(ViewContext, view, newViewData, writer); + + if (For?.Name != null) + { + newViewData.TemplateInfo.HtmlFieldPrefix = newViewData.TemplateInfo.GetFullHtmlFieldName(For.Name); + } + + using (view as IDisposable) + { + await view.RenderAsync(partialViewContext); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs index 411940c2a9..c590669745 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs @@ -178,6 +178,20 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers internal static string FormatArgumentCannotContainHtmlSpace() => GetString("ArgumentCannotContainHtmlSpace"); + /// + /// The partial view '{0}' was not found. The following locations were searched:{1} + /// + internal static string ViewEngine_PartialViewNotFound + { + get => GetString("ViewEngine_PartialViewNotFound"); + } + + /// + /// The partial view '{0}' was not found. The following locations were searched:{1} + /// + internal static string FormatViewEngine_PartialViewNotFound(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("ViewEngine_PartialViewNotFound"), p0, p1); + 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 7242ee001b..e93c480395 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Resources.resx @@ -153,4 +153,7 @@ Value cannot contain HTML space characters. + + The partial view '{0}' was not found. The following locations were searched:{1} + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Controller.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Controller.cs index 31e182b4f5..a4ad48b4f6 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Controller.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Controller.cs @@ -108,7 +108,7 @@ namespace Microsoft.AspNetCore.Mvc /// /// Creates a object by specifying a . /// - /// The name of the view that is rendered to the response. + /// The name or path of the view that is rendered to the response. /// The created object for the response. [NonAction] public virtual ViewResult View(string viewName) @@ -132,7 +132,7 @@ namespace Microsoft.AspNetCore.Mvc /// Creates a object by specifying a /// and the to be rendered by the view. /// - /// The name of the view that is rendered to the response. + /// The name or path of the view that is rendered to the response. /// The model that is rendered by the view. /// The created object for the response. [NonAction] @@ -161,7 +161,7 @@ namespace Microsoft.AspNetCore.Mvc /// /// Creates a object by specifying a . /// - /// The name of the view that is rendered to the response. + /// The name or path of the partial view that is rendered to the response. /// The created object for the response. [NonAction] public virtual PartialViewResult PartialView(string viewName) @@ -185,7 +185,7 @@ namespace Microsoft.AspNetCore.Mvc /// Creates a object by specifying a /// and the to be rendered by the partial view. /// - /// The name of the partial view that is rendered to the response. + /// The name or path of the partial view that is rendered to the response. /// The model that is rendered by the partial view. /// The created object for the response. [NonAction] diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/PartialViewResult.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/PartialViewResult.cs index 64a8c5fcbb..e9f2763057 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/PartialViewResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/PartialViewResult.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; -using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Mvc @@ -23,7 +22,7 @@ namespace Microsoft.AspNetCore.Mvc public int? StatusCode { get; set; } /// - /// Gets or sets the name of the partial view to render. + /// Gets or sets the name or path of the partial view that is rendered to the response. /// /// /// When null, defaults to . diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/HtmlHelperPartialExtensions.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/HtmlHelperPartialExtensions.cs index 1cd317bc60..8d9bdac59a 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/HtmlHelperPartialExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/HtmlHelperPartialExtensions.cs @@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering /// /// The instance this method extends. /// - /// The name of the partial view used to create the HTML markup. Must not be null. + /// The name or path of the partial view used to create the HTML markup. Must not be null. /// /// /// A that on completion returns a new instance containing @@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering /// /// The instance this method extends. /// - /// The name of the partial view used to create the HTML markup. Must not be null. + /// The name or path of the partial view used to create the HTML markup. Must not be null. /// /// A to pass into the partial view. /// @@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering /// /// The instance this method extends. /// - /// The name of the partial view used to create the HTML markup. Must not be null. + /// The name or path of the partial view used to create the HTML markup. Must not be null. /// /// A model to pass into the partial view. /// @@ -106,7 +106,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering /// /// The instance this method extends. /// - /// The name of the partial view used to create the HTML markup. Must not be null. + /// The name or path of the partial view used to create the HTML markup. Must not be null. /// /// /// Returns a new instance containing the created HTML. @@ -137,7 +137,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering /// /// The instance this method extends. /// - /// The name of the partial view used to create the HTML markup. Must not be null. + /// The name or path of the partial view used to create the HTML markup. Must not be null. /// /// A to pass into the partial view. /// @@ -170,7 +170,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering /// /// The instance this method extends. /// - /// The name of the partial view used to create the HTML markup. Must not be null. + /// The name or path of the partial view used to create the HTML markup. Must not be null. /// /// A model to pass into the partial view. /// @@ -203,7 +203,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering /// /// The instance this method extends. /// - /// The name of the partial view used to create the HTML markup. Must not be null. + /// The name or path of the partial view used to create the HTML markup. Must not be null. /// /// A model to pass into the partial view. /// A to pass into the partial view. @@ -239,7 +239,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering /// /// The instance this method extends. /// - /// The name of the partial view used to create the HTML markup. Must not be null. + /// The name or path of the partial view used to create the HTML markup. Must not be null. /// /// /// In this context, "renders" means the method writes its output using . @@ -267,7 +267,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering /// /// The instance this method extends. /// - /// The name of the partial view used to create the HTML markup. Must not be null. + /// The name or path of the partial view used to create the HTML markup. Must not be null. /// /// A to pass into the partial view. /// @@ -297,7 +297,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering /// /// The instance this method extends. /// - /// The name of the partial view used to create the HTML markup. Must not be null. + /// The name or path of the partial view used to create the HTML markup. Must not be null. /// /// A model to pass into the partial view. /// @@ -327,7 +327,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering /// /// The instance this method extends. /// - /// The name of the partial view used to create the HTML markup. Must not be null. + /// The name or path of the partial view used to create the HTML markup. Must not be null. /// /// A that renders the created HTML when it executes. /// @@ -355,7 +355,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering /// /// The instance this method extends. /// - /// The name of the partial view used to create the HTML markup. Must not be null. + /// The name or path of the partial view used to create the HTML markup. Must not be null. /// /// A to pass into the partial view. /// A that renders the created HTML when it executes. @@ -385,7 +385,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering /// /// The instance this method extends. /// - /// The name of the partial view used to create the HTML markup. Must not be null. + /// The name or path of the partial view used to create the HTML markup. Must not be null. /// /// A model to pass into the partial view. /// A that renders the created HTML when it executes. diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/IHtmlHelper.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/IHtmlHelper.cs index 0a31ca723a..357d429c94 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/IHtmlHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/IHtmlHelper.cs @@ -570,7 +570,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering /// Renders HTML markup for the specified partial view. /// /// - /// The name of the partial view used to create the HTML markup. Must not be null. + /// The name or path of the partial view used to create the HTML markup. Must not be null. /// /// A model to pass into the partial view. /// A to pass into the partial view. diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewEngines/IViewEngine.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewEngines/IViewEngine.cs index f08850bc21..f6f5c93cc7 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewEngines/IViewEngine.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewEngines/IViewEngine.cs @@ -13,9 +13,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewEngines /// . /// /// The . - /// The name of the view. + /// The name or path of the view that is rendered to the response. /// Determines if the page being found is the main page for an action. /// The of locating the view. + /// Use when the absolute or relative + /// path of the view is known. ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage); /// diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewResult.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewResult.cs index b169604d14..c98562fc9e 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewResult.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; -using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Mvc @@ -23,7 +22,7 @@ namespace Microsoft.AspNetCore.Mvc public int? StatusCode { get; set; } /// - /// Gets or sets the name of the view to render. + /// Gets or sets the name or path of the view that is rendered to the response. /// /// /// When null, defaults to . diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs index 1579544e7c..db7411fff9 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs @@ -61,8 +61,12 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests // Only attribute order should differ. { "Order", "/HtmlGeneration_Order/Submit" }, { "OrderUsingHtmlHelpers", "/HtmlGeneration_Order/Submit" }, + // Testing PartialTagHelper + { "PartialTagHelperWithoutModel", null }, + { "Warehouse", null }, // Testing InputTagHelpers invoked in the partial views { "ProductList", "/HtmlGeneration_Product" }, + { "ProductListUsingTagHelpers", "/HtmlGeneration_Product" }, // Testing the ScriptTagHelper { "Script", null }, }; diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.PartialTagHelperWithoutModel.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.PartialTagHelperWithoutModel.html new file mode 100644 index 0000000000..81eed3cee9 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.PartialTagHelperWithoutModel.html @@ -0,0 +1 @@ +PartialTagHelperWithoutModel: Hello from partial \ 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 new file mode 100644 index 0000000000..861d7b33a4 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.ProductListUsingTagHelpers.html @@ -0,0 +1,66 @@ + + + + + +
+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+ +
HtmlFieldPrefix =
+ +
+ + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Warehouse.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Warehouse.html new file mode 100644 index 0000000000..d86cdb95db --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Warehouse.html @@ -0,0 +1,15 @@ +

City_1

+ +
+ + +
+
+ + +
+
+ + +
\ 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 new file mode 100644 index 0000000000..937e95ad44 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs @@ -0,0 +1,473 @@ +// 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 System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.TestCommon; +using Microsoft.AspNetCore.Mvc.ViewEngines; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.WebEncoders.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.TagHelpers +{ + public class PartialTagHelperTest + { + [Fact] + public async Task ProcessAsync_RendersPartialView_IfGetViewReturnsView() + { + // Arrange + var expected = "Hello world!"; + var bufferScope = new TestViewBufferScope(); + var partialName = "_Partial"; + var model = new object(); + var viewContext = GetViewContext(); + + var view = new Mock(); + view.Setup(v => v.RenderAsync(It.IsAny())) + .Callback((ViewContext v) => + { + v.Writer.Write(expected); + }) + .Returns(Task.CompletedTask); + + var viewEngine = new Mock(); + viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false)) + .Returns(ViewEngineResult.Found(partialName, view.Object)); + + var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope) + { + Name = partialName, + ViewContext = viewContext, + }; + var tagHelperContext = GetTagHelperContext(); + var output = GetTagHelperOutput(); + + // Act + await tagHelper.ProcessAsync(tagHelperContext, output); + + // Assert + var content = HtmlContentUtilities.HtmlContentToString(output.Content, new HtmlTestEncoder()); + Assert.Equal(expected, content); + } + + [Fact] + public async Task ProcessAsync_RendersPartialView_IfFindViewReturnsView() + { + // Arrange + var expected = "Hello world!"; + var bufferScope = new TestViewBufferScope(); + var partialName = "_Partial"; + var model = new object(); + var viewContext = GetViewContext(); + + var view = new Mock(); + view.Setup(v => v.RenderAsync(It.IsAny())) + .Callback((ViewContext v) => + { + v.Writer.Write(expected); + }) + .Returns(Task.CompletedTask); + + var viewEngine = new Mock(); + viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false)) + .Returns(ViewEngineResult.NotFound(partialName, new[] { partialName })); + + viewEngine.Setup(v => v.FindView(viewContext, partialName, false)) + .Returns(ViewEngineResult.Found(partialName, view.Object)); + + var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope) + { + Name = partialName, + ViewContext = viewContext, + }; + var tagHelperContext = GetTagHelperContext(); + var output = GetTagHelperOutput(); + + // Act + await tagHelper.ProcessAsync(tagHelperContext, output); + + // Assert + var content = HtmlContentUtilities.HtmlContentToString(output.Content, new HtmlTestEncoder()); + Assert.Equal(expected, content); + } + + [Fact] + public async Task ProcessAsync_UsesViewDataFromContext() + { + // Arrange + var expected = "Implicit"; + var bufferScope = new TestViewBufferScope(); + var partialName = "_Partial"; + var viewContext = GetViewContext(); + viewContext.ViewData["key"] = expected; + + var view = new Mock(); + view.Setup(v => v.RenderAsync(It.IsAny())) + .Callback((ViewContext v) => + { + v.Writer.Write(v.ViewData["key"]); + }) + .Returns(Task.CompletedTask); + + var viewEngine = new Mock(); + viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false)) + .Returns(ViewEngineResult.NotFound(partialName, new[] { partialName })); + + viewEngine.Setup(v => v.FindView(viewContext, partialName, false)) + .Returns(ViewEngineResult.Found(partialName, view.Object)); + + var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope) + { + Name = partialName, + ViewContext = viewContext, + }; + var tagHelperContext = GetTagHelperContext(); + var output = GetTagHelperOutput(); + + // Act + await tagHelper.ProcessAsync(tagHelperContext, output); + + // Assert + var content = HtmlContentUtilities.HtmlContentToString(output.Content, new HtmlTestEncoder()); + Assert.Equal(expected, content); + } + + [Fact] + public async Task ProcessAsync_UsesPassedInViewData_WhenNotNull() + { + // Arrange + var expected = "Explicit"; + var bufferScope = new TestViewBufferScope(); + var partialName = "_Partial"; + var model = new object(); + var viewData = new ViewDataDictionary(new TestModelMetadataProvider(), new ModelStateDictionary()); + viewData["key"] = expected; + var viewContext = GetViewContext(); + viewContext.ViewData["key"] = "ViewContext"; + + var view = new Mock(); + view.Setup(v => v.RenderAsync(It.IsAny())) + .Callback((ViewContext v) => + { + v.Writer.Write(v.ViewData["key"]); + }) + .Returns(Task.CompletedTask); + + var viewEngine = new Mock(); + viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false)) + .Returns(ViewEngineResult.Found(partialName, view.Object)); + + var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope) + { + Name = partialName, + ViewContext = viewContext, + ViewData = viewData, + }; + var tagHelperContext = GetTagHelperContext(); + var output = GetTagHelperOutput(); + + // Act + await tagHelper.ProcessAsync(tagHelperContext, output); + + // Assert + var content = HtmlContentUtilities.HtmlContentToString(output.Content, new HtmlTestEncoder()); + Assert.Equal(expected, content); + } + + [Fact] + public async Task ProcessAsync_UsesModelExpression_ToDetermineModel() + { + // Arrange + var expected = new PropertyModel(); + var bufferScope = new TestViewBufferScope(); + var partialName = "_Partial"; + var modelMetadataProvider = new TestModelMetadataProvider(); + var containerModel = new TestModel { Property = expected }; + var containerModelExplorer = modelMetadataProvider.GetModelExplorerForType( + typeof(TestModel), + containerModel); + var propertyModelExplorer = containerModelExplorer.GetExplorerForProperty(nameof(TestModel.Property)); + + var modelExpression = new ModelExpression("Property", propertyModelExplorer); + var viewContext = GetViewContext(); + viewContext.ViewData.Model = new object(); + + var view = new Mock(); + view.Setup(v => v.RenderAsync(It.IsAny())) + .Callback((ViewContext v) => + { + var actual = Assert.IsType(v.ViewData.Model); + Assert.Same(expected, actual); + }) + .Returns(Task.CompletedTask) + .Verifiable(); + + var viewEngine = new Mock(); + viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false)) + .Returns(ViewEngineResult.Found(partialName, view.Object)); + + var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope) + { + Name = partialName, + ViewContext = viewContext, + For = modelExpression, + }; + var tagHelperContext = GetTagHelperContext(); + var output = GetTagHelperOutput(); + + // Act + await tagHelper.ProcessAsync(tagHelperContext, output); + + // Assert + view.Verify(); + } + + [Fact] + public async Task ProcessAsync_SetsHtmlFieldPrefix_UsingModelExpression() + { + // Arrange + var expected = "order.items[0].Property"; + var bufferScope = new TestViewBufferScope(); + var partialName = "_Partial"; + var modelMetadataProvider = new TestModelMetadataProvider(); + var containerModel = new TestModel { Property = new PropertyModel() }; + var containerModelExplorer = modelMetadataProvider.GetModelExplorerForType( + typeof(TestModel), + containerModel); + var propertyModelExplorer = containerModelExplorer.GetExplorerForProperty(nameof(TestModel.Property)); + + var modelExpression = new ModelExpression("Property", propertyModelExplorer); + var viewContext = GetViewContext(); + viewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "order.items[0]"; + + var view = new Mock(); + view.Setup(v => v.RenderAsync(It.IsAny())) + .Callback((ViewContext v) => + { + Assert.Equal(expected, v.ViewData.TemplateInfo.HtmlFieldPrefix); + }) + .Returns(Task.CompletedTask) + .Verifiable(); + + var viewEngine = new Mock(); + viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false)) + .Returns(ViewEngineResult.Found(partialName, view.Object)); + + var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope) + { + Name = partialName, + ViewContext = viewContext, + For = modelExpression, + }; + var tagHelperContext = GetTagHelperContext(); + var output = GetTagHelperOutput(); + + // Act + await tagHelper.ProcessAsync(tagHelperContext, output); + + // Assert + view.Verify(); + Assert.Equal("order.items[0]", viewContext.ViewData.TemplateInfo.HtmlFieldPrefix); + } + + [Fact] + public async Task ProcessAsync_UsesModelOnViewContextViewData_WhenModelExpresionIsNull() + { + // Arrange + var bufferScope = new TestViewBufferScope(); + var partialName = "_Partial"; + var model = new object(); + var viewContext = GetViewContext(); + viewContext.ViewData.Model = model; + + var view = new Mock(); + view.Setup(v => v.RenderAsync(It.IsAny())) + .Callback((ViewContext v) => + { + Assert.Same(model, v.ViewData.Model); + }) + .Returns(Task.CompletedTask) + .Verifiable(); + + var viewEngine = new Mock(); + viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false)) + .Returns(ViewEngineResult.Found(partialName, view.Object)); + + var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope) + { + Name = partialName, + ViewContext = viewContext, + }; + var tagHelperContext = GetTagHelperContext(); + var output = GetTagHelperOutput(); + + // Act + await tagHelper.ProcessAsync(tagHelperContext, output); + + // Assert + view.Verify(); + } + + [Fact] + public async Task ProcessAsync_DoesNotModifyHtmlFieldPrefix_WhenModelExpressionIsNull() + { + // Arrange + var expected = "original"; + var bufferScope = new TestViewBufferScope(); + var partialName = "_Partial"; + var model = new object(); + var viewContext = GetViewContext(); + viewContext.ViewData.Model = model; + viewContext.ViewData.TemplateInfo.HtmlFieldPrefix = expected; + + var view = new Mock(); + view.Setup(v => v.RenderAsync(It.IsAny())) + .Callback((ViewContext v) => + { + Assert.Equal(expected, v.ViewData.TemplateInfo.HtmlFieldPrefix); + }) + .Returns(Task.CompletedTask) + .Verifiable(); + + var viewEngine = new Mock(); + viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false)) + .Returns(ViewEngineResult.Found(partialName, view.Object)); + + var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope) + { + Name = partialName, + ViewContext = viewContext, + }; + var tagHelperContext = GetTagHelperContext(); + var output = GetTagHelperOutput(); + + // Act + await tagHelper.ProcessAsync(tagHelperContext, output); + + // Assert + view.Verify(); + } + + [Fact] + public async Task ProcessAsync_DisposesViewInstance() + { + // Arrange + var bufferScope = new TestViewBufferScope(); + var partialName = "_Partial"; + var viewContext = GetViewContext(); + + var disposable = new Mock(); + disposable.Setup(d => d.Dispose()).Verifiable(); + var view = disposable.As(); + + view.Setup(v => v.RenderAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + var viewEngine = new Mock(); + viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false)) + .Returns(ViewEngineResult.Found(partialName, view.Object)); + + var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope) + { + Name = partialName, + ViewContext = viewContext, + }; + var tagHelperContext = GetTagHelperContext(); + var output = GetTagHelperOutput(); + + // Act + await tagHelper.ProcessAsync(tagHelperContext, output); + + // Assert + disposable.Verify(); + view.Verify(); + } + + [Fact] + public async Task ProcessAsync_Throws_IfGetViewAndFindReturnNotFoundResults() + { + // Arrange + var bufferScope = new TestViewBufferScope(); + var partialName = "_Partial"; + var expected = string.Join(Environment.NewLine, + $"The partial view '{partialName}' was not found. The following locations were searched:", + "NotFound1", + "NotFound2", + "NotFound3", + "NotFound4"); + var viewData = new ViewDataDictionary(new TestModelMetadataProvider(), new ModelStateDictionary()); + var viewContext = GetViewContext(); + + var view = Mock.Of(); + var viewEngine = new Mock(); + viewEngine.Setup(v => v.GetView(It.IsAny(), partialName, false)) + .Returns(ViewEngineResult.NotFound(partialName, new[] { "NotFound1", "NotFound2" })); + + viewEngine.Setup(v => v.FindView(viewContext, partialName, false)) + .Returns(ViewEngineResult.NotFound(partialName, new[] { $"NotFound3", $"NotFound4" })); + + var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope) + { + Name = partialName, + ViewContext = viewContext, + ViewData = viewData, + }; + var tagHelperContext = GetTagHelperContext(); + var output = GetTagHelperOutput(); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => tagHelper.ProcessAsync(tagHelperContext, output)); + Assert.Equal(expected, exception.Message); + } + + private static ViewContext GetViewContext() + { + return new ViewContext( + new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()), + NullView.Instance, + new ViewDataDictionary(new TestModelMetadataProvider(), new ModelStateDictionary()), + Mock.Of(), + TextWriter.Null, + new HtmlHelperOptions()); + } + + private static TagHelperContext GetTagHelperContext() + { + return new TagHelperContext( + "partial", + new TagHelperAttributeList(), + new Dictionary(), + Guid.NewGuid().ToString("N")); + } + + private static TagHelperOutput GetTagHelperOutput() + { + return new TagHelperOutput( + "partial", + new TagHelperAttributeList(), + (_, __) => Task.FromResult(new DefaultTagHelperContent())); + } + + private class TestModel + { + public PropertyModel Property { get; set; } + } + + private class PropertyModel + { + } + } +} diff --git a/test/WebSites/HtmlGenerationWebSite/Controllers/HtmlGeneration_HomeController.cs b/test/WebSites/HtmlGenerationWebSite/Controllers/HtmlGeneration_HomeController.cs index 3736f9554c..c0f4560c1e 100644 --- a/test/WebSites/HtmlGenerationWebSite/Controllers/HtmlGeneration_HomeController.cs +++ b/test/WebSites/HtmlGenerationWebSite/Controllers/HtmlGeneration_HomeController.cs @@ -108,6 +108,8 @@ namespace HtmlGenerationWebSite.Controllers return View(_products); } + public IActionResult ProductListUsingTagHelpers() => View(_products); + public IActionResult EmployeeList() { var employees = new List @@ -165,6 +167,22 @@ namespace HtmlGenerationWebSite.Controllers return View(warehouse); } + public IActionResult Warehouse() + { + var warehouse = new Warehouse + { + City = "City_1", + Employee = new Employee + { + Name = "EmployeeName_1", + OfficeNumber = "Number_1", + Address = "Address_1", + } + }; + + return View(warehouse); + } + public IActionResult Environment() { return View(); @@ -209,5 +227,7 @@ namespace HtmlGenerationWebSite.Controllers { return View(); } + + public IActionResult PartialTagHelperWithoutModel() => View(); } } diff --git a/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/PartialTagHelperWithoutModel.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/PartialTagHelperWithoutModel.cshtml new file mode 100644 index 0000000000..5074a5280f --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/PartialTagHelperWithoutModel.cshtml @@ -0,0 +1,2 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +PartialTagHelperWithoutModel: \ No newline at end of file diff --git a/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/ProductListUsingTagHelpers.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/ProductListUsingTagHelpers.cshtml new file mode 100644 index 0000000000..17da2c943e --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/ProductListUsingTagHelpers.cshtml @@ -0,0 +1,25 @@ +@using HtmlGenerationWebSite.Models +@model IList + +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + + + + + + +
+ @for (var i = 0; i < Model.Count; i++) + { +
+ + +
+ + } + @* Print the HtmlFieldPrefix outside of the partial tag helper to ensure it hasn't been modified *@ +
HtmlFieldPrefix = @ViewData.TemplateInfo.HtmlFieldPrefix
+ + + + diff --git a/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/Warehouse.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/Warehouse.cshtml new file mode 100644 index 0000000000..64ca9c12af --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/Warehouse.cshtml @@ -0,0 +1,5 @@ +@model HtmlGenerationWebSite.Models.Warehouse +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + +

@Html.DisplayFor(m => m.City)

+ \ No newline at end of file diff --git a/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/_EmployeePartial.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/_EmployeePartial.cshtml new file mode 100644 index 0000000000..d15ee345e3 --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/_EmployeePartial.cshtml @@ -0,0 +1,15 @@ +@model HtmlGenerationWebSite.Models.Employee +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + +
+ + +
+
+ + +
+
+ + +
diff --git a/test/WebSites/HtmlGenerationWebSite/Views/Shared/_Partial.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/Shared/_Partial.cshtml new file mode 100644 index 0000000000..fcdc0458e2 --- /dev/null +++ b/test/WebSites/HtmlGenerationWebSite/Views/Shared/_Partial.cshtml @@ -0,0 +1 @@ +Hello from partial