Flow IHtmlContent through to the razor buffer

This commit is contained in:
Ryan Nowak 2015-09-15 11:48:53 -07:00
parent a707311d9e
commit 9a15b54d30
11 changed files with 286 additions and 69 deletions

View File

@ -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);
}
}
}

View File

@ -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
{
/// <summary>
/// A <see cref="TextWriter"/> that is backed by a unbuffered writer (over the Response stream) and a buffered
/// An <see cref="HtmlTextWriter"/> that is backed by a unbuffered writer (over the Response stream) and a buffered
/// <see cref="StringCollectionTextWriter"/>. When <c>Flush</c> or <c>FlushAsync</c> 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
/// <see cref="RazorPage.FlushAsync"/> expects.
/// </remarks>
public class RazorTextWriter : TextWriter, IBufferedTextWriter
public class RazorTextWriter : HtmlTextWriter, IBufferedTextWriter
{
/// <summary>
/// Creates a new instance of <see cref="RazorTextWriter"/>.
@ -66,19 +65,6 @@ namespace Microsoft.AspNet.Mvc.Razor
TargetWriter.Write(value);
}
/// <inheritdoc />
public override void Write(object value)
{
var htmlContent = value as IHtmlContent;
if (htmlContent != null)
{
htmlContent.WriteTo(TargetWriter, HtmlEncoder);
return;
}
base.Write(value);
}
/// <inheritdoc />
public override void Write([NotNull] char[] buffer, int index, int count)
{
@ -103,6 +89,20 @@ namespace Microsoft.AspNet.Mvc.Razor
}
}
/// <inheritdoc />
public override void Write(IHtmlContent value)
{
var htmlTextWriter = TargetWriter as HtmlTextWriter;
if (htmlTextWriter == null)
{
value.WriteTo(TargetWriter, HtmlEncoder);
}
else
{
htmlTextWriter.Write(value);
}
}
/// <inheritdoc />
public override Task WriteAsync(char value)
{

View File

@ -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
{
/// <summary>
/// A <see cref="TextWriter"/> which supports special processing of <see cref="IHtmlContent"/>.
/// </summary>
public abstract class HtmlTextWriter : TextWriter
{
/// <summary>
/// Writes an <see cref="IHtmlContent"/> value.
/// </summary>
/// <param name="value">The <see cref="IHtmlContent"/> value.</param>
public abstract void Write(IHtmlContent value);
/// <inheritdoc />
public override void Write(object value)
{
var htmlContent = value as IHtmlContent;
if (htmlContent == null)
{
base.Write(value);
}
else
{
Write(htmlContent);
}
}
/// <inheritdoc />
public override void WriteLine(object value)
{
var htmlContent = value as IHtmlContent;
if (htmlContent == null)
{
base.Write(value);
}
else
{
Write(htmlContent);
}
base.WriteLine();
}
}
}

View File

@ -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();

View File

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

View File

@ -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
{
/// <summary>
/// A <see cref="TextWriter"/> that represents individual write operations as a sequence of strings.
/// A <see cref="HtmlTextWriter"/> that stores individual write operations as a sequence of
/// <see cref="string"/> and <see cref="IHtmlContent"/> instances.
/// </summary>
/// <remarks>
/// This is primarily designed to avoid creating large in-memory strings.
/// Refer to https://aspnetwebstack.codeplex.com/workitem/585 for more details.
/// </remarks>
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);
}
/// <inheritdoc />
public override void Write(IHtmlContent value)
{
_content.Append(value);
}
/// <inheritdoc />
public override Task WriteAsync(char value)
{
@ -189,17 +198,8 @@ namespace Microsoft.AspNet.Mvc.Rendering
return _completedTask;
}
/// <inheritdoc />
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<object> _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();
}
}
}
}
}

View File

@ -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
/// <summary>
/// String content which gets encoded when written.
/// </summary>
[DebuggerDisplay("{DebuggerToString()}")]
public class StringHtmlContent : IHtmlContent
{
private readonly string _input;
@ -30,10 +32,13 @@ namespace Microsoft.AspNet.Mvc.Rendering
encoder.HtmlEncode(_input, writer);
}
/// <inheritdoc />
public override string ToString()
private string DebuggerToString()
{
return _input;
using (var writer = new StringWriter())
{
WriteTo(writer, HtmlEncoder.Default);
return writer.ToString();
}
}
}
}

View File

@ -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<HtmlString>(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<TagHelperOutput, string> 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("<p>Hello World!</p>", writer.ToString());
Assert.Equal("<p>Hello World!</p>", 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(

View File

@ -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<object>(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()
{

View File

@ -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()
{

View File

@ -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()
{