From 9a15b54d30413de518949718c75d3493ee542676 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 15 Sep 2015 11:48:53 -0700 Subject: [PATCH] Flow IHtmlContent through to the razor buffer --- src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs | 38 ++++++++--- .../RazorTextWriter.cs | 34 +++++----- .../HtmlTextWriter.cs | 50 ++++++++++++++ .../Rendering/Html/HtmlHelper.cs | 26 ++++++- .../Rendering/Html/TagBuilder.cs | 11 +++ .../Rendering/StringCollectionTextWriter.cs | 35 ++++++---- .../Rendering/StringHtmlContent.cs | 11 ++- .../RazorPageTest.cs | 13 ++-- .../RazorTextWriterTest.cs | 68 +++++++++++++++++++ .../StringCollectionTextWriterTest.cs | 49 +++++++++++++ .../Rendering/StringHtmlContentTest.cs | 20 ------ 11 files changed, 286 insertions(+), 69 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.ViewFeatures/HtmlTextWriter.cs diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs index 205206bca2..274a63bd0f 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs @@ -14,6 +14,7 @@ using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.Actions; using Microsoft.AspNet.Mvc.Razor.Internal; using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.Mvc.ViewFeatures; using Microsoft.AspNet.PageExecutionInstrumentation; using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.Framework.DependencyInjection; @@ -434,17 +435,34 @@ namespace Microsoft.AspNet.Mvc.Razor // an attribute value that may have been quoted with single quotes, must handle any double quotes // in the value. Writing the value out surrounded by double quotes. // - // Do not combine following condition with check of escapeQuotes; htmlContent.ToString() can be - // expensive when the IHtmlContent is created with a BufferedHtmlContent. - var stringValue = htmlContent.ToString(); - if (stringValue.Contains("\"")) + // This is really not optimal from a perf point of view, but it's the best we can do for right now. + using (var stringWriter = new StringWriter()) { - writer.Write(stringValue.Replace("\"", """)); + htmlContent.WriteTo(stringWriter, encoder); + + var stringValue = stringWriter.ToString(); + if (stringValue.Contains("\"")) + { + stringValue = stringValue.Replace("\"", """); + } + + writer.Write(stringValue); return; } } - htmlContent.WriteTo(writer, encoder); + var htmlTextWriter = writer as HtmlTextWriter; + if (htmlTextWriter == null) + { + htmlContent.WriteTo(writer, encoder); + } + else + { + // This special case alows us to keep buffering as IHtmlContent until we get to the 'final' + // TextWriter. + htmlTextWriter.Write(htmlContent); + } + return; } @@ -618,9 +636,13 @@ namespace Microsoft.AspNet.Mvc.Razor WriteUnprefixedAttributeValueTo(valueBuffer, value); } - var htmlString = new HtmlString(valueBuffer.ToString()); + using (var stringWriter = new StringWriter()) + { + valueBuffer.Content.WriteTo(stringWriter, HtmlEncoder); - executionContext.AddHtmlAttribute(attributeName, htmlString); + var htmlString = new HtmlString(stringWriter.ToString()); + executionContext.AddHtmlAttribute(attributeName, htmlString); + } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs index f4d325d821..afdcaab6be 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorTextWriter.cs @@ -2,20 +2,19 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using System.IO; using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Html.Abstractions; -using Microsoft.AspNet.Mvc.Razor.Internal; using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.Mvc.ViewFeatures; using Microsoft.Framework.Internal; using Microsoft.Framework.WebEncoders; namespace Microsoft.AspNet.Mvc.Razor { /// - /// A that is backed by a unbuffered writer (over the Response stream) and a buffered + /// An that is backed by a unbuffered writer (over the Response stream) and a buffered /// . When Flush or FlushAsync is invoked, the writer /// copies all content from the buffered writer to the unbuffered one and switches to writing to the unbuffered /// writer for all further write operations. @@ -24,7 +23,7 @@ namespace Microsoft.AspNet.Mvc.Razor /// This type is designed to avoid creating large in-memory strings when buffering and supporting the contract that /// expects. /// - public class RazorTextWriter : TextWriter, IBufferedTextWriter + public class RazorTextWriter : HtmlTextWriter, IBufferedTextWriter { /// /// Creates a new instance of . @@ -66,19 +65,6 @@ namespace Microsoft.AspNet.Mvc.Razor TargetWriter.Write(value); } - /// - public override void Write(object value) - { - var htmlContent = value as IHtmlContent; - if (htmlContent != null) - { - htmlContent.WriteTo(TargetWriter, HtmlEncoder); - return; - } - - base.Write(value); - } - /// public override void Write([NotNull] char[] buffer, int index, int count) { @@ -103,6 +89,20 @@ namespace Microsoft.AspNet.Mvc.Razor } } + /// + public override void Write(IHtmlContent value) + { + var htmlTextWriter = TargetWriter as HtmlTextWriter; + if (htmlTextWriter == null) + { + value.WriteTo(TargetWriter, HtmlEncoder); + } + else + { + htmlTextWriter.Write(value); + } + } + /// public override Task WriteAsync(char value) { diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/HtmlTextWriter.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/HtmlTextWriter.cs new file mode 100644 index 0000000000..ba63907357 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/HtmlTextWriter.cs @@ -0,0 +1,50 @@ +// 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.IO; +using Microsoft.AspNet.Html.Abstractions; + +namespace Microsoft.AspNet.Mvc.ViewFeatures +{ + /// + /// A which supports special processing of . + /// + public abstract class HtmlTextWriter : TextWriter + { + /// + /// Writes an value. + /// + /// The value. + public abstract void Write(IHtmlContent value); + + /// + public override void Write(object value) + { + var htmlContent = value as IHtmlContent; + if (htmlContent == null) + { + base.Write(value); + } + else + { + Write(htmlContent); + } + } + + /// + public override void WriteLine(object value) + { + var htmlContent = value as IHtmlContent; + if (htmlContent == null) + { + base.Write(value); + } + else + { + Write(htmlContent); + } + + base.WriteLine(); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/Html/HtmlHelper.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/Html/HtmlHelper.cs index 90d3c8d661..f789f92332 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/Html/HtmlHelper.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/Html/HtmlHelper.cs @@ -748,7 +748,18 @@ namespace Microsoft.AspNet.Mvc.Rendering if (tagBuilder != null) { tagBuilder.TagRenderMode = TagRenderMode.StartTag; - tagBuilder.WriteTo(ViewContext.Writer, _htmlEncoder); + + // As a perf optimization, we can buffer this output rather than writing it + // out character by character. + var htmlWriter = ViewContext.Writer as HtmlTextWriter; + if (htmlWriter == null) + { + tagBuilder.WriteTo(ViewContext.Writer, _htmlEncoder); + } + else + { + htmlWriter.Write(tagBuilder); + } } return CreateForm(); @@ -791,7 +802,18 @@ namespace Microsoft.AspNet.Mvc.Rendering if (tagBuilder != null) { tagBuilder.TagRenderMode = TagRenderMode.StartTag; - tagBuilder.WriteTo(ViewContext.Writer, _htmlEncoder); + + // As a perf optimization, we can buffer this output rather than writing it + // out character by character. + var htmlWriter = ViewContext.Writer as HtmlTextWriter; + if (htmlWriter == null) + { + tagBuilder.WriteTo(ViewContext.Writer, _htmlEncoder); + } + else + { + htmlWriter.Write(tagBuilder); + } } return CreateForm(); diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/Html/TagBuilder.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/Html/TagBuilder.cs index 495e031a33..0b4ab7764a 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/Html/TagBuilder.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/Html/TagBuilder.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; @@ -13,6 +14,7 @@ using Microsoft.Framework.WebEncoders; namespace Microsoft.AspNet.Mvc.Rendering { + [DebuggerDisplay("{DebuggerToString()}")] public class TagBuilder : IHtmlContent { public TagBuilder(string tagName) @@ -227,6 +229,15 @@ namespace Microsoft.AspNet.Mvc.Rendering } } + private string DebuggerToString() + { + using (var writer = new StringWriter()) + { + WriteTo(writer, HtmlEncoder.Default); + return writer.ToString(); + } + } + private static class Html401IdUtil { public static bool IsAsciiLetter(char testChar) diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/StringCollectionTextWriter.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/StringCollectionTextWriter.cs index ceadacb064..30eb6d26e0 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/StringCollectionTextWriter.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/StringCollectionTextWriter.cs @@ -3,23 +3,26 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Html.Abstractions; using Microsoft.Framework.Internal; using Microsoft.Framework.WebEncoders; +using Microsoft.AspNet.Mvc.ViewFeatures; namespace Microsoft.AspNet.Mvc.Rendering { /// - /// A that represents individual write operations as a sequence of strings. + /// A that stores individual write operations as a sequence of + /// and instances. /// /// /// This is primarily designed to avoid creating large in-memory strings. /// Refer to https://aspnetwebstack.codeplex.com/workitem/585 for more details. /// - public class StringCollectionTextWriter : TextWriter + public class StringCollectionTextWriter : HtmlTextWriter { private const int MaxCharToStringLength = 1024; private static readonly Task _completedTask = Task.FromResult(0); @@ -96,6 +99,12 @@ namespace Microsoft.AspNet.Mvc.Rendering _content.Append(value); } + /// + public override void Write(IHtmlContent value) + { + _content.Append(value); + } + /// public override Task WriteAsync(char value) { @@ -189,17 +198,8 @@ namespace Microsoft.AspNet.Mvc.Rendering return _completedTask; } - /// - public override string ToString() - { - using (var writer = new StringWriter()) - { - Content.WriteTo(writer, HtmlEncoder.Default); - return writer.ToString(); - } - } - - internal class StringCollectionTextWriterContent : IHtmlContent + [DebuggerDisplay("{DebuggerToString()}")] + private class StringCollectionTextWriterContent : IHtmlContent { private readonly List _entries; @@ -238,6 +238,15 @@ namespace Microsoft.AspNet.Mvc.Rendering } } } + + private string DebuggerToString() + { + using (var writer = new StringWriter()) + { + WriteTo(writer, HtmlEncoder.Default); + return writer.ToString(); + } + } } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/StringHtmlContent.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/StringHtmlContent.cs index 1604142348..6b78985053 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/StringHtmlContent.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/StringHtmlContent.cs @@ -1,6 +1,7 @@ // 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.Diagnostics; using System.IO; using Microsoft.AspNet.Html.Abstractions; using Microsoft.Framework.Internal; @@ -11,6 +12,7 @@ namespace Microsoft.AspNet.Mvc.Rendering /// /// String content which gets encoded when written. /// + [DebuggerDisplay("{DebuggerToString()}")] public class StringHtmlContent : IHtmlContent { private readonly string _input; @@ -30,10 +32,13 @@ namespace Microsoft.AspNet.Mvc.Rendering encoder.HtmlEncode(_input, writer); } - /// - public override string ToString() + private string DebuggerToString() { - return _input; + using (var writer = new StringWriter()) + { + WriteTo(writer, HtmlEncoder.Default); + return writer.ToString(); + } } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs index 9e25487b2b..1ff23da79f 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNet.Html.Abstractions; using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Mvc.TestCommon; @@ -847,7 +848,7 @@ namespace Microsoft.AspNet.Mvc.Razor var htmlAttribute = Assert.Single(executionContext.HTMLAttributes); Assert.Equal("someattr", htmlAttribute.Name, StringComparer.Ordinal); Assert.IsType(htmlAttribute.Value); - Assert.Equal(expectedValue, htmlAttribute.Value.ToString(), StringComparer.Ordinal); + Assert.Equal(expectedValue, HtmlContentUtilities.HtmlContentToString((IHtmlContent)htmlAttribute.Value)); Assert.False(htmlAttribute.Minimized); var allAttribute = Assert.Single(executionContext.AllAttributes); Assert.Equal("someattr", allAttribute.Name, StringComparer.Ordinal); @@ -1032,7 +1033,7 @@ namespace Microsoft.AspNet.Mvc.Razor // Assert var buffer = writer.BufferedWriter.Entries; Assert.Equal(1, buffer.Count); - Assert.Equal("Hello world", buffer[0]); + Assert.Equal("Hello world", HtmlContentUtilities.HtmlContentToString(((IHtmlContent)buffer[0]))); } public static TheoryData WriteTagHelper_InputData @@ -1660,7 +1661,7 @@ namespace Microsoft.AspNet.Mvc.Razor await page.ExecuteAsync(); // Assert - Assert.Equal(expected, writer.ToString()); + Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(writer.Content)); } [Theory] @@ -1702,7 +1703,7 @@ namespace Microsoft.AspNet.Mvc.Razor await page.ExecuteAsync(); // Assert - Assert.Equal(expected, writer.ToString()); + Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(writer.Content)); } [Fact] @@ -1731,7 +1732,7 @@ namespace Microsoft.AspNet.Mvc.Razor await page.ExecuteAsync(); // Assert - Assert.Equal("

Hello World!

", writer.ToString()); + Assert.Equal("

Hello World!

", HtmlContentUtilities.HtmlContentToString(writer.Content)); } [Theory] @@ -1760,7 +1761,7 @@ namespace Microsoft.AspNet.Mvc.Razor await page.ExecuteAsync(); // Assert - Assert.Equal(expected, writer.ToString()); + Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(writer.Content)); } private static TagHelperOutput GetTagHelperOutput( diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs index 39b1f2c760..8a4aceb0ca 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorTextWriterTest.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Testing; using Microsoft.Framework.WebEncoders.Testing; using Moq; @@ -215,6 +216,73 @@ namespace Microsoft.AspNet.Mvc.Razor.Test Assert.Equal(new[] { input1, input2, newLine, input3, input4, newLine }, actual); } + [Fact] + public void Write_HtmlContent_AddsToEntries() + { + // Arrange + var writer = new RazorTextWriter(TextWriter.Null, Encoding.UTF8, new CommonTestEncoder()); + var content = new HtmlString("Hello, world!"); + + // Act + writer.Write(content); + + // Assert + Assert.Collection( + writer.BufferedWriter.Entries, + item => Assert.Same(content, item)); + } + + [Fact] + public void Write_Object_HtmlContent_AddsToEntries() + { + // Arrange + var writer = new RazorTextWriter(TextWriter.Null, Encoding.UTF8, new CommonTestEncoder()); + var content = new HtmlString("Hello, world!"); + + // Act + writer.Write((object)content); + + // Assert + Assert.Collection( + writer.BufferedWriter.Entries, + item => Assert.Same(content, item)); + } + + [Fact] + public void WriteLine_Object_HtmlContent_AddsToEntries() + { + // Arrange + var writer = new RazorTextWriter(TextWriter.Null, Encoding.UTF8, new CommonTestEncoder()); + var content = new HtmlString("Hello, world!"); + + // Act + writer.WriteLine(content); + + // Assert + Assert.Collection( + writer.BufferedWriter.Entries, + item => Assert.Same(content, item), + item => Assert.Equal(Environment.NewLine, item)); + } + + [Fact] + public void Write_HtmlContent_AfterFlush_GoesToStream() + { + // Arrange + var stringWriter = new StringWriter(); + + var writer = new RazorTextWriter(stringWriter, Encoding.UTF8, new CommonTestEncoder()); + writer.Flush(); + + var content = new HtmlString("Hello, world!"); + + // Act + writer.Write(content); + + // Assert + Assert.Equal("Hello, world!", stringWriter.ToString()); + } + [Fact] public void Copy_CopiesContent_IfTargetTextWriterIsARazorTextWriterAndBuffering() { diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/StringCollectionTextWriterTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/StringCollectionTextWriterTest.cs index 6ae94c5122..317e980cca 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/StringCollectionTextWriterTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/StringCollectionTextWriterTest.cs @@ -115,6 +115,55 @@ namespace Microsoft.AspNet.Mvc.Rendering Assert.Equal(new[] { input1, input2, newLine, input3, input4, newLine }, actual); } + [Fact] + public void Write_HtmlContent_AddsToEntries() + { + // Arrange + var writer = new StringCollectionTextWriter(Encoding.UTF8); + var content = new HtmlString("Hello, world!"); + + // Act + writer.Write(content); + + // Assert + Assert.Collection( + writer.Entries, + item => Assert.Same(content, item)); + } + + [Fact] + public void Write_Object_HtmlContent_AddsToEntries() + { + // Arrange + var writer = new StringCollectionTextWriter(Encoding.UTF8); + var content = new HtmlString("Hello, world!"); + + // Act + writer.Write((object)content); + + // Assert + Assert.Collection( + writer.Entries, + item => Assert.Same(content, item)); + } + + [Fact] + public void WriteLine_Object_HtmlContent_AddsToEntries() + { + // Arrange + var writer = new StringCollectionTextWriter(Encoding.UTF8); + var content = new HtmlString("Hello, world!"); + + // Act + writer.WriteLine(content); + + // Assert + Assert.Collection( + writer.Entries, + item => Assert.Same(content, item), + item => Assert.Equal(Environment.NewLine, item)); + } + [Fact] public void Copy_CopiesContent_IfTargetTextWriterIsAStringCollectionTextWriter() { diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/StringHtmlContentTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/StringHtmlContentTest.cs index a5c4723aaa..885e69f6fc 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/StringHtmlContentTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/StringHtmlContentTest.cs @@ -9,26 +9,6 @@ namespace Microsoft.AspNet.Mvc.Rendering { public class StringHtmlContentTest { - [Fact] - public void ToString_ReturnsAString() - { - // Arrange & Act - var content = new StringHtmlContent("Hello World"); - - // Assert - Assert.Equal("Hello World", content.ToString()); - } - - [Fact] - public void ToString_ReturnsNullForNullInput() - { - // Arrange & Act - var content = new StringHtmlContent(null); - - // Assert - Assert.Null(content.ToString()); - } - [Fact] public void WriteTo_WritesContent() {