diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorHost.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorHost.cs
index 9ea649538f..139b80304f 100644
--- a/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorHost.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorHost.cs
@@ -106,6 +106,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor
FormatInvalidIndexerAssignmentMethodName = "InvalidTagHelperIndexerAssignment",
StartTagHelperWritingScopeMethodName = "StartTagHelperWritingScope",
EndTagHelperWritingScopeMethodName = "EndTagHelperWritingScope",
+ BeginWriteTagHelperAttributeMethodName = "BeginWriteTagHelperAttribute",
+ EndWriteTagHelperAttributeMethodName = "EndWriteTagHelperAttribute",
// Can't use nameof because IHtmlHelper is (also) not accessible here.
MarkAsHtmlEncodedMethodName = HtmlHelperPropertyName + ".Raw",
diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs
index 8c460c1ceb..fe96da1030 100644
--- a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs
@@ -478,6 +478,22 @@ namespace Microsoft.AspNetCore.Mvc.Razor
return string.Format(CultureInfo.CurrentCulture, GetString("ViewLocationFormatsIsRequired"), p0);
}
+ ///
+ /// Nesting of TagHelper attribute writing scopes is not supported.
+ ///
+ internal static string RazorPage_NestingAttributeWritingScopesNotSupported
+ {
+ get { return GetString("RazorPage_NestingAttributeWritingScopesNotSupported"); }
+ }
+
+ ///
+ /// Nesting of TagHelper attribute writing scopes is not supported.
+ ///
+ internal static string FormatRazorPage_NestingAttributeWritingScopesNotSupported()
+ {
+ return GetString("RazorPage_NestingAttributeWritingScopesNotSupported");
+ }
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs
index 2795e37a44..0e5581ee75 100644
--- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs
@@ -40,6 +40,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
private IViewBufferScope _bufferScope;
private bool _ignoreBody;
private HashSet _ignoredSections;
+ private TextWriter _pageWriter;
public RazorPage()
{
@@ -231,6 +232,61 @@ namespace Microsoft.AspNetCore.Mvc.Razor
return tagHelperContent;
}
+ ///
+ /// Starts a new scope for writing attribute values.
+ ///
+ ///
+ /// All writes to the or after calling this method will
+ /// be buffered until is called.
+ /// The content will be buffered using a shared within this
+ /// Nesting of and method calls
+ /// is not supported.
+ ///
+ public void BeginWriteTagHelperAttribute()
+ {
+ if (_pageWriter != null)
+ {
+ throw new InvalidOperationException(Resources.RazorPage_NestingAttributeWritingScopesNotSupported);
+ }
+
+ _pageWriter = ViewContext.Writer;
+
+ if (_valueBuffer == null)
+ {
+ _valueBuffer = new StringWriter();
+ }
+
+ // We need to replace the ViewContext's Writer to ensure that all content (including content written
+ // from HTML helpers) is redirected.
+ ViewContext.Writer = _valueBuffer;
+
+ }
+
+ ///
+ /// Ends the current writing scope that was started by calling .
+ ///
+ /// The content buffered by the shared of this .
+ ///
+ /// This method assumes that there will be no nesting of
+ /// and method calls.
+ ///
+ public string EndWriteTagHelperAttribute()
+ {
+ if (_pageWriter == null)
+ {
+ throw new InvalidOperationException(Resources.RazorPage_ThereIsNoActiveWritingScopeToEnd);
+ }
+
+ var content = _valueBuffer.ToString();
+ _valueBuffer.GetStringBuilder().Clear();
+
+ // Restore previous writer.
+ ViewContext.Writer = _pageWriter;
+ _pageWriter = null;
+
+ return content;
+ }
+
///
/// Writes the specified with HTML encoding to .
///
diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx
index 762d5356b5..5d8203d3da 100644
--- a/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx
+++ b/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx
@@ -206,4 +206,7 @@
'{0}' cannot be empty. These locations are required to locate a view for rendering.
+
+ Nesting of TagHelper attribute writing scopes is not supported.
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/TestFiles/Output/Runtime/ModelExpressionTagHelper.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/TestFiles/Output/Runtime/ModelExpressionTagHelper.cs
index 1d2e2bc004..43acb22deb 100644
--- a/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/TestFiles/Output/Runtime/ModelExpressionTagHelper.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Razor.Host.Test/TestFiles/Output/Runtime/ModelExpressionTagHelper.cs
@@ -13,7 +13,7 @@ namespace AspNetCore
{
#line hidden
#pragma warning disable 0414
- private global::Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContent __tagHelperStringValueBuffer = null;
+ private string __tagHelperStringValueBuffer = null;
#pragma warning restore 0414
private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperExecutionContext __tagHelperExecutionContext = null;
private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner __tagHelperRunner = null;
diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs
index e4812c5605..011e07ee7f 100644
--- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs
@@ -225,6 +225,64 @@ namespace Microsoft.AspNetCore.Mvc.Razor
await page.ExecuteAsync();
}
+ [Fact]
+ public async Task EndWriteTagHelperAttribute_RestoresPageWriter()
+ {
+ // Arrange
+ var page = CreatePage(v =>
+ {
+ v.BeginWriteTagHelperAttribute();
+ v.Write("Hello World!");
+ v.EndWriteTagHelperAttribute();
+ });
+ var originalWriter = page.Output;
+
+ // Act
+ await page.ExecuteAsync();
+
+ // Assert
+ Assert.NotNull(originalWriter);
+ Assert.Same(originalWriter, page.Output);
+ }
+
+ [Fact]
+ public async Task EndWriteTagHelperAttribute_ReturnsAppropriateContent()
+ {
+ // Arrange
+ var viewContext = CreateViewContext();
+ var page = CreatePage(v =>
+ {
+ v.HtmlEncoder = new HtmlTestEncoder();
+ v.BeginWriteTagHelperAttribute();
+ v.Write("Hello World!");
+ var returnValue = v.EndWriteTagHelperAttribute();
+
+ // Assert
+ var content = Assert.IsType(returnValue);
+ Assert.Equal("HtmlEncode[[Hello World!]]", content);
+ });
+
+ // Act & Assert
+ await page.ExecuteAsync();
+ }
+
+ [Fact]
+ public async Task BeginWriteTagHelperAttribute_NestingWritingScopesThrows()
+ {
+ // Arrange
+ var viewContext = CreateViewContext();
+ var page = CreatePage(v =>
+ {
+ v.BeginWriteTagHelperAttribute();
+ v.BeginWriteTagHelperAttribute();
+ v.Write("Hello World!");
+ });
+
+ // Act & Assert
+ var ex = await Assert.ThrowsAsync(() => page.ExecuteAsync());
+ Assert.Equal("Nesting of TagHelper attribute writing scopes is not supported.", ex.Message);
+ }
+
// This is an integration test for ensuring that ViewBuffer segments used by
// TagHelpers can be merged back into the 'main' segment where possible.
[Fact]