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;
}
/// <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 />
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
{

View File

@ -17,17 +17,20 @@ namespace Microsoft.AspNetCore.Html
/// </summary>
public static readonly IHtmlContent NewLine = new HtmlEncodedString(Environment.NewLine);
private readonly string _value;
/// <summary>
/// Creates a new <see cref="HtmlEncodedString"/>.
/// </summary>
/// <param name="value">The HTML encoded value.</param>
public HtmlEncodedString(string value)
{
_value = value;
Value = value;
}
/// <summary>
/// Gets the HTML encoded value.
/// </summary>
public string Value { get; }
/// <inheritdoc />
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
{
@ -41,13 +44,13 @@ namespace Microsoft.AspNetCore.Html
throw new ArgumentNullException(nameof(encoder));
}
writer.Write(_value);
writer.Write(Value);
}
/// <inheritdoc />
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>
/// A builder for HTML content.
/// </summary>
public interface IHtmlContentBuilder : IHtmlContent
public interface IHtmlContentBuilder : IHtmlContentContainer
{
/// <summary>
/// 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;
}
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)
{
foreach (var entry in Entries)

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;
using System.IO;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Html;
@ -105,6 +106,106 @@ namespace Microsoft.Extensions.Internal
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]
public void WriteTo_WritesAllItems()
{
@ -122,7 +223,7 @@ namespace Microsoft.Extensions.Internal
Assert.Equal("Written from TestHtmlContent: HelloHtmlEncode[[Test]]", writer.ToString());
}
private class TestHtmlContent : IHtmlContent
private class TestHtmlContent : IHtmlContent, IEquatable<TestHtmlContent>
{
private string _content;
@ -140,6 +241,27 @@ namespace Microsoft.Extensions.Internal
{
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);
}
}
}
}