Add a <partial /> tag helper

Fixes #5916
This commit is contained in:
Pranav K 2017-11-28 17:52:22 -08:00
parent 41104bff7e
commit e6c716444d
21 changed files with 792 additions and 25 deletions

View File

@ -15,9 +15,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor
/// <paramref name="context"/>.
/// </summary>
/// <param name="context">The <see cref="ActionContext"/>.</param>
/// <param name="pageName">The name of the page.</param>
/// <param name="pageName">The name or path of the page.</param>
/// <returns>The <see cref="RazorPageResult"/> of locating the page.</returns>
/// <remarks><seealso cref="IViewEngine.FindView"/>.</remarks>
/// <remarks>
/// <remarks>Use <see cref="GetPage(string, string)"/> when the absolute or relative
/// path of the page is known.</remarks>
/// <seealso cref="IViewEngine.FindView"/>.
/// </remarks>
RazorPageResult FindPage(ActionContext context, string pageName);
/// <summary>

View File

@ -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
{
/// <summary>
/// Renders a partial view.
/// </summary>
[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));
}
/// <summary>
/// The name or path of the partial view that is rendered to the response.
/// </summary>
public string Name { get; set; }
/// <summary>
/// An expression to be evaluated against the current model.
/// </summary>
[HtmlAttributeName(ForAttributeName)]
public ModelExpression For { get; set; }
/// <summary>
/// A <see cref="ViewDataDictionary"/> to pass into the partial view.
/// </summary>
public ViewDataDictionary ViewData { get; set; }
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; }
/// <inheritdoc />
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<object>(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);
}
}
}
}

View File

@ -178,6 +178,20 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
internal static string FormatArgumentCannotContainHtmlSpace()
=> GetString("ArgumentCannotContainHtmlSpace");
/// <summary>
/// The partial view '{0}' was not found. The following locations were searched:{1}
/// </summary>
internal static string ViewEngine_PartialViewNotFound
{
get => GetString("ViewEngine_PartialViewNotFound");
}
/// <summary>
/// The partial view '{0}' was not found. The following locations were searched:{1}
/// </summary>
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);

View File

@ -153,4 +153,7 @@
<data name="ArgumentCannotContainHtmlSpace" xml:space="preserve">
<value>Value cannot contain HTML space characters.</value>
</data>
<data name="ViewEngine_PartialViewNotFound" xml:space="preserve">
<value>The partial view '{0}' was not found. The following locations were searched:{1}</value>
</data>
</root>

View File

@ -108,7 +108,7 @@ namespace Microsoft.AspNetCore.Mvc
/// <summary>
/// Creates a <see cref="ViewResult"/> object by specifying a <paramref name="viewName"/>.
/// </summary>
/// <param name="viewName">The name of the view that is rendered to the response.</param>
/// <param name="viewName">The name or path of the view that is rendered to the response.</param>
/// <returns>The created <see cref="ViewResult"/> object for the response.</returns>
[NonAction]
public virtual ViewResult View(string viewName)
@ -132,7 +132,7 @@ namespace Microsoft.AspNetCore.Mvc
/// Creates a <see cref="ViewResult"/> object by specifying a <paramref name="viewName"/>
/// and the <paramref name="model"/> to be rendered by the view.
/// </summary>
/// <param name="viewName">The name of the view that is rendered to the response.</param>
/// <param name="viewName">The name or path of the view that is rendered to the response.</param>
/// <param name="model">The model that is rendered by the view.</param>
/// <returns>The created <see cref="ViewResult"/> object for the response.</returns>
[NonAction]
@ -161,7 +161,7 @@ namespace Microsoft.AspNetCore.Mvc
/// <summary>
/// Creates a <see cref="PartialViewResult"/> object by specifying a <paramref name="viewName"/>.
/// </summary>
/// <param name="viewName">The name of the view that is rendered to the response.</param>
/// <param name="viewName">The name or path of the partial view that is rendered to the response.</param>
/// <returns>The created <see cref="PartialViewResult"/> object for the response.</returns>
[NonAction]
public virtual PartialViewResult PartialView(string viewName)
@ -185,7 +185,7 @@ namespace Microsoft.AspNetCore.Mvc
/// Creates a <see cref="PartialViewResult"/> object by specifying a <paramref name="viewName"/>
/// and the <paramref name="model"/> to be rendered by the partial view.
/// </summary>
/// <param name="viewName">The name of the partial view that is rendered to the response.</param>
/// <param name="viewName">The name or path of the partial view that is rendered to the response.</param>
/// <param name="model">The model that is rendered by the partial view.</param>
/// <returns>The created <see cref="PartialViewResult"/> object for the response.</returns>
[NonAction]

View File

@ -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; }
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// When <c>null</c>, defaults to <see cref="ControllerActionDescriptor.ActionName"/>.

View File

@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/> instance this method extends.</param>
/// <param name="partialViewName">
/// The name of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// The name or path of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// </param>
/// <returns>
/// A <see cref="Task"/> that on completion returns a new <see cref="IHtmlContent"/> instance containing
@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/> instance this method extends.</param>
/// <param name="partialViewName">
/// The name of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// The name or path of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// </param>
/// <param name="viewData">A <see cref="ViewDataDictionary"/> to pass into the partial view.</param>
/// <returns>
@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/> instance this method extends.</param>
/// <param name="partialViewName">
/// The name of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// The name or path of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// </param>
/// <param name="model">A model to pass into the partial view.</param>
/// <returns>
@ -106,7 +106,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/> instance this method extends.</param>
/// <param name="partialViewName">
/// The name of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// The name or path of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// </param>
/// <returns>
/// Returns a new <see cref="IHtmlContent"/> instance containing the created HTML.
@ -137,7 +137,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/> instance this method extends.</param>
/// <param name="partialViewName">
/// The name of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// The name or path of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// </param>
/// <param name="viewData">A <see cref="ViewDataDictionary"/> to pass into the partial view.</param>
/// <returns>
@ -170,7 +170,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/> instance this method extends.</param>
/// <param name="partialViewName">
/// The name of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// The name or path of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// </param>
/// <param name="model">A model to pass into the partial view.</param>
/// <returns>
@ -203,7 +203,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/> instance this method extends.</param>
/// <param name="partialViewName">
/// The name of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// The name or path of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// </param>
/// <param name="model">A model to pass into the partial view.</param>
/// <param name="viewData">A <see cref="ViewDataDictionary"/> to pass into the partial view.</param>
@ -239,7 +239,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/> instance this method extends.</param>
/// <param name="partialViewName">
/// The name of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// The name or path of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// </param>
/// <remarks>
/// In this context, "renders" means the method writes its output using <see cref="ViewContext.Writer"/>.
@ -267,7 +267,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/> instance this method extends.</param>
/// <param name="partialViewName">
/// The name of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// The name or path of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// </param>
/// <param name="viewData">A <see cref="ViewDataDictionary"/> to pass into the partial view.</param>
/// <remarks>
@ -297,7 +297,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/> instance this method extends.</param>
/// <param name="partialViewName">
/// The name of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// The name or path of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// </param>
/// <param name="model">A model to pass into the partial view.</param>
/// <remarks>
@ -327,7 +327,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/> instance this method extends.</param>
/// <param name="partialViewName">
/// The name of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// The name or path of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// </param>
/// <returns>A <see cref="Task"/> that renders the created HTML when it executes.</returns>
/// <remarks>
@ -355,7 +355,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/> instance this method extends.</param>
/// <param name="partialViewName">
/// The name of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// The name or path of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// </param>
/// <param name="viewData">A <see cref="ViewDataDictionary"/> to pass into the partial view.</param>
/// <returns>A <see cref="Task"/> that renders the created HTML when it executes.</returns>
@ -385,7 +385,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/> instance this method extends.</param>
/// <param name="partialViewName">
/// The name of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// The name or path of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// </param>
/// <param name="model">A model to pass into the partial view.</param>
/// <returns>A <see cref="Task"/> that renders the created HTML when it executes.</returns>

View File

@ -570,7 +570,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// Renders HTML markup for the specified partial view.
/// </summary>
/// <param name="partialViewName">
/// The name of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// The name or path of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// </param>
/// <param name="model">A model to pass into the partial view.</param>
/// <param name="viewData">A <see cref="ViewDataDictionary"/> to pass into the partial view.</param>

View File

@ -13,9 +13,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewEngines
/// <paramref name="context"/>.
/// </summary>
/// <param name="context">The <see cref="ActionContext"/>.</param>
/// <param name="viewName">The name of the view.</param>
/// <param name="viewName">The name or path of the view that is rendered to the response.</param>
/// <param name="isMainPage">Determines if the page being found is the main page for an action.</param>
/// <returns>The <see cref="ViewEngineResult"/> of locating the view.</returns>
/// <remarks>Use <see cref="GetView(string, string, bool)"/> when the absolute or relative
/// path of the view is known.</remarks>
ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage);
/// <summary>

View File

@ -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; }
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// When <c>null</c>, defaults to <see cref="ControllerActionDescriptor.ActionName"/>.

View File

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

View File

@ -0,0 +1,66 @@
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<form action="/HtmlGeneration_Product" method="post">
<div>
<label class="product" for="z0__HomePage">HomePage</label>
<input type="url" size="50" disabled="disabled" readonly="readonly" id="z0__HomePage" name="[0].HomePage" value="http://www.contoso.com/" />
</div>
<div>
<label class="product" for="z0__Number">Number</label>
<input type="number" data-val="true" data-val-required="The Number field is required." id="z0__Number" name="[0].Number" value="0" />
</div>
<div>
<label class="product" for="z0__ProductName">ProductName</label>
<input type="text" data-val="true" data-val-required="The ProductName field is required." id="z0__ProductName" name="[0].ProductName" value="Product_0" />
</div>
<div>
<label class="product" for="z0__Description">Description</label>
<textarea rows="4" cols="50" class="product" id="z0__Description" name="[0].Description">
</textarea>
</div>
<div>
<label class="product" for="z1__HomePage">HomePage</label>
<input type="url" size="50" disabled="disabled" readonly="readonly" id="z1__HomePage" name="[1].HomePage" value="" />
</div>
<div>
<label class="product" for="z1__Number">Number</label>
<input type="number" data-val="true" data-val-required="The Number field is required." id="z1__Number" name="[1].Number" value="1" />
</div>
<div>
<label class="product" for="z1__ProductName">ProductName</label>
<input type="text" data-val="true" data-val-required="The ProductName field is required." id="z1__ProductName" name="[1].ProductName" value="Product_1" />
</div>
<div>
<label class="product" for="z1__Description">Description</label>
<textarea rows="4" cols="50" class="product" id="z1__Description" name="[1].Description">
</textarea>
</div>
<div>
<label class="product" for="z2__HomePage">HomePage</label>
<input type="url" size="50" disabled="disabled" readonly="readonly" id="z2__HomePage" name="[2].HomePage" value="" />
</div>
<div>
<label class="product" for="z2__Number">Number</label>
<input type="number" data-val="true" data-val-required="The Number field is required." id="z2__Number" name="[2].Number" value="2" />
</div>
<div>
<label class="product" for="z2__ProductName">ProductName</label>
<input type="text" data-val="true" data-val-required="The ProductName field is required." id="z2__ProductName" name="[2].ProductName" value="Product_2" />
</div>
<div>
<label class="product" for="z2__Description">Description</label>
<textarea rows="4" cols="50" class="product" id="z2__Description" name="[2].Description">
Product_2 description</textarea>
</div>
<div>HtmlFieldPrefix = </div>
<input type="submit" />
<input name="__RequestVerificationToken" type="hidden" value="{0}" /></form>
</body>
</html>

View File

@ -0,0 +1,15 @@
<h3>City_1</h3>
<div>
<label for="Employee_Name">Name</label>
<input type="text" id="Employee_Name" name="Employee.Name" value="EmployeeName_1" />
</div>
<div>
<label for="Employee_OfficeNumber">OfficeNumber</label>
<input type="number" id="Employee_OfficeNumber" name="Employee.OfficeNumber" value="Number_1" />
</div>
<div>
<label for="Employee_Address">Address</label>
<textarea rows="4" cols="50" id="Employee_Address" name="Employee.Address">
Address_1</textarea>
</div>

View File

@ -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<IView>();
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
.Callback((ViewContext v) =>
{
v.Writer.Write(expected);
})
.Returns(Task.CompletedTask);
var viewEngine = new Mock<ICompositeViewEngine>();
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), 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<IView>();
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
.Callback((ViewContext v) =>
{
v.Writer.Write(expected);
})
.Returns(Task.CompletedTask);
var viewEngine = new Mock<ICompositeViewEngine>();
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), 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<IView>();
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
.Callback((ViewContext v) =>
{
v.Writer.Write(v.ViewData["key"]);
})
.Returns(Task.CompletedTask);
var viewEngine = new Mock<ICompositeViewEngine>();
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), 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<IView>();
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
.Callback((ViewContext v) =>
{
v.Writer.Write(v.ViewData["key"]);
})
.Returns(Task.CompletedTask);
var viewEngine = new Mock<ICompositeViewEngine>();
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), 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<IView>();
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
.Callback((ViewContext v) =>
{
var actual = Assert.IsType<PropertyModel>(v.ViewData.Model);
Assert.Same(expected, actual);
})
.Returns(Task.CompletedTask)
.Verifiable();
var viewEngine = new Mock<ICompositeViewEngine>();
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), 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<IView>();
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
.Callback((ViewContext v) =>
{
Assert.Equal(expected, v.ViewData.TemplateInfo.HtmlFieldPrefix);
})
.Returns(Task.CompletedTask)
.Verifiable();
var viewEngine = new Mock<ICompositeViewEngine>();
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), 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<IView>();
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
.Callback((ViewContext v) =>
{
Assert.Same(model, v.ViewData.Model);
})
.Returns(Task.CompletedTask)
.Verifiable();
var viewEngine = new Mock<ICompositeViewEngine>();
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), 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<IView>();
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
.Callback((ViewContext v) =>
{
Assert.Equal(expected, v.ViewData.TemplateInfo.HtmlFieldPrefix);
})
.Returns(Task.CompletedTask)
.Verifiable();
var viewEngine = new Mock<ICompositeViewEngine>();
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), 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<IDisposable>();
disposable.Setup(d => d.Dispose()).Verifiable();
var view = disposable.As<IView>();
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
.Returns(Task.CompletedTask)
.Verifiable();
var viewEngine = new Mock<ICompositeViewEngine>();
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), 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<IView>();
var viewEngine = new Mock<ICompositeViewEngine>();
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), 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<InvalidOperationException>(
() => 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<ITempDataDictionary>(),
TextWriter.Null,
new HtmlHelperOptions());
}
private static TagHelperContext GetTagHelperContext()
{
return new TagHelperContext(
"partial",
new TagHelperAttributeList(),
new Dictionary<object, object>(),
Guid.NewGuid().ToString("N"));
}
private static TagHelperOutput GetTagHelperOutput()
{
return new TagHelperOutput(
"partial",
new TagHelperAttributeList(),
(_, __) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
}
private class TestModel
{
public PropertyModel Property { get; set; }
}
private class PropertyModel
{
}
}
}

View File

@ -108,6 +108,8 @@ namespace HtmlGenerationWebSite.Controllers
return View(_products);
}
public IActionResult ProductListUsingTagHelpers() => View(_products);
public IActionResult EmployeeList()
{
var employees = new List<Employee>
@ -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();
}
}

View File

@ -0,0 +1,2 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
PartialTagHelperWithoutModel: <partial name="../Shared/_Partial" />

View File

@ -0,0 +1,25 @@
@using HtmlGenerationWebSite.Models
@model IList<Product>
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<form asp-action="Index" asp-controller="HtmlGeneration_Product" asp-antiforgery="true">
@for (var i = 0; i < Model.Count; i++)
{
<div>
<label asp-for="@Model[i].HomePage" class="product"></label>
<input asp-for="@Model[i].HomePage" type="url" size="50" disabled="disabled" readonly="readonly" />
</div>
<partial name="_ProductPartial" asp-for="@Model[i]" />
}
@* Print the HtmlFieldPrefix outside of the partial tag helper to ensure it hasn't been modified *@
<div>HtmlFieldPrefix = @ViewData.TemplateInfo.HtmlFieldPrefix</div>
<input type="submit" />
</form>
</body>
</html>

View File

@ -0,0 +1,5 @@
@model HtmlGenerationWebSite.Models.Warehouse
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<h3>@Html.DisplayFor(m => m.City)</h3>
<partial name="_EmployeePartial" asp-for="Employee" />

View File

@ -0,0 +1,15 @@
@model HtmlGenerationWebSite.Models.Employee
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<div>
<label asp-for="Name"></label>
<input asp-for="Name" type="text"/>
</div>
<div>
<label asp-for="OfficeNumber"></label>
<input asp-for="OfficeNumber" type="number"/>
</div>
<div>
<label asp-for="Address"></label>
<textarea asp-for="Address" rows="4" cols="50"></textarea>
</div>

View File

@ -0,0 +1 @@
Hello from partial