Change `DefaultTagHelperContent` to be smart about single content entries.

- Today `TagHelperContent`s always allocate their underlying buffer even though they typically only ever have a single entry. Added a field to enable us to only allocate the backing buffer when we absolutely need to.
- Removed `IsEmpty` from `TagHelperContent` since it was not used in any of our `TagHelper`s for simplification. Changed `IsWhiteSpace` naming to be `IsEmptyOrWhiteSpace` since it can be used to indicate either state.
- Updated tests.

#621
This commit is contained in:
N. Taylor Mullen 2016-03-30 12:29:08 -07:00
parent 62fe5d6c4e
commit c400289de5
4 changed files with 200 additions and 188 deletions

View File

@ -17,20 +17,29 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
[DebuggerDisplay("{DebuggerToString(),nq}")] [DebuggerDisplay("{DebuggerToString(),nq}")]
public class DefaultTagHelperContent : TagHelperContent public class DefaultTagHelperContent : TagHelperContent
{ {
private List<object> _buffer; private object _singleContent;
private bool _isSingleContentSet;
private bool _isModified; private bool _isModified;
private bool _hasContent;
private List<object> _buffer;
private List<object> Buffer private List<object> Buffer
{ {
get get
{ {
_isModified = true;
if (_buffer == null) if (_buffer == null)
{ {
_buffer = new List<object>(); _buffer = new List<object>();
} }
if (_isSingleContentSet)
{
Debug.Assert(_buffer.Count == 0);
_buffer.Add(_singleContent);
_isSingleContentSet = false;
}
return _buffer; return _buffer;
} }
} }
@ -40,39 +49,27 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
/// <inheritdoc /> /// <inheritdoc />
/// <remarks>Returns <c>true</c> for a cleared <see cref="TagHelperContent"/>.</remarks> /// <remarks>Returns <c>true</c> for a cleared <see cref="TagHelperContent"/>.</remarks>
public override bool IsWhiteSpace public override bool IsEmptyOrWhiteSpace
{ {
get get
{ {
if (!IsModified) if (!_hasContent)
{ {
return true; return true;
} }
using (var writer = new EmptyOrWhiteSpaceWriter()) using (var writer = new EmptyOrWhiteSpaceWriter())
{ {
foreach (var entry in _buffer) if (_isSingleContentSet)
{ {
if (entry == null) return IsEmptyOrWhiteSpaceCore(_singleContent, writer);
{ }
continue;
}
var stringValue = entry as string; for (var i = 0; i < (_buffer?.Count ?? 0); i++)
if (stringValue != null) {
if (!IsEmptyOrWhiteSpaceCore(Buffer[i], writer))
{ {
if (!string.IsNullOrWhiteSpace(stringValue)) return false;
{
return false;
}
}
else
{
((IHtmlContent)entry).WriteTo(writer, HtmlEncoder.Default);
if (!writer.IsWhiteSpace)
{
return false;
}
} }
} }
} }
@ -82,73 +79,20 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
} }
/// <inheritdoc /> /// <inheritdoc />
public override bool IsEmpty public override TagHelperContent Append(string unencoded) => AppendCore(unencoded);
{
get
{
if (!IsModified)
{
return true;
}
using (var writer = new EmptyOrWhiteSpaceWriter())
{
foreach (var entry in _buffer)
{
if (entry == null)
{
continue;
}
var stringValue = entry as string;
if (stringValue != null)
{
if (!string.IsNullOrEmpty(stringValue))
{
return false;
}
}
else
{
((IHtmlContent)entry).WriteTo(writer, HtmlEncoder.Default);
if (!writer.IsEmpty)
{
return false;
}
}
}
}
return true;
}
}
/// <inheritdoc /> /// <inheritdoc />
public override TagHelperContent Append(string unencoded) public override TagHelperContent AppendHtml(IHtmlContent htmlContent) => AppendCore(htmlContent);
{
Buffer.Add(unencoded);
return this;
}
/// <inheritdoc /> /// <inheritdoc />
public override TagHelperContent AppendHtml(string encoded) public override TagHelperContent AppendHtml(string encoded)
{ {
if (encoded == null) if (encoded == null)
{ {
Buffer.Add(null); return AppendCore(null);
} }
else
{
Buffer.Add(new HtmlEncodedString(encoded));
}
return this;
}
/// <inheritdoc /> return AppendCore(new HtmlEncodedString(encoded));
public override TagHelperContent AppendHtml(IHtmlContent htmlContent)
{
Buffer.Add(htmlContent);
return this;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -159,32 +103,20 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
throw new ArgumentNullException(nameof(destination)); throw new ArgumentNullException(nameof(destination));
} }
if (!IsModified) if (!_hasContent)
{ {
return; return;
} }
for (var i = 0; i < Buffer.Count; i++) if (_isSingleContentSet)
{ {
var entry = Buffer[i]; CopyToCore(_singleContent, destination);
if (entry == null) }
else
{
for (var i = 0; i < (_buffer?.Count ?? 0); i++)
{ {
continue; CopyToCore(Buffer[i], destination);
}
string entryAsString;
IHtmlContentContainer entryAsContainer;
if ((entryAsString = entry as string) != null)
{
destination.Append(entryAsString);
}
else if ((entryAsContainer = entry as IHtmlContentContainer) != null)
{
entryAsContainer.CopyTo(destination);
}
else
{
destination.AppendHtml((IHtmlContent)entry);
} }
} }
} }
@ -197,62 +129,50 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
throw new ArgumentNullException(nameof(destination)); throw new ArgumentNullException(nameof(destination));
} }
if (!IsModified) if (!_hasContent)
{ {
return; return;
} }
for (var i = 0; i < Buffer.Count; i++) if (_isSingleContentSet)
{ {
var entry = Buffer[i]; MoveToCore(_singleContent, destination);
if (entry == null) }
else
{
for (var i = 0; i < (_buffer?.Count ?? 0); i++)
{ {
continue; MoveToCore(Buffer[i], destination);
}
string entryAsString;
IHtmlContentContainer entryAsContainer;
if ((entryAsString = entry as string) != null)
{
destination.Append(entryAsString);
}
else if ((entryAsContainer = entry as IHtmlContentContainer) != null)
{
entryAsContainer.MoveTo(destination);
}
else
{
destination.AppendHtml((IHtmlContent)entry);
} }
} }
Buffer.Clear(); Clear();
} }
/// <inheritdoc /> /// <inheritdoc />
public override TagHelperContent Clear() public override TagHelperContent Clear()
{ {
Buffer.Clear(); _hasContent = false;
_isModified = true;
_isSingleContentSet = false;
_buffer?.Clear();
return this; return this;
} }
/// <inheritdoc /> /// <inheritdoc />
public override void Reinitialize() public override void Reinitialize()
{ {
_buffer?.Clear(); Clear();
_isModified = false; _isModified = false;
} }
/// <inheritdoc /> /// <inheritdoc />
public override string GetContent() public override string GetContent() => GetContent(HtmlEncoder.Default);
{
return GetContent(HtmlEncoder.Default);
}
/// <inheritdoc /> /// <inheritdoc />
public override string GetContent(HtmlEncoder encoder) public override string GetContent(HtmlEncoder encoder)
{ {
if (_buffer == null) if (!_hasContent)
{ {
return string.Empty; return string.Empty;
} }
@ -277,28 +197,130 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
throw new ArgumentNullException(nameof(encoder)); throw new ArgumentNullException(nameof(encoder));
} }
if (!IsModified) if (!_hasContent)
{ {
return; return;
} }
foreach (var entry in _buffer) if (_isSingleContentSet)
{ {
if (entry == null) WriteToCore(_singleContent, writer, encoder);
{ return;
continue; }
}
var stringValue = entry as string; for (var i = 0; i < (_buffer?.Count ?? 0); i++)
if (stringValue != null) {
WriteToCore(Buffer[i], writer, encoder);
}
}
private void WriteToCore(object entry, TextWriter writer, HtmlEncoder encoder)
{
if (entry == null)
{
return;
}
var stringValue = entry as string;
if (stringValue != null)
{
encoder.Encode(writer, stringValue);
}
else
{
((IHtmlContent)entry).WriteTo(writer, encoder);
}
}
private void CopyToCore(object entry, IHtmlContentBuilder destination)
{
if (entry == null)
{
return;
}
string entryAsString;
IHtmlContentContainer entryAsContainer;
if ((entryAsString = entry as string) != null)
{
destination.Append(entryAsString);
}
else if ((entryAsContainer = entry as IHtmlContentContainer) != null)
{
entryAsContainer.CopyTo(destination);
}
else
{
destination.AppendHtml((IHtmlContent)entry);
}
}
private void MoveToCore(object entry, IHtmlContentBuilder destination)
{
if (entry == null)
{
return;
}
string entryAsString;
IHtmlContentContainer entryAsContainer;
if ((entryAsString = entry as string) != null)
{
destination.Append(entryAsString);
}
else if ((entryAsContainer = entry as IHtmlContentContainer) != null)
{
entryAsContainer.MoveTo(destination);
}
else
{
destination.AppendHtml((IHtmlContent)entry);
}
}
private bool IsEmptyOrWhiteSpaceCore(object entry, EmptyOrWhiteSpaceWriter writer)
{
if (entry == null)
{
return false;
}
var stringValue = entry as string;
if (stringValue != null)
{
if (!string.IsNullOrWhiteSpace(stringValue))
{ {
encoder.Encode(writer, stringValue); return false;
}
else
{
((IHtmlContent)entry).WriteTo(writer, encoder);
} }
} }
else
{
((IHtmlContent)entry).WriteTo(writer, HtmlEncoder.Default);
if (!writer.IsEmptyOrWhiteSpace)
{
return false;
}
}
return true;
}
private TagHelperContent AppendCore(object entry)
{
if (!_hasContent)
{
_isSingleContentSet = true;
_singleContent = entry;
}
else
{
Buffer.Add(entry);
}
_isModified = true;
_hasContent = true;
return this;
} }
private string DebuggerToString() private string DebuggerToString()
@ -317,9 +339,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
} }
} }
public bool IsEmpty { get; private set; } = true; public bool IsEmptyOrWhiteSpace { get; private set; } = true;
public bool IsWhiteSpace { get; private set; } = true;
#if NETSTANDARD1_5 #if NETSTANDARD1_5
// This is an abstract method in DNXCore // This is an abstract method in DNXCore
@ -331,14 +351,9 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
public override void Write(string value) public override void Write(string value)
{ {
if (IsEmpty && !string.IsNullOrEmpty(value)) if (IsEmptyOrWhiteSpace && !string.IsNullOrWhiteSpace(value))
{ {
IsEmpty = false; IsEmptyOrWhiteSpace = false;
}
if (IsWhiteSpace && !string.IsNullOrWhiteSpace(value))
{
IsWhiteSpace = false;
} }
} }
} }

View File

@ -19,14 +19,9 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
public abstract bool IsModified { get; } public abstract bool IsModified { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether the content is empty. /// Gets a value indicating whether the content is empty or whitespace.
/// </summary> /// </summary>
public abstract bool IsEmpty { get; } public abstract bool IsEmptyOrWhiteSpace { get; }
/// <summary>
/// Gets a value indicating whether the content is whitespace.
/// </summary>
public abstract bool IsWhiteSpace { get; }
/// <summary> /// <summary>
/// Sets the content. /// Sets the content.

View File

@ -164,7 +164,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
source.MoveTo(destination); source.MoveTo(destination);
// Assert // Assert
Assert.True(source.IsEmpty); Assert.Equal(string.Empty, source.GetContent());
Assert.Equal(3, items.Count); Assert.Equal(3, items.Count);
Assert.Equal("some-content", Assert.IsType<string>(items[0])); Assert.Equal("some-content", Assert.IsType<string>(items[0]));
@ -191,8 +191,8 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
source.MoveTo(destination); source.MoveTo(destination);
// Assert // Assert
Assert.True(source.IsEmpty); Assert.Equal(string.Empty, source.GetContent());
Assert.True(nested.IsEmpty); Assert.Equal(string.Empty, nested.GetContent());
Assert.Equal(3, items.Count); Assert.Equal(3, items.Count);
Assert.Equal("some-content", Assert.IsType<string>(items[0])); Assert.Equal("some-content", Assert.IsType<string>(items[0]));
@ -362,7 +362,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
[InlineData("\n")] [InlineData("\n")]
[InlineData("\t")] [InlineData("\t")]
[InlineData("\r")] [InlineData("\r")]
public void CanIdentifyWhiteSpace(string data) public void CanIdentifyEmptyOrWhiteSpace(string data)
{ {
// Arrange // Arrange
var tagHelperContent = new DefaultTagHelperContent(); var tagHelperContent = new DefaultTagHelperContent();
@ -372,7 +372,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
tagHelperContent.Append(data); tagHelperContent.Append(data);
// Assert // Assert
Assert.True(tagHelperContent.IsWhiteSpace); Assert.True(tagHelperContent.IsEmptyOrWhiteSpace);
} }
[Fact] [Fact]
@ -386,21 +386,22 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
tagHelperContent.Append("Hello"); tagHelperContent.Append("Hello");
// Assert // Assert
Assert.False(tagHelperContent.IsWhiteSpace); Assert.True(tagHelperContent.GetContent().Length > 0);
Assert.False(tagHelperContent.IsEmptyOrWhiteSpace);
} }
[Fact] [Fact]
public void IsEmpty_InitiallyTrue() public void IsEmptyOrWhiteSpace_InitiallyTrue()
{ {
// Arrange // Arrange
var tagHelperContent = new DefaultTagHelperContent(); var tagHelperContent = new DefaultTagHelperContent();
// Act & Assert // Act & Assert
Assert.True(tagHelperContent.IsEmpty); Assert.True(tagHelperContent.IsEmptyOrWhiteSpace);
} }
[Fact] [Fact]
public void IsEmpty_TrueAfterSetEmptyContent() public void IsEmptyOrWhiteSpace_TrueAfterSetEmptyContent()
{ {
// Arrange // Arrange
var tagHelperContent = new DefaultTagHelperContent(); var tagHelperContent = new DefaultTagHelperContent();
@ -409,11 +410,11 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
tagHelperContent.SetContent(string.Empty); tagHelperContent.SetContent(string.Empty);
// Assert // Assert
Assert.True(tagHelperContent.IsEmpty); Assert.True(tagHelperContent.IsEmptyOrWhiteSpace);
} }
[Fact] [Fact]
public void IsEmpty_TrueAfterAppendEmptyContent() public void IsEmptyOrWhiteSpace_TrueAfterAppendEmptyContent()
{ {
// Arrange // Arrange
var tagHelperContent = new DefaultTagHelperContent(); var tagHelperContent = new DefaultTagHelperContent();
@ -423,11 +424,11 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
tagHelperContent.Append(string.Empty); tagHelperContent.Append(string.Empty);
// Assert // Assert
Assert.True(tagHelperContent.IsEmpty); Assert.True(tagHelperContent.IsEmptyOrWhiteSpace);
} }
[Fact] [Fact]
public void IsEmpty_TrueAfterAppendEmptyTagHelperContent() public void IsEmptyOrWhiteSpace_TrueAfterAppendEmptyTagHelperContent()
{ {
// Arrange // Arrange
var tagHelperContent = new DefaultTagHelperContent(); var tagHelperContent = new DefaultTagHelperContent();
@ -438,11 +439,11 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
tagHelperContent.Append(string.Empty); tagHelperContent.Append(string.Empty);
// Assert // Assert
Assert.True(tagHelperContent.IsEmpty); Assert.True(tagHelperContent.IsEmptyOrWhiteSpace);
} }
[Fact] [Fact]
public void IsEmpty_TrueAfterClear() public void IsEmptyOrWhiteSpace_TrueAfterClear()
{ {
// Arrange // Arrange
var tagHelperContent = new DefaultTagHelperContent(); var tagHelperContent = new DefaultTagHelperContent();
@ -451,11 +452,12 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
tagHelperContent.Clear(); tagHelperContent.Clear();
// Assert // Assert
Assert.True(tagHelperContent.IsEmpty); Assert.Equal(string.Empty, tagHelperContent.GetContent());
Assert.True(tagHelperContent.IsEmptyOrWhiteSpace);
} }
[Fact] [Fact]
public void IsEmpty_FalseAfterSetContent() public void IsEmptyOrWhiteSpace_FalseAfterSetContent()
{ {
// Arrange // Arrange
var tagHelperContent = new DefaultTagHelperContent(); var tagHelperContent = new DefaultTagHelperContent();
@ -464,11 +466,11 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
tagHelperContent.SetContent("Hello"); tagHelperContent.SetContent("Hello");
// Assert // Assert
Assert.False(tagHelperContent.IsEmpty); Assert.False(tagHelperContent.IsEmptyOrWhiteSpace);
} }
[Fact] [Fact]
public void IsEmpty_FalseAfterAppend() public void IsEmptyOrWhiteSpace_FalseAfterAppend()
{ {
// Arrange // Arrange
var tagHelperContent = new DefaultTagHelperContent(); var tagHelperContent = new DefaultTagHelperContent();
@ -477,11 +479,11 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
tagHelperContent.Append("Hello"); tagHelperContent.Append("Hello");
// Assert // Assert
Assert.False(tagHelperContent.IsEmpty); Assert.False(tagHelperContent.IsEmptyOrWhiteSpace);
} }
[Fact] [Fact]
public void IsEmpty_FalseAfterAppendTagHelper() public void IsEmptyOrWhiteSpace_FalseAfterAppendTagHelper()
{ {
// Arrange // Arrange
var tagHelperContent = new DefaultTagHelperContent(); var tagHelperContent = new DefaultTagHelperContent();
@ -492,7 +494,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
tagHelperContent.AppendHtml(copiedTagHelperContent); tagHelperContent.AppendHtml(copiedTagHelperContent);
// Assert // Assert
Assert.False(tagHelperContent.IsEmpty); Assert.False(tagHelperContent.IsEmptyOrWhiteSpace);
} }
[Fact] [Fact]
@ -506,7 +508,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
tagHelperContent.Clear(); tagHelperContent.Clear();
// Assert // Assert
Assert.True(tagHelperContent.IsEmpty); Assert.True(tagHelperContent.IsEmptyOrWhiteSpace);
} }
[Fact] [Fact]
@ -632,4 +634,4 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
Assert.Equal("Hi", writer.ToString()); Assert.Equal("Hi", writer.ToString());
} }
} }
} }

View File

@ -1079,11 +1079,11 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
// Assert // Assert
buffer.WriteTo(writer, testEncoder); buffer.WriteTo(writer, testEncoder);
Assert.True(output.PreElement.IsEmpty); Assert.Equal(string.Empty, output.PreElement.GetContent());
Assert.True(output.PreContent.IsEmpty); Assert.Equal(string.Empty, output.PreContent.GetContent());
Assert.True(output.Content.IsEmpty); Assert.Equal(string.Empty, output.Content.GetContent());
Assert.True(output.PostContent.IsEmpty); Assert.Equal(string.Empty, output.PostContent.GetContent());
Assert.True(output.PostElement.IsEmpty); Assert.Equal(string.Empty, output.PostElement.GetContent());
Assert.Empty(output.Attributes); Assert.Empty(output.Attributes);
Assert.Equal(expected, writer.ToString(), StringComparer.Ordinal); Assert.Equal(expected, writer.ToString(), StringComparer.Ordinal);