From 2705510508b08f6c50007c87e35fd32951208811 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Fri, 12 Feb 2016 18:11:48 -0800 Subject: [PATCH] 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. --- .../HtmlContentBuilder.cs | 64 +++++++++ .../HtmlEncodedString.cs | 13 +- .../HtmlTextWriter.cs | 49 ------- .../IHtmlContentBuilder.cs | 2 +- .../IHtmlContentContainer.cs | 30 +++++ .../HtmlContentBuilderExtensionsTest.cs | 14 ++ .../HtmlContentBuilderTest.cs | 124 +++++++++++++++++- 7 files changed, 240 insertions(+), 56 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.Html.Abstractions/HtmlTextWriter.cs create mode 100644 src/Microsoft.AspNetCore.Html.Abstractions/IHtmlContentContainer.cs diff --git a/src/Microsoft.AspNetCore.Html.Abstractions/HtmlContentBuilder.cs b/src/Microsoft.AspNetCore.Html.Abstractions/HtmlContentBuilder.cs index b79e400375..be218244b0 100644 --- a/src/Microsoft.AspNetCore.Html.Abstractions/HtmlContentBuilder.cs +++ b/src/Microsoft.AspNetCore.Html.Abstractions/HtmlContentBuilder.cs @@ -96,6 +96,70 @@ namespace Microsoft.AspNetCore.Html return this; } + /// + 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); + } + } + } + + /// + 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(); + } + /// public void WriteTo(TextWriter writer, HtmlEncoder encoder) { diff --git a/src/Microsoft.AspNetCore.Html.Abstractions/HtmlEncodedString.cs b/src/Microsoft.AspNetCore.Html.Abstractions/HtmlEncodedString.cs index c50fba890c..bf38ac3682 100644 --- a/src/Microsoft.AspNetCore.Html.Abstractions/HtmlEncodedString.cs +++ b/src/Microsoft.AspNetCore.Html.Abstractions/HtmlEncodedString.cs @@ -17,17 +17,20 @@ namespace Microsoft.AspNetCore.Html /// public static readonly IHtmlContent NewLine = new HtmlEncodedString(Environment.NewLine); - private readonly string _value; - /// /// Creates a new . /// /// The HTML encoded value. public HtmlEncodedString(string value) { - _value = value; + Value = value; } + /// + /// Gets the HTML encoded value. + /// + public string Value { get; } + /// 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); } /// public override string ToString() { - return _value ?? string.Empty; + return Value ?? string.Empty; } } } diff --git a/src/Microsoft.AspNetCore.Html.Abstractions/HtmlTextWriter.cs b/src/Microsoft.AspNetCore.Html.Abstractions/HtmlTextWriter.cs deleted file mode 100644 index c9158b49b6..0000000000 --- a/src/Microsoft.AspNetCore.Html.Abstractions/HtmlTextWriter.cs +++ /dev/null @@ -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 -{ - /// - /// 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.AspNetCore.Html.Abstractions/IHtmlContentBuilder.cs b/src/Microsoft.AspNetCore.Html.Abstractions/IHtmlContentBuilder.cs index 978fedc03f..912fe442aa 100644 --- a/src/Microsoft.AspNetCore.Html.Abstractions/IHtmlContentBuilder.cs +++ b/src/Microsoft.AspNetCore.Html.Abstractions/IHtmlContentBuilder.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.Html /// /// A builder for HTML content. /// - public interface IHtmlContentBuilder : IHtmlContent + public interface IHtmlContentBuilder : IHtmlContentContainer { /// /// Appends an instance. diff --git a/src/Microsoft.AspNetCore.Html.Abstractions/IHtmlContentContainer.cs b/src/Microsoft.AspNetCore.Html.Abstractions/IHtmlContentContainer.cs new file mode 100644 index 0000000000..f17811433c --- /dev/null +++ b/src/Microsoft.AspNetCore.Html.Abstractions/IHtmlContentContainer.cs @@ -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 +{ + /// + /// Defines a contract for instances made up of several components which + /// can be copied into an . + /// + public interface IHtmlContentContainer : IHtmlContent + { + /// + /// Copies the contained content of this into . + /// + /// The . + void CopyTo(IHtmlContentBuilder builder); + + /// + /// + /// Moves the contained content of this into . + /// + /// + /// After is called, this instance should be left + /// in an empty state. + /// + /// + /// The . + void MoveTo(IHtmlContentBuilder builder); + } +} diff --git a/test/Microsoft.AspNetCore.Html.Abstractions.Test/HtmlContentBuilderExtensionsTest.cs b/test/Microsoft.AspNetCore.Html.Abstractions.Test/HtmlContentBuilderExtensionsTest.cs index 6b4307f690..08151b3675 100644 --- a/test/Microsoft.AspNetCore.Html.Abstractions.Test/HtmlContentBuilderExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Html.Abstractions.Test/HtmlContentBuilderExtensionsTest.cs @@ -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) diff --git a/test/Microsoft.AspNetCore.Html.Abstractions.Test/HtmlContentBuilderTest.cs b/test/Microsoft.AspNetCore.Html.Abstractions.Test/HtmlContentBuilderTest.cs index 901512fdad..40219b19f7 100644 --- a/test/Microsoft.AspNetCore.Html.Abstractions.Test/HtmlContentBuilderTest.cs +++ b/test/Microsoft.AspNetCore.Html.Abstractions.Test/HtmlContentBuilderTest.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; 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(destination.Entries[0])); + Assert.Equal(new TestHtmlContent("hello"), Assert.IsType(destination.Entries[1])); + Assert.Equal("Test", Assert.IsType(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(destination.Entries[0])); + Assert.Equal(new TestHtmlContent("hello"), Assert.IsType(destination.Entries[1])); + Assert.Equal("Test", Assert.IsType(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(destination.Entries[0])); + Assert.Equal(new TestHtmlContent("hello"), Assert.IsType(destination.Entries[1])); + Assert.Equal("Test", Assert.IsType(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(destination.Entries[0])); + Assert.Equal(new TestHtmlContent("hello"), Assert.IsType(destination.Entries[1])); + Assert.Equal("Test", Assert.IsType(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 { 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); + } } } }