Modify TagHelpers to use new content mode design.

- React to aspnet/Razor#221
- Modified existing TagHelpers to no longer rely on ContentBehavior and to instead utilize GetChildContentAsync, PreContent, Content and PostContent.
This commit is contained in:
N. Taylor Mullen 2014-12-17 16:32:05 -08:00 committed by NTaylorMullen
parent 2cf56c7c83
commit 7b52559366
13 changed files with 64 additions and 41 deletions

View File

@ -13,7 +13,7 @@
<form asp-anti-forgery="false" asp-action="Create"> <form asp-anti-forgery="false" asp-action="Create">
<div class="form-horizontal"> <div class="form-horizontal">
@* validation summary tag helper will target just <div/> elements and append the list of errors *@ @* validation summary tag helper will target just <div/> elements and append the list of errors *@
@* - i.e. this helper, like <select/> helper, has ContentBehavior.Append *@ @* - i.e. this helper, like <select/> helper appends content. *@
@* helper does nothing if model is valid and (client-side validation is disabled or @* helper does nothing if model is valid and (client-side validation is disabled or
asp-validation-summary="ValidationSummary.ModelOnly") *@ asp-validation-summary="ValidationSummary.ModelOnly") *@
@* don't need a bound attribute to match Html.ValidationSummary()'s headerTag parameter; users wrap message as they wish *@ @* don't need a bound attribute to match Html.ValidationSummary()'s headerTag parameter; users wrap message as they wish *@
@ -48,7 +48,7 @@
<div class="form-group"> <div class="form-group">
<label asp-for="YearsEmployeed" class="control-label col-md-2" /> <label asp-for="YearsEmployeed" class="control-label col-md-2" />
<div class="col-md-10"> <div class="col-md-10">
@* <select/> tag helper has ContentBehavior.Append -- items render after static options *@ @* <select/> items render after static options *@
@{ var @object = "multiple"; } @{ var @object = "multiple"; }
<select asp-for="@(Model.YearsEmployeed)" asp-items="@((IEnumerable<SelectListItem>)ViewBag.Items)" size="2" class="form-control" multiple="@(@object)"> <select asp-for="@(Model.YearsEmployeed)" asp-items="@((IEnumerable<SelectListItem>)ViewBag.Items)" size="2" class="form-control" multiple="@(@object)">
@* Static use of "selected" attribute may cause HTML errors if in a single-selection <select/> *@ @* Static use of "selected" attribute may cause HTML errors if in a single-selection <select/> *@

View File

@ -155,6 +155,18 @@ namespace Microsoft.AspNet.Mvc.Razor
/// be buffered until <see cref="EndWritingScope"/> is called. /// be buffered until <see cref="EndWritingScope"/> is called.
/// </remarks> /// </remarks>
public void StartWritingScope() public void StartWritingScope()
{
StartWritingScope(new StringWriter());
}
/// <summary>
/// Starts a new writing scope with the given <paramref name="writer"/>.
/// </summary>
/// <remarks>
/// All writes to the <see cref="Output"/> or <see cref="ViewContext.Writer"/> after calling this method will
/// be buffered until <see cref="EndWritingScope"/> is called.
/// </remarks>
public void StartWritingScope(TextWriter writer)
{ {
// If there isn't a base writer take the ViewContext.Writer // If there isn't a base writer take the ViewContext.Writer
if (_originalWriter == null) if (_originalWriter == null)
@ -164,7 +176,7 @@ namespace Microsoft.AspNet.Mvc.Razor
// We need to replace the ViewContext's Writer to ensure that all content (including content written // We need to replace the ViewContext's Writer to ensure that all content (including content written
// from HTML helpers) is redirected. // from HTML helpers) is redirected.
ViewContext.Writer = new StringWriter(); ViewContext.Writer = writer;
_writerScopes.Push(ViewContext.Writer); _writerScopes.Push(ViewContext.Writer);
} }

View File

@ -6,14 +6,12 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.AspNet.Razor.TagHelpers;
namespace Microsoft.AspNet.Mvc.TagHelpers namespace Microsoft.AspNet.Mvc.TagHelpers
{ {
/// <summary> /// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;form&gt; elements. /// <see cref="ITagHelper"/> implementation targeting &lt;form&gt; elements.
/// </summary> /// </summary>
[ContentBehavior(ContentBehavior.Append)]
public class FormTagHelper : TagHelper public class FormTagHelper : TagHelper
{ {
private const string ActionAttributeName = "asp-action"; private const string ActionAttributeName = "asp-action";
@ -106,7 +104,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
if (tagBuilder != null) if (tagBuilder != null)
{ {
output.MergeAttributes(tagBuilder); output.MergeAttributes(tagBuilder);
output.Content += tagBuilder.InnerHtml; output.PostContent += tagBuilder.InnerHtml;
output.SelfClosing = false; output.SelfClosing = false;
} }
} }
@ -116,7 +114,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var antiForgeryTagBuilder = Generator.GenerateAntiForgery(ViewContext); var antiForgeryTagBuilder = Generator.GenerateAntiForgery(ViewContext);
if (antiForgeryTagBuilder != null) if (antiForgeryTagBuilder != null)
{ {
output.Content += antiForgeryTagBuilder.ToString(TagRenderMode.SelfClosing); output.PostContent += antiForgeryTagBuilder.ToString(TagRenderMode.SelfClosing);
} }
} }
} }

View File

@ -15,7 +15,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <summary> /// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;input&gt; elements with an <c>asp-for</c> attribute. /// <see cref="ITagHelper"/> implementation targeting &lt;input&gt; elements with an <c>asp-for</c> attribute.
/// </summary> /// </summary>
[ContentBehavior(ContentBehavior.Replace)]
public class InputTagHelper : TagHelper public class InputTagHelper : TagHelper
{ {
private const string ForAttributeName = "asp-for"; private const string ForAttributeName = "asp-for";

View File

@ -1,16 +1,15 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.AspNet.Razor.TagHelpers;
namespace Microsoft.AspNet.Mvc.TagHelpers namespace Microsoft.AspNet.Mvc.TagHelpers
{ {
/// <summary> /// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;label&gt; elements with an <c>asp-for</c> attribute. /// <see cref="ITagHelper"/> implementation targeting &lt;label&gt; elements with an <c>asp-for</c> attribute.
/// </summary> /// </summary>
[ContentBehavior(ContentBehavior.Modify)]
public class LabelTagHelper : TagHelper public class LabelTagHelper : TagHelper
{ {
private const string ForAttributeName = "asp-for"; private const string ForAttributeName = "asp-for";
@ -31,7 +30,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <inheritdoc /> /// <inheritdoc />
/// <remarks>Does nothing if <see cref="For"/> is <c>null</c>.</remarks> /// <remarks>Does nothing if <see cref="For"/> is <c>null</c>.</remarks>
public override void Process(TagHelperContext context, TagHelperOutput output) public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{ {
if (For != null) if (For != null)
{ {
@ -48,9 +47,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
// We check for whitespace to detect scenarios such as: // We check for whitespace to detect scenarios such as:
// <label for="Name"> // <label for="Name">
// </label> // </label>
if (string.IsNullOrWhiteSpace(output.Content)) if (!output.ContentSet)
{ {
output.Content = tagBuilder.InnerHtml; var childContent = await context.GetChildContentAsync();
if (string.IsNullOrWhiteSpace(childContent))
{
// Provide default label text since there was nothing useful in the Razor source.
output.Content = tagBuilder.InnerHtml;
}
else
{
output.Content = childContent;
}
} }
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers;
@ -13,11 +14,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <see cref="ITagHelper"/> implementation targeting &lt;option&gt; elements. /// <see cref="ITagHelper"/> implementation targeting &lt;option&gt; elements.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This <see cref="ITagHelper"/> works in conjunction with <see cref="SelectTagHelper"/>. It has /// This <see cref="ITagHelper"/> works in conjunction with <see cref="SelectTagHelper"/>. It reads elements
/// <see cref="ContentBehavior.Modify"/> in order to read element's content but does not modify that content. The /// content but does not modify that content. The only modification it makes is to add a <c>selected</c> attribute
/// only modification it makes is to add a <c>selected</c> attribute in some cases. /// in some cases.
/// </remarks> /// </remarks>
[ContentBehavior(ContentBehavior.Modify)]
public class OptionTagHelper : TagHelper public class OptionTagHelper : TagHelper
{ {
// Protected to ensure subclasses are correctly activated. Internal for ease of use when testing. // Protected to ensure subclasses are correctly activated. Internal for ease of use when testing.
@ -51,7 +51,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <see cref="ICollection{string}"/> instance. Also does nothing if the associated &lt;option&gt; is already /// <see cref="ICollection{string}"/> instance. Also does nothing if the associated &lt;option&gt; is already
/// selected. /// selected.
/// </remarks> /// </remarks>
public override void Process(TagHelperContext context, TagHelperOutput output) public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{ {
// Pass through attributes that are also well-known HTML attributes. // Pass through attributes that are also well-known HTML attributes.
if (Value != null) if (Value != null)
@ -84,9 +84,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
} }
// Select this <option/> element if value attribute or content matches a selected value. Callers // Select this <option/> element if value attribute or content matches a selected value. Callers
// encode values as-needed before setting TagHelperOutput.Content. But TagHelperOutput itself // encode values as-needed while executing child content. But TagHelperOutput itself
// encodes attribute values later, when GenerateStartTag() is called. // encodes attribute values later, when GenerateStartTag() is called.
var text = output.Content; var text = await context.GetChildContentAsync();
var selected = (Value != null) ? selectedValues.Contains(Value) : encodedValues.Contains(text); var selected = (Value != null) ? selectedValues.Contains(Value) : encodedValues.Contains(text);
if (selected) if (selected)
{ {

View File

@ -15,7 +15,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <summary> /// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;select&gt; elements with an <c>asp-for</c> attribute. /// <see cref="ITagHelper"/> implementation targeting &lt;select&gt; elements with an <c>asp-for</c> attribute.
/// </summary> /// </summary>
[ContentBehavior(ContentBehavior.Append)]
public class SelectTagHelper : TagHelper public class SelectTagHelper : TagHelper
{ {
private const string ForAttributeName = "asp-for"; private const string ForAttributeName = "asp-for";
@ -146,7 +145,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
if (tagBuilder != null) if (tagBuilder != null)
{ {
output.MergeAttributes(tagBuilder); output.MergeAttributes(tagBuilder);
output.Content += tagBuilder.InnerHtml; output.PostContent += tagBuilder.InnerHtml;
output.SelfClosing = false; output.SelfClosing = false;
} }

View File

@ -10,7 +10,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <summary> /// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;textarea&gt; elements with an <c>asp-for</c> attribute. /// <see cref="ITagHelper"/> implementation targeting &lt;textarea&gt; elements with an <c>asp-for</c> attribute.
/// </summary> /// </summary>
[ContentBehavior(ContentBehavior.Replace)]
[HtmlElementName("textarea")] [HtmlElementName("textarea")]
public class TextAreaTagHelper : TagHelper public class TextAreaTagHelper : TagHelper
{ {

View File

@ -1,9 +1,9 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.AspNet.Razor.TagHelpers;
namespace Microsoft.AspNet.Mvc.TagHelpers namespace Microsoft.AspNet.Mvc.TagHelpers
{ {
@ -12,7 +12,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// attribute. /// attribute.
/// </summary> /// </summary>
[HtmlElementName("span")] [HtmlElementName("span")]
[ContentBehavior(ContentBehavior.Modify)]
public class ValidationMessageTagHelper : TagHelper public class ValidationMessageTagHelper : TagHelper
{ {
private const string ValidationForAttributeName = "asp-validation-for"; private const string ValidationForAttributeName = "asp-validation-for";
@ -33,7 +32,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <inheritdoc /> /// <inheritdoc />
/// <remarks>Does nothing if <see cref="For"/> is <c>null</c>.</remarks> /// <remarks>Does nothing if <see cref="For"/> is <c>null</c>.</remarks>
public override void Process(TagHelperContext context, TagHelperOutput output) public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{ {
if (For != null) if (For != null)
{ {
@ -50,9 +49,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
// We check for whitespace to detect scenarios such as: // We check for whitespace to detect scenarios such as:
// <span validation-for="Name"> // <span validation-for="Name">
// </span> // </span>
if (string.IsNullOrWhiteSpace(output.Content)) if (!output.ContentSet)
{ {
output.Content = tagBuilder.InnerHtml; var childContent = await context.GetChildContentAsync();
if (string.IsNullOrWhiteSpace(childContent))
{
// Provide default label text since there was nothing useful in the Razor source.
output.Content = tagBuilder.InnerHtml;
}
else
{
output.Content = childContent;
}
} }
} }
} }

View File

@ -13,7 +13,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// attribute. /// attribute.
/// </summary> /// </summary>
[HtmlElementName("div")] [HtmlElementName("div")]
[ContentBehavior(ContentBehavior.Append)]
public class ValidationSummaryTagHelper : TagHelper public class ValidationSummaryTagHelper : TagHelper
{ {
private const string ValidationSummaryAttributeName = "asp-validation-summary"; private const string ValidationSummaryAttributeName = "asp-validation-summary";
@ -81,7 +80,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
if (tagBuilder != null) if (tagBuilder != null)
{ {
output.MergeAttributes(tagBuilder); output.MergeAttributes(tagBuilder);
output.Content += tagBuilder.InnerHtml; output.PostContent += tagBuilder.InnerHtml;
} }
} }
} }

View File

@ -4,12 +4,11 @@
using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.AspNet.Razor.TagHelpers; using System.Threading.Tasks;
namespace ActivatorWebSite.TagHelpers namespace ActivatorWebSite.TagHelpers
{ {
[HtmlElementName("span")] [HtmlElementName("span")]
[ContentBehavior(ContentBehavior.Modify)]
public class HiddenTagHelper : TagHelper public class HiddenTagHelper : TagHelper
{ {
public string Name { get; set; } public string Name { get; set; }
@ -17,9 +16,11 @@ namespace ActivatorWebSite.TagHelpers
[Activate] [Activate]
public IHtmlHelper HtmlHelper { get; set; } public IHtmlHelper HtmlHelper { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output) public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{ {
output.Content = HtmlHelper.Hidden(Name, output.Content).ToString(); var content = await context.GetChildContentAsync();
output.Content = HtmlHelper.Hidden(Name, content).ToString();
} }
} }
} }

View File

@ -4,12 +4,11 @@
using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.AspNet.Razor.TagHelpers; using System.Threading.Tasks;
namespace ActivatorWebSite.TagHelpers namespace ActivatorWebSite.TagHelpers
{ {
[HtmlElementName("div")] [HtmlElementName("div")]
[ContentBehavior(ContentBehavior.Modify)]
public class RepeatContentTagHelper : TagHelper public class RepeatContentTagHelper : TagHelper
{ {
public int RepeatContent { get; set; } public int RepeatContent { get; set; }
@ -19,14 +18,14 @@ namespace ActivatorWebSite.TagHelpers
[Activate] [Activate]
public IHtmlHelper HtmlHelper { get; set; } public IHtmlHelper HtmlHelper { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output) public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{ {
var content = await context.GetChildContentAsync();
var repeatContent = HtmlHelper.Encode(Expression.Metadata.Model.ToString()); var repeatContent = HtmlHelper.Encode(Expression.Metadata.Model.ToString());
if (string.IsNullOrEmpty(repeatContent)) if (string.IsNullOrEmpty(repeatContent))
{ {
repeatContent = output.Content; repeatContent = content;
output.Content = string.Empty;
} }
for (int i = 0; i < RepeatContent; i++) for (int i = 0; i < RepeatContent; i++)

View File

@ -9,7 +9,6 @@ using Microsoft.AspNet.Razor.TagHelpers;
namespace ActivatorWebSite.TagHelpers namespace ActivatorWebSite.TagHelpers
{ {
[HtmlElementName("body")] [HtmlElementName("body")]
[ContentBehavior(ContentBehavior.Prepend)]
public class TitleTagHelper : TagHelper public class TitleTagHelper : TagHelper
{ {
[Activate] [Activate]
@ -23,7 +22,7 @@ namespace ActivatorWebSite.TagHelpers
var builder = new TagBuilder("h2"); var builder = new TagBuilder("h2");
var title = ViewContext.ViewBag.Title; var title = ViewContext.ViewBag.Title;
builder.InnerHtml = HtmlHelper.Encode(title); builder.InnerHtml = HtmlHelper.Encode(title);
output.Content = builder.ToString(); output.PreContent = builder.ToString();
} }
} }
} }