Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Add a <partial /> tag helper #7089

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs
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.
Copy link
Member

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?

Copy link
Member

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:

  1. Should they be?
  2. The docs need to be clearer about what a "partial view name" is (perhaps by giving an example)

Copy link
Member

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 in Controller.View(...) methods. Those are generally documented as The 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.

Copy link
Contributor Author

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

Copy link
Member

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)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, let's be consistent.

I'd rather have correct information than have consistent-but-useless information

Oh, and "partial view name" doesn't appear anywhere.

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.

Copy link
Member

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 that isMainPage "Determines if the page being found is the main page for an action.". The details are again specific to the IViewEngine implementation. (For RazorViewEngine, this parameter mainly controls whether or not _ViewStart.cshtml files are used.)

/// </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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a suggestion: Could make this tag helper an HtmlHelper subclass which implements ITagHelper. Would have to make ViewContext a new property that calls Contextualize(...) in its setter but that should be the primary wrinkle.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot the punchline: Then could use RenderPartialCoreAsync(...).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sample @DamianEdwards has essentially DIs a IHtmlHelper, Contextualizes it and then renders the view using it. That kinda felt odd since we don't have any other use cases of doing this in any other tag helper. Given we already have a similarish copy of the code in PartialViewExecutor, I didn't think it was terrible to have yet another copy of it here.

{
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);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could move everything except var partialViewContext = ...; outside the using. Up to you.


await view.RenderAsync(partialViewContext);
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Microsoft.AspNetCore.Mvc.TagHelpers/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,12 @@ public static TheoryData<string, string> WebPagesData
// 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 },
};
Expand Down
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>
Loading