Lazily initialize `TagHelperAttributeList`s.

- Not all `TagHelper`s have unbound HTML attributes or any attributes at all. A great example of this is MVC's input `TagHelper` which usually takes the format of `<input asp-for="..." />`. By lazily initializing we don't build extra attribute lists where not needed.
- Moved `TagHelperContext` and `TagHelperOutput` creation to CreateX methods on `TagHelperExecutionContext`.

#604
This commit is contained in:
N. Taylor Mullen 2016-01-29 15:48:14 -08:00
parent 84ac19571c
commit b7b3273fa4
9 changed files with 112 additions and 153 deletions

View File

@ -14,12 +14,17 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
/// </summary>
public class TagHelperExecutionContext
{
private readonly string _tagName;
private readonly string _uniqueId;
private readonly TagMode _tagMode;
private readonly List<ITagHelper> _tagHelpers;
private readonly Func<Task> _executeChildContentAsync;
private readonly Action<HtmlEncoder> _startTagHelperWritingScope;
private readonly Func<TagHelperContent> _endTagHelperWritingScope;
private TagHelperContent _childContent;
private Dictionary<HtmlEncoder, TagHelperContent> _perEncoderChildContent;
private TagHelperAttributeList _htmlAttributes;
private TagHelperAttributeList _allAttributes;
/// <summary>
/// Internal for testing purposes only.
@ -93,19 +98,12 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
_startTagHelperWritingScope = startTagHelperWritingScope;
_endTagHelperWritingScope = endTagHelperWritingScope;
TagMode = tagMode;
HtmlAttributes = new TagHelperAttributeList();
AllAttributes = new TagHelperAttributeList();
TagName = tagName;
_tagMode = tagMode;
_tagName = tagName;
Items = items;
UniqueId = uniqueId;
_uniqueId = uniqueId;
}
/// <summary>
/// Gets the HTML syntax of the element in the Razor source.
/// </summary>
public TagMode TagMode { get; }
/// <summary>
/// Indicates if <see cref="GetChildContentAsync"/> has been called.
/// </summary>
@ -122,21 +120,6 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
/// </summary>
public IDictionary<object, object> Items { get; }
/// <summary>
/// HTML attributes.
/// </summary>
public TagHelperAttributeList HtmlAttributes { get; }
/// <summary>
/// <see cref="ITagHelper"/> bound attributes and HTML attributes.
/// </summary>
public TagHelperAttributeList AllAttributes { get; }
/// <summary>
/// An identifier unique to the HTML element this context is for.
/// </summary>
public string UniqueId { get; }
/// <summary>
/// <see cref="ITagHelper"/>s that should be run.
/// </summary>
@ -148,16 +131,19 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
}
}
/// <summary>
/// The HTML tag name in the Razor source.
/// </summary>
public string TagName { get; }
/// <summary>
/// The <see cref="ITagHelper"/>s' output.
/// </summary>
public TagHelperOutput Output { get; set; }
public TagHelperContext CreateTagHelperContext() =>
new TagHelperContext(_allAttributes, Items, _uniqueId);
public TagHelperOutput CreateTagHelperOutput() =>
new TagHelperOutput(_tagName, _htmlAttributes, GetChildContentAsync)
{
TagMode = _tagMode
};
/// <summary>
/// Tracks the given <paramref name="tagHelper"/>.
/// </summary>
@ -183,9 +169,12 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
throw new ArgumentNullException(nameof(name));
}
EnsureHtmlAttributes();
EnsureAllAttributes();
var attribute = new TagHelperAttribute(name);
HtmlAttributes.Add(attribute);
AllAttributes.Add(attribute);
_htmlAttributes.Add(attribute);
_allAttributes.Add(attribute);
}
/// <summary>
@ -200,8 +189,12 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
throw new ArgumentNullException(nameof(name));
}
HtmlAttributes.Add(name, value);
AllAttributes.Add(name, value);
EnsureHtmlAttributes();
EnsureAllAttributes();
var attribute = new TagHelperAttribute(name, value);
_htmlAttributes.Add(attribute);
_allAttributes.Add(attribute);
}
/// <summary>
@ -216,24 +209,13 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
throw new ArgumentNullException(nameof(name));
}
AllAttributes.Add(name, value);
EnsureAllAttributes();
_allAttributes.Add(name, value);
}
/// <summary>
/// Executes children asynchronously with the given <paramref name="encoder"/> in scope and returns their
/// rendered content.
/// </summary>
/// <param name="useCachedResult">
/// If <c>true</c>, multiple calls with the same <see cref="HtmlEncoder"/> will not cause children to
/// re-execute; returns cached content.
/// </param>
/// <param name="encoder">
/// The <see cref="HtmlEncoder"/> to use when the page handles
/// non-<see cref="Microsoft.AspNetCore.Html.IHtmlContent"/> C# expressions. If <c>null</c>, executes children with
/// the page's current <see cref="HtmlEncoder"/>.
/// </param>
/// <returns>A <see cref="Task"/> that on completion returns the rendered child content.</returns>
public async Task<TagHelperContent> GetChildContentAsync(bool useCachedResult, HtmlEncoder encoder)
// Internal for testing.
internal async Task<TagHelperContent> GetChildContentAsync(bool useCachedResult, HtmlEncoder encoder)
{
// Get cached content for this encoder.
TagHelperContent childContent;
@ -272,5 +254,21 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
return new DefaultTagHelperContent().SetContent(childContent);
}
private void EnsureHtmlAttributes()
{
if (_htmlAttributes == null)
{
_htmlAttributes = new TagHelperAttributeList();
}
}
private void EnsureAllAttributes()
{
if (_allAttributes == null)
{
_allAttributes = new TagHelperAttributeList();
}
}
}
}

View File

@ -27,10 +27,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
throw new ArgumentNullException(nameof(executionContext));
}
var tagHelperContext = new TagHelperContext(
executionContext.AllAttributes,
executionContext.Items,
executionContext.UniqueId);
var tagHelperContext = executionContext.CreateTagHelperContext();
OrderTagHelpers(executionContext.TagHelpers);
@ -39,13 +36,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
executionContext.TagHelpers[i].Init(tagHelperContext);
}
var tagHelperOutput = new TagHelperOutput(
executionContext.TagName,
executionContext.HtmlAttributes,
executionContext.GetChildContentAsync)
{
TagMode = executionContext.TagMode,
};
var tagHelperOutput = executionContext.CreateTagHelperOutput();
for (var i = 0; i < executionContext.TagHelpers.Count; i++)
{

View File

@ -31,8 +31,8 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
/// <paramref name="attributes"/>.
/// </summary>
/// <param name="attributes">The collection to wrap.</param>
public ReadOnlyTagHelperAttributeList(IEnumerable<TagHelperAttribute> attributes)
: base(new List<TagHelperAttribute>(attributes))
public ReadOnlyTagHelperAttributeList(IList<TagHelperAttribute> attributes)
: base(attributes)
{
}

View File

@ -25,6 +25,20 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
/// </summary>
/// <param name="attributes">The collection to wrap.</param>
public TagHelperAttributeList(IEnumerable<TagHelperAttribute> attributes)
: base (new List<TagHelperAttribute>(attributes))
{
if (attributes == null)
{
throw new ArgumentNullException(nameof(attributes));
}
}
/// <summary>
/// Instantiates a new instance of <see cref="TagHelperAttributeList"/> with the specified
/// <paramref name="attributes"/>.
/// </summary>
/// <param name="attributes">The collection to wrap.</param>
public TagHelperAttributeList(List<TagHelperAttribute> attributes)
: base(attributes)
{
if (attributes == null)

View File

@ -11,8 +11,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
/// </summary>
public class TagHelperContext
{
private ReadOnlyTagHelperAttributeList _allAttributes;
private IEnumerable<TagHelperAttribute> _allAttributesData;
private static ReadOnlyTagHelperAttributeList EmptyAttributes = new TagHelperAttributeList();
/// <summary>
/// Instantiates a new <see cref="TagHelperContext"/>.
@ -22,15 +21,10 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
/// <param name="uniqueId">The unique identifier for the source element this <see cref="TagHelperContext" />
/// applies to.</param>
public TagHelperContext(
IEnumerable<TagHelperAttribute> allAttributes,
ReadOnlyTagHelperAttributeList allAttributes,
IDictionary<object, object> items,
string uniqueId)
{
if (allAttributes == null)
{
throw new ArgumentNullException(nameof(allAttributes));
}
if (items == null)
{
throw new ArgumentNullException(nameof(items));
@ -41,7 +35,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
throw new ArgumentNullException(nameof(uniqueId));
}
_allAttributesData = allAttributes;
AllAttributes = allAttributes ?? EmptyAttributes;
Items = items;
UniqueId = uniqueId;
}
@ -49,18 +43,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
/// <summary>
/// Every attribute associated with the current HTML element.
/// </summary>
public ReadOnlyTagHelperAttributeList AllAttributes
{
get
{
if (_allAttributes == null)
{
_allAttributes = new TagHelperAttributeList(_allAttributesData);
}
return _allAttributes;
}
}
public ReadOnlyTagHelperAttributeList AllAttributes { get; }
/// <summary>
/// Gets the collection of items used to communicate with other <see cref="ITagHelper"/>s.

View File

@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
public class TagHelperOutput : IHtmlContent
{
private readonly Func<bool, HtmlEncoder, Task<TagHelperContent>> _getChildContentAsync;
private TagHelperAttributeList _attributes;
private TagHelperContent _preElement;
private TagHelperContent _preContent;
private TagHelperContent _content;
@ -26,7 +27,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
internal TagHelperOutput(string tagName)
: this(
tagName,
new TagHelperAttributeList(),
null,
(useCachedResult, encoder) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()))
{
}
@ -45,19 +46,14 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
TagHelperAttributeList attributes,
Func<bool, HtmlEncoder, Task<TagHelperContent>> getChildContentAsync)
{
if (attributes == null)
{
throw new ArgumentNullException(nameof(attributes));
}
if (getChildContentAsync == null)
{
throw new ArgumentNullException(nameof(getChildContentAsync));
}
TagName = tagName;
Attributes = attributes;
_getChildContentAsync = getChildContentAsync;
_attributes = attributes;
}
/// <summary>
@ -187,7 +183,18 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
/// a <c>Microsoft.AspNetCore.Mvc.Rendering.HtmlString</c> instance. MVC converts most other types to a
/// <see cref="string"/>, then HTML encodes the result.
/// </remarks>
public TagHelperAttributeList Attributes { get; }
public TagHelperAttributeList Attributes
{
get
{
if (_attributes == null)
{
_attributes = new TagHelperAttributeList();
}
return _attributes;
}
}
/// <summary>
/// Changes <see cref="TagHelperOutput"/> to generate nothing.
@ -287,9 +294,9 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
writer.Write(TagName);
// Perf: Avoid allocating enumerator
for (var i = 0; i < Attributes.Count; i++)
for (var i = 0; i < (_attributes?.Count ?? 0); i++)
{
var attribute = Attributes[i];
var attribute = _attributes[i];
writer.Write(" ");
writer.Write(attribute.Name);

View File

@ -18,13 +18,16 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
[InlineData(TagMode.SelfClosing)]
[InlineData(TagMode.StartTagAndEndTag)]
[InlineData(TagMode.StartTagOnly)]
public void TagMode_ReturnsExpectedValue(TagMode tagMode)
public void ExecutionContext_CreateTagHelperOutput_ReturnsExpectedTagMode(TagMode tagMode)
{
// Arrange & Act
// Arrange
var executionContext = new TagHelperExecutionContext("p", tagMode);
// Act
var output = executionContext.CreateTagHelperOutput();
// Assert
Assert.Equal(tagMode, executionContext.TagMode);
Assert.Equal(tagMode, output.TagMode);
}
[Fact]
@ -270,51 +273,6 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
Assert.NotSame(content1, content2);
}
public static TheoryData<string, string> DictionaryCaseTestingData
{
get
{
return new TheoryData<string, string>
{
{ "class", "CLaSS" },
{ "Class", "class" },
{ "Class", "claSS" }
};
}
}
[Theory]
[MemberData(nameof(DictionaryCaseTestingData))]
public void HtmlAttributes_IgnoresCase(string originalName, string updatedName)
{
// Arrange
var executionContext = new TagHelperExecutionContext("p", TagMode.StartTagAndEndTag);
executionContext.HtmlAttributes.SetAttribute(originalName, "hello");
// Act
executionContext.HtmlAttributes.SetAttribute(updatedName, "something else");
// Assert
var attribute = Assert.Single(executionContext.HtmlAttributes);
Assert.Equal(new TagHelperAttribute(originalName, "something else"), attribute);
}
[Theory]
[MemberData(nameof(DictionaryCaseTestingData))]
public void AllAttributes_IgnoresCase(string originalName, string updatedName)
{
// Arrange
var executionContext = new TagHelperExecutionContext("p", tagMode: TagMode.StartTagAndEndTag);
executionContext.AllAttributes.SetAttribute(originalName, value: false);
// Act
executionContext.AllAttributes.SetAttribute(updatedName, true);
// Assert
var attribute = Assert.Single(executionContext.AllAttributes);
Assert.Equal(new TagHelperAttribute(originalName, true), attribute);
}
[Fact]
public void AddHtmlAttribute_MaintainsHtmlAttributes()
{
@ -329,11 +287,12 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
// Act
executionContext.AddHtmlAttribute("class", "btn");
executionContext.AddHtmlAttribute("foo", "bar");
var output = executionContext.CreateTagHelperOutput();
// Assert
Assert.Equal(
expectedAttributes,
executionContext.HtmlAttributes,
output.Attributes,
CaseSensitiveTagHelperAttributeComparer.Default);
}
@ -351,11 +310,12 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
// Act
executionContext.AddMinimizedHtmlAttribute("checked");
executionContext.AddMinimizedHtmlAttribute("visible");
var output = executionContext.CreateTagHelperOutput();
// Assert
Assert.Equal(
expectedAttributes,
executionContext.HtmlAttributes,
output.Attributes,
CaseSensitiveTagHelperAttributeComparer.Default);
}
@ -377,11 +337,12 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
executionContext.AddHtmlAttribute("foo", "bar");
executionContext.AddMinimizedHtmlAttribute("checked");
executionContext.AddMinimizedHtmlAttribute("visible");
var output = executionContext.CreateTagHelperOutput();
// Assert
Assert.Equal(
expectedAttributes,
executionContext.HtmlAttributes,
output.Attributes,
CaseSensitiveTagHelperAttributeComparer.Default);
}
@ -401,11 +362,12 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
executionContext.AddHtmlAttribute("class", "btn");
executionContext.AddTagHelperAttribute("something", true);
executionContext.AddHtmlAttribute("foo", "bar");
var context = executionContext.CreateTagHelperContext();
// Assert
Assert.Equal(
expectedAttributes,
executionContext.AllAttributes,
context.AllAttributes,
CaseSensitiveTagHelperAttributeComparer.Default);
}

View File

@ -125,16 +125,17 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
}
[Fact]
public void Begin_CreatesContextWithAppropriateTagName()
public void Begin_CreatesContexts_TagHelperOutput_WithAppropriateTagName()
{
// Arrange
var scopeManager = new TagHelperScopeManager();
// Act
var executionContext = BeginDefaultScope(scopeManager, tagName: "p");
var output = executionContext.CreateTagHelperOutput();
// Assert
Assert.Equal("p", executionContext.TagName);
Assert.Equal("p", output.TagName);
}
[Fact]
@ -146,25 +147,27 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
// Act
var executionContext = BeginDefaultScope(scopeManager, tagName: "p");
executionContext = BeginDefaultScope(scopeManager, tagName: "div");
var output = executionContext.CreateTagHelperOutput();
// Assert
Assert.Equal("div", executionContext.TagName);
Assert.Equal("div", output.TagName);
}
[Theory]
[InlineData(TagMode.SelfClosing)]
[InlineData(TagMode.StartTagAndEndTag)]
[InlineData(TagMode.StartTagOnly)]
public void Begin_SetsExecutionContextTagMode(TagMode tagMode)
public void Begin_SetsExecutionContexts_TagHelperOutputTagMode(TagMode tagMode)
{
// Arrange
var scopeManager = new TagHelperScopeManager();
// Act
var executionContext = BeginDefaultScope(scopeManager, "p", tagMode);
var output = executionContext.CreateTagHelperOutput();
// Assert
Assert.Equal(tagMode, executionContext.TagMode);
Assert.Equal(tagMode, output.TagMode);
}
[Fact]
@ -177,9 +180,10 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
var executionContext = BeginDefaultScope(scopeManager, tagName: "p");
executionContext = BeginDefaultScope(scopeManager, tagName: "div");
executionContext = scopeManager.End();
var output = executionContext.CreateTagHelperOutput();
// Assert
Assert.Equal("p", executionContext.TagName);
Assert.Equal("p", output.TagName);
}
[Fact]

View File

@ -730,7 +730,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
private class TestableReadOnlyTagHelperAttributes : ReadOnlyTagHelperAttributeList
{
public TestableReadOnlyTagHelperAttributes(IEnumerable<TagHelperAttribute> attributes)
: base(attributes)
: base(new List<TagHelperAttribute>(attributes))
{
}