diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageBase.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageBase.cs index a646a962de..ab8818d0d4 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageBase.cs @@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// public abstract class RazorPageBase : IRazorPage { + private readonly Stack _textWriterStack = new Stack(); private StringWriter _valueBuffer; private ITagHelperFactory _tagHelperFactory; private IViewBufferScope _bufferScope; @@ -275,6 +276,25 @@ namespace Microsoft.AspNetCore.Mvc.Razor return content; } + // Internal for unit testing. + protected internal virtual void PushWriter(TextWriter writer) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + _textWriterStack.Push(ViewContext.Writer); + ViewContext.Writer = writer; + } + + // Internal for unit testing. + protected internal virtual TextWriter PopWriter() + { + ViewContext.Writer = _textWriterStack.Pop(); + return ViewContext.Writer; + } + public virtual string Href(string contentPath) { if (contentPath == null) @@ -335,63 +355,14 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// The to write. public virtual void Write(object value) { - WriteTo(Output, value); - } - - /// - /// Writes the specified with HTML encoding to . - /// - /// The instance to write to. - /// The to write. - /// - /// s of type are written using - /// . - /// For all other types, the encoded result of is written to the - /// . - /// - public virtual void WriteTo(TextWriter writer, object value) - { - if (writer == null) - { - throw new ArgumentNullException(nameof(writer)); - } - - WriteTo(writer, HtmlEncoder, value); - } - - /// - /// Writes the specified with HTML encoding to given . - /// - /// The instance to write to. - /// - /// The to use when encoding . - /// - /// The to write. - /// - /// s of type are written using - /// . - /// For all other types, the encoded result of is written to the - /// . - /// - public static void WriteTo(TextWriter writer, HtmlEncoder encoder, object value) - { - if (writer == null) - { - throw new ArgumentNullException(nameof(writer)); - } - - if (encoder == null) - { - throw new ArgumentNullException(nameof(encoder)); - } - if (value == null || value == HtmlString.Empty) { return; } - var htmlContent = value as IHtmlContent; - if (htmlContent != null) + var writer = Output; + var encoder = HtmlEncoder; + if (value is IHtmlContent htmlContent) { var bufferedWriter = writer as ViewBufferTextWriter; if (bufferedWriter == null || !bufferedWriter.IsBuffering) @@ -400,8 +371,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor } else { - var htmlContentContainer = value as IHtmlContentContainer; - if (htmlContentContainer != null) + if (value is IHtmlContentContainer htmlContentContainer) { // This is likely another ViewBuffer. htmlContentContainer.MoveTo(bufferedWriter.Buffer); @@ -417,26 +387,17 @@ namespace Microsoft.AspNetCore.Mvc.Razor return; } - WriteTo(writer, encoder, value.ToString()); + Write(value.ToString()); } /// - /// Writes the specified with HTML encoding to . + /// Writes the specified with HTML encoding to . /// - /// The instance to write to. /// The to write. - public virtual void WriteTo(TextWriter writer, string value) - { - if (writer == null) - { - throw new ArgumentNullException(nameof(writer)); - } - - WriteTo(writer, HtmlEncoder, value); - } - - private static void WriteTo(TextWriter writer, HtmlEncoder encoder, string value) + public virtual void Write(string value) { + var writer = Output; + var encoder = HtmlEncoder; if (!string.IsNullOrEmpty(value)) { // Perf: Encode right away instead of writing it character-by-character. @@ -452,42 +413,18 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// The to write. public virtual void WriteLiteral(object value) { - WriteLiteralTo(Output, value); - } - - /// - /// Writes the specified without HTML encoding to the . - /// - /// The instance to write to. - /// The to write. - public virtual void WriteLiteralTo(TextWriter writer, object value) - { - if (writer == null) - { - throw new ArgumentNullException(nameof(writer)); - } - - if (value != null) - { - WriteLiteralTo(writer, value.ToString()); - } + WriteLiteral(value.ToString()); } /// /// Writes the specified without HTML encoding to . /// - /// The instance to write to. /// The to write. - public virtual void WriteLiteralTo(TextWriter writer, string value) + public virtual void WriteLiteral(string value) { - if (writer == null) - { - throw new ArgumentNullException(nameof(writer)); - } - if (!string.IsNullOrEmpty(value)) { - writer.Write(value); + Output.Write(value); } } @@ -499,23 +436,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor int suffixOffset, int attributeValuesCount) { - BeginWriteAttributeTo(Output, name, prefix, prefixOffset, suffix, suffixOffset, attributeValuesCount); - } - - public virtual void BeginWriteAttributeTo( - TextWriter writer, - string name, - string prefix, - int prefixOffset, - string suffix, - int suffixOffset, - int attributeValuesCount) - { - if (writer == null) - { - throw new ArgumentNullException(nameof(writer)); - } - if (prefix == null) { throw new ArgumentNullException(nameof(prefix)); @@ -532,7 +452,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor // null or false. Consequently defer the prefix generation until we encounter the attribute value. if (attributeValuesCount != 1) { - WritePositionTaggedLiteral(writer, prefix, prefixOffset); + WritePositionTaggedLiteral(prefix, prefixOffset); } } @@ -543,18 +463,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor int valueOffset, int valueLength, bool isLiteral) - { - WriteAttributeValueTo(Output, prefix, prefixOffset, value, valueOffset, valueLength, isLiteral); - } - - public void WriteAttributeValueTo( - TextWriter writer, - string prefix, - int prefixOffset, - object value, - int valueOffset, - int valueLength, - bool isLiteral) { if (_attributeInfo.AttributeValuesCount == 1) { @@ -566,7 +474,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor } // We are not omitting the attribute. Write the prefix. - WritePositionTaggedLiteral(writer, _attributeInfo.Prefix, _attributeInfo.PrefixOffset); + WritePositionTaggedLiteral(_attributeInfo.Prefix, _attributeInfo.PrefixOffset); if (IsBoolTrueWithEmptyPrefixValue(prefix, value)) { @@ -582,12 +490,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor { if (!string.IsNullOrEmpty(prefix)) { - WritePositionTaggedLiteral(writer, prefix, prefixOffset); + WritePositionTaggedLiteral(prefix, prefixOffset); } BeginContext(valueOffset, valueLength, isLiteral); - WriteUnprefixedAttributeValueTo(writer, value, isLiteral); + WriteUnprefixedAttributeValue(value, isLiteral); EndContext(); } @@ -595,19 +503,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor public virtual void EndWriteAttribute() { - EndWriteAttributeTo(Output); - } - - public virtual void EndWriteAttributeTo(TextWriter writer) - { - if (writer == null) - { - throw new ArgumentNullException(nameof(writer)); - } - if (!_attributeInfo.Suppressed) { - WritePositionTaggedLiteral(writer, _attributeInfo.Suffix, _attributeInfo.SuffixOffset); + WritePositionTaggedLiteral(_attributeInfo.Suffix, _attributeInfo.SuffixOffset); } } @@ -668,12 +566,14 @@ namespace Microsoft.AspNetCore.Mvc.Razor _valueBuffer = new StringWriter(); } + PushWriter(_valueBuffer); if (!string.IsNullOrEmpty(prefix)) { - WriteLiteralTo(_valueBuffer, prefix); + WriteLiteral(prefix); } - WriteUnprefixedAttributeValueTo(_valueBuffer, value, isLiteral); + WriteUnprefixedAttributeValue(value, isLiteral); + PopWriter(); } } @@ -738,33 +638,33 @@ namespace Microsoft.AspNetCore.Mvc.Razor return HtmlString.Empty; } - private void WriteUnprefixedAttributeValueTo(TextWriter writer, object value, bool isLiteral) + private void WriteUnprefixedAttributeValue(object value, bool isLiteral) { var stringValue = value as string; // The extra branching here is to ensure that we call the Write*To(string) overload where possible. if (isLiteral && stringValue != null) { - WriteLiteralTo(writer, stringValue); + WriteLiteral(stringValue); } else if (isLiteral) { - WriteLiteralTo(writer, value); + WriteLiteral(value); } else if (stringValue != null) { - WriteTo(writer, stringValue); + Write(stringValue); } else { - WriteTo(writer, value); + Write(value); } } - private void WritePositionTaggedLiteral(TextWriter writer, string value, int position) + private void WritePositionTaggedLiteral(string value, int position) { BeginContext(position, value.Length, isLiteral: true); - WriteLiteralTo(writer, value); + WriteLiteral(value); EndContext(); } diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs index cb076afc5c..a9266c6a16 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageTest.cs @@ -1394,7 +1394,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor [Theory] [MemberData(nameof(WriteAttributeData))] - public void WriteAttributeTo_WritesAsExpected( + public void WriteAttribute_UsesSpecifiedWriter_WritesAsExpected( Tuple[] attributeValues, string expectedOutput) { @@ -1406,11 +1406,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor var suffix = string.Empty; // Act - page.BeginWriteAttributeTo(writer, "someattr", prefix, 0, suffix, 0, attributeValues.Length); + page.PushWriter(writer); + page.BeginWriteAttribute("someattr", prefix, 0, suffix, 0, attributeValues.Length); foreach (var value in attributeValues) { - page.WriteAttributeValueTo( - writer, + page.WriteAttributeValue( value.Item1, value.Item2, value.Item3, @@ -1418,12 +1418,89 @@ namespace Microsoft.AspNetCore.Mvc.Razor value.Item3?.ToString().Length ?? 0, value.Item5); } - page.EndWriteAttributeTo(writer); + page.EndWriteAttribute(); + page.PopWriter(); // Assert Assert.Equal(expectedOutput, writer.ToString()); } + [Fact] + public void PushWriter_SetsUnderlyingWriter() + { + // Arrange + var page = CreatePage(p => { }); + var writer = new StringWriter(); + + // Act + page.PushWriter(writer); + + // Assert + Assert.Same(writer, page.ViewContext.Writer); + } + + [Fact] + public void PopWriter_ResetsUnderlyingWriter() + { + // Arrange + var page = CreatePage(p => { }); + var defaultWriter = new StringWriter(); + page.ViewContext.Writer = defaultWriter; + + var writer = new StringWriter(); + + // Act 1 + page.PushWriter(writer); + + // Assert 1 + Assert.Same(writer, page.ViewContext.Writer); + + // Act 2 + var poppedWriter = page.PopWriter(); + + // Assert 2 + Assert.Same(defaultWriter, poppedWriter); + Assert.Same(defaultWriter, page.ViewContext.Writer); + } + + [Fact] + public void WriteLiteral_BuffersResultToPushedWriter() + { + // Arrange + var page = CreatePage(p => { }); + var defaultWriter = new StringWriter(); + page.ViewContext.Writer = defaultWriter; + + var bufferWriter = new StringWriter(); + + // Act + page.WriteLiteral("Not"); + page.PushWriter(bufferWriter); + page.WriteLiteral("This should be buffered"); + page.PopWriter(); + page.WriteLiteral(" buffered"); + + // Assert + Assert.Equal("Not buffered", defaultWriter.ToString()); + Assert.Equal("This should be buffered", bufferWriter.ToString()); + } + + [Fact] + public void Write_StringValue_UsesSpecifiedWriter_EncodesValue() + { + // Arrange + var page = CreatePage(p => { }); + var bufferWriter = new StringWriter(); + + // Act + page.PushWriter(bufferWriter); + page.Write("This should be encoded"); + page.PopWriter(); + + // Assert + Assert.Equal("HtmlEncode[[This should be encoded]]", bufferWriter.ToString()); + } + [Fact] public async Task Write_WithHtmlString_WritesValueWithoutEncoding() { diff --git a/test/WebSites/RazorWebSite/MyBasePage.cs b/test/WebSites/RazorWebSite/MyBasePage.cs index 917d852cf2..1a37870c52 100644 --- a/test/WebSites/RazorWebSite/MyBasePage.cs +++ b/test/WebSites/RazorWebSite/MyBasePage.cs @@ -13,10 +13,22 @@ namespace RazorWebSite base.WriteLiteral(value); } + public override void WriteLiteral(string value) + { + base.WriteLiteral("WriteLiteral says:"); + base.WriteLiteral(value); + } + public override void Write(object value) { base.WriteLiteral("Write says:"); base.Write(value); } + + public override void Write(string value) + { + base.WriteLiteral("Write says:"); + base.Write(value); + } } } \ No newline at end of file