-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Add a <partial /> tag helper #7089
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 of the partial view used to create the HTML markup. | ||
/// </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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a suggestion: Could make this tag helper an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Forgot the punchline: Then could use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The sample @DamianEdwards has essentially DIs a |
||
{ | ||
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; | ||
using (view as IDisposable) | ||
{ | ||
// 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); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could move everything except |
||
|
||
await view.RenderAsync(partialViewContext); | ||
} | ||
} | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
PartialTagHelperWithoutModel: Hello from partial |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this do just view "names" (e.g.
_LoginPartial
) or does it also do paths? Do we need to be clear about this in the docs?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't see any tests using a path, but would it work? And if paths aren't supported:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like these properties are handled identically to e.g.
viewName
parameters inController.View(...)
methods. Those are generally documented asThe name of the view that is rendered to the response.
. Same for the corresponding result properties e.g.ViewResult.ViewName
.If we need more documentation about what names, paths, et cetera mean, suggest we handle that in a separate issue. For now, let's be consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does path - https://github.com/aspnet/Mvc/pull/7089/files#diff-883159b6acb104f0fb64afa28c27a9ccR2 . The name I think comes from the IHtmlHelper docs. I'd be happy to update it if you have suggestions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, and "partial view name" doesn't appear anywhere. Isn't "name of the partial view" clear enough (assuming we stick with just "name" for now)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather have correct information than have consistent-but-useless information
These mean the same thing so I don't think this is critical.
...
So, let's come up with some good wording here and then apply elsewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How 'bout "The name or path of the [partial] view that is rendered to the response." We do lots of shenanigans under the covers handling absolute and relative paths as well as distinguishing paths from names. But, describing exactly how we choose to interpret "name" versus "path" doesn't help much -- especially because it's specific to the
RazorViewEngine
.I'm not positive what extra words we need to inform readers what distinguishes a "partial" view. It's a view found with
isMainPage: false
but the interfaces say only thatisMainPage
"Determines if the page being found is the main page for an action.". The details are again specific to theIViewEngine
implementation. (ForRazorViewEngine
, this parameter mainly controls whether or not_ViewStart.cshtml
files are used.)