Add Copy/Move for content - Remove HtmlTextWriter

Currently we overload the definition of WriteTo on IHtmlContent
implementation classes to either represent a "real" write or a "flatten"
by checking if the writer inherits HtmlText writer.

This overloading is a bit of an odd fit and hides the real semantic we
want for flattening.

Additionally, we want to gradually make the concept of a pooled
backing-buffer for IHtmlContent first-class - using pooled buffers
dictates that we support move-semantics to some degree.

This change makes the work that we do for flattening into pooled buffers
explicit rather than hidden.
This commit is contained in:
Ryan Nowak 2016-02-12 18:11:48 -08:00
parent b9d2dc89aa
commit 2705510508
7 changed files with 240 additions and 56 deletions

View File

@ -96,6 +96,70 @@ namespace Microsoft.AspNetCore.Html
return this; return this;
} }
/// <inheritdoc />
public void CopyTo(IHtmlContentBuilder destination)
{
if (destination == null)
{
throw new ArgumentNullException(nameof(destination));
}
for (var i = 0; i < Entries.Count; i++)
{
var entry = Entries[i];
string entryAsString;
IHtmlContentContainer entryAsContainer;
if ((entryAsString = entry as string) != null)
{
destination.Append(entryAsString);
}
else if ((entryAsContainer = entry as IHtmlContentContainer) != null)
{
// Since we're copying, do a deep flatten.
entryAsContainer.CopyTo(destination);
}
else
{
// Only string, IHtmlContent values can be added to the buffer.
destination.AppendHtml((IHtmlContent)entry);
}
}
}
/// <inheritdoc />
public void MoveTo(IHtmlContentBuilder destination)
{
if (destination == null)
{
throw new ArgumentNullException(nameof(destination));
}
for (var i = 0; i < Entries.Count; i++)
{
var entry = Entries[i];
string entryAsString;
IHtmlContentContainer entryAsContainer;
if ((entryAsString = entry as string) != null)
{
destination.Append(entryAsString);
}
else if ((entryAsContainer = entry as IHtmlContentContainer) != null)
{
// Since we're moving, do a deep flatten.
entryAsContainer.MoveTo(destination);
}
else
{
// Only string, IHtmlContent values can be added to the buffer.
destination.AppendHtml((IHtmlContent)entry);
}
}
Entries.Clear();
}
/// <inheritdoc /> /// <inheritdoc />
public void WriteTo(TextWriter writer, HtmlEncoder encoder) public void WriteTo(TextWriter writer, HtmlEncoder encoder)
{ {

View File

@ -17,17 +17,20 @@ namespace Microsoft.AspNetCore.Html
/// </summary> /// </summary>
public static readonly IHtmlContent NewLine = new HtmlEncodedString(Environment.NewLine); public static readonly IHtmlContent NewLine = new HtmlEncodedString(Environment.NewLine);
private readonly string _value;
/// <summary> /// <summary>
/// Creates a new <see cref="HtmlEncodedString"/>. /// Creates a new <see cref="HtmlEncodedString"/>.
/// </summary> /// </summary>
/// <param name="value">The HTML encoded value.</param> /// <param name="value">The HTML encoded value.</param>
public HtmlEncodedString(string value) public HtmlEncodedString(string value)
{ {
_value = value; Value = value;
} }
/// <summary>
/// Gets the HTML encoded value.
/// </summary>
public string Value { get; }
/// <inheritdoc /> /// <inheritdoc />
public void WriteTo(TextWriter writer, HtmlEncoder encoder) public void WriteTo(TextWriter writer, HtmlEncoder encoder)
{ {
@ -41,13 +44,13 @@ namespace Microsoft.AspNetCore.Html
throw new ArgumentNullException(nameof(encoder)); throw new ArgumentNullException(nameof(encoder));
} }
writer.Write(_value); writer.Write(Value);
} }
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() public override string ToString()
{ {
return _value ?? string.Empty; return Value ?? string.Empty;
} }
} }
} }

View File

@ -1,49 +0,0 @@
// 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;
namespace Microsoft.AspNetCore.Html
{
/// <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

@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.Html
/// <summary> /// <summary>
/// A builder for HTML content. /// A builder for HTML content.
/// </summary> /// </summary>
public interface IHtmlContentBuilder : IHtmlContent public interface IHtmlContentBuilder : IHtmlContentContainer
{ {
/// <summary> /// <summary>
/// Appends an <see cref="IHtmlContent"/> instance. /// Appends an <see cref="IHtmlContent"/> instance.

View File

@ -0,0 +1,30 @@
// 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.
namespace Microsoft.AspNetCore.Html
{
/// <summary>
/// Defines a contract for <see cref="IHtmlContent"/> instances made up of several components which
/// can be copied into an <see cref="IHtmlContentBuilder"/>.
/// </summary>
public interface IHtmlContentContainer : IHtmlContent
{
/// <summary>
/// Copies the contained content of this <see cref="IHtmlContentContainer"/> into <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="IHtmlContentBuilder"/>.</param>
void CopyTo(IHtmlContentBuilder builder);
/// <summary>
/// <para>
/// Moves the contained content of this <see cref="IHtmlContentContainer"/> into <paramref name="builder"/>.
/// </para>
/// <para>
/// After <see cref="MoveTo"/> is called, this <see cref="IHtmlContentContainer"/> instance should be left
/// in an empty state.
/// </para>
/// </summary>
/// <param name="builder">The <see cref="IHtmlContentBuilder"/>.</param>
void MoveTo(IHtmlContentBuilder builder);
}
}

View File

@ -392,6 +392,20 @@ namespace Microsoft.AspNetCore.Html.Test
return this; return this;
} }
public void CopyTo(IHtmlContentBuilder destination)
{
foreach (var entry in Entries)
{
destination.AppendHtml(entry);
}
}
public void MoveTo(IHtmlContentBuilder destination)
{
CopyTo(destination);
Clear();
}
public void WriteTo(TextWriter writer, HtmlEncoder encoder) public void WriteTo(TextWriter writer, HtmlEncoder encoder)
{ {
foreach (var entry in Entries) foreach (var entry in Entries)

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO; using System.IO;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Html;
@ -105,6 +106,106 @@ namespace Microsoft.Extensions.Internal
Assert.Equal(0, content.Entries.Count); Assert.Equal(0, content.Entries.Count);
} }
[Fact]
public void CopyTo_CopiesAllItems()
{
// Arrange
var source = new HtmlContentBuilder();
source.AppendHtml(new TestHtmlContent("hello"));
source.Append("Test");
var destination = new HtmlContentBuilder();
destination.Append("some-content");
// Act
source.CopyTo(destination);
// Assert
Assert.Equal(2, source.Entries.Count);
Assert.Equal(3, destination.Entries.Count);
Assert.Equal("some-content", Assert.IsType<string>(destination.Entries[0]));
Assert.Equal(new TestHtmlContent("hello"), Assert.IsType<TestHtmlContent>(destination.Entries[1]));
Assert.Equal("Test", Assert.IsType<string>(destination.Entries[2]));
}
[Fact]
public void CopyTo_DoesDeepCopy()
{
// Arrange
var source = new HtmlContentBuilder();
var nested = new HtmlContentBuilder();
source.AppendHtml(nested);
nested.AppendHtml(new TestHtmlContent("hello"));
source.Append("Test");
var destination = new HtmlContentBuilder();
destination.Append("some-content");
// Act
source.CopyTo(destination);
// Assert
Assert.Equal(2, source.Entries.Count);
Assert.Equal(1, nested.Entries.Count);
Assert.Equal(3, destination.Entries.Count);
Assert.Equal("some-content", Assert.IsType<string>(destination.Entries[0]));
Assert.Equal(new TestHtmlContent("hello"), Assert.IsType<TestHtmlContent>(destination.Entries[1]));
Assert.Equal("Test", Assert.IsType<string>(destination.Entries[2]));
}
[Fact]
public void MoveTo_CopiesAllItems_AndClears()
{
// Arrange
var source = new HtmlContentBuilder();
source.AppendHtml(new TestHtmlContent("hello"));
source.Append("Test");
var destination = new HtmlContentBuilder();
destination.Append("some-content");
// Act
source.MoveTo(destination);
// Assert
Assert.Equal(0, source.Entries.Count);
Assert.Equal(3, destination.Entries.Count);
Assert.Equal("some-content", Assert.IsType<string>(destination.Entries[0]));
Assert.Equal(new TestHtmlContent("hello"), Assert.IsType<TestHtmlContent>(destination.Entries[1]));
Assert.Equal("Test", Assert.IsType<string>(destination.Entries[2]));
}
[Fact]
public void MoveTo_DoesDeepMove()
{
// Arrange
var source = new HtmlContentBuilder();
var nested = new HtmlContentBuilder();
source.AppendHtml(nested);
nested.AppendHtml(new TestHtmlContent("hello"));
source.Append("Test");
var destination = new HtmlContentBuilder();
destination.Append("some-content");
// Act
source.MoveTo(destination);
// Assert
Assert.Equal(0, source.Entries.Count);
Assert.Equal(0, nested.Entries.Count);
Assert.Equal(3, destination.Entries.Count);
Assert.Equal("some-content", Assert.IsType<string>(destination.Entries[0]));
Assert.Equal(new TestHtmlContent("hello"), Assert.IsType<TestHtmlContent>(destination.Entries[1]));
Assert.Equal("Test", Assert.IsType<string>(destination.Entries[2]));
}
[Fact] [Fact]
public void WriteTo_WritesAllItems() public void WriteTo_WritesAllItems()
{ {
@ -122,7 +223,7 @@ namespace Microsoft.Extensions.Internal
Assert.Equal("Written from TestHtmlContent: HelloHtmlEncode[[Test]]", writer.ToString()); Assert.Equal("Written from TestHtmlContent: HelloHtmlEncode[[Test]]", writer.ToString());
} }
private class TestHtmlContent : IHtmlContent private class TestHtmlContent : IHtmlContent, IEquatable<TestHtmlContent>
{ {
private string _content; private string _content;
@ -140,6 +241,27 @@ namespace Microsoft.Extensions.Internal
{ {
return "Written from TestHtmlContent: " + _content; return "Written from TestHtmlContent: " + _content;
} }
public override int GetHashCode()
{
return _content.GetHashCode();
}
public override bool Equals(object obj)
{
var other = obj as TestHtmlContent;
if (other != null)
{
return Equals(other);
}
return base.Equals(obj);
}
public bool Equals(TestHtmlContent other)
{
return string.Equals(_content, other._content);
}
} }
} }
} }