Add `[HtmlAttributeName(..., DictionaryAttributePrefix="prefix")]` part II

- relates to #89 because that changes `string` property checks and needs this refactor
- determine `string`-ness when creating `TagHelperAttributeDescriptor`s
 - add `TagHelperAttributeDescriptor.IsStringProperty` (set in constructor)
 - avoid repeated `string` comparisons and be more explicit
- change `TagHelperBlockRewriter` to centralize more of the `string`-ness determination
 - also add `TryParseResult` DTO, avoiding multiple `out` parameters
- refactor `CSharpTagHelperCodeRenderer` to allow reuse of core attribute value rendering
- test all of it
 - add `TagHelperDescriptorTest` to confirm serialization / deserialization

minor:
- fix `TagHelperBlockRewriter.TryParseBlock()` end quote removal when tag is malformed

nits:
- remove dangling mention of fixed bug #220
- make recently-added `TagHelperBlockRewriterTest` tests realistic
 - multiple `TagHelperDescriptor`s for same tag helper have identical `Attributes`
This commit is contained in:
Doug Bunting 2015-05-15 23:17:52 -07:00
parent 2fe78d70db
commit 94f2f904b3
9 changed files with 584 additions and 199 deletions

View File

@ -67,14 +67,14 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
RenderTagHelpersCreation(chunk, tagHelperDescriptors);
var attributeDescriptors = tagHelperDescriptors.SelectMany(descriptor => descriptor.Attributes);
var boundHTMLAttributes = attributeDescriptors.Select(descriptor => descriptor.Name);
// Determine what attributes exist in the element and divide them up.
var htmlAttributes = chunk.Attributes;
var unboundHTMLAttributes =
htmlAttributes.Where(htmlAttribute => !boundHTMLAttributes.Contains(htmlAttribute.Key,
StringComparer.OrdinalIgnoreCase));
var attributeDescriptors = tagHelperDescriptors.SelectMany(descriptor => descriptor.Attributes);
var unboundHtmlAttributes = htmlAttributes.Where(
attribute => !attributeDescriptors.Any(
descriptor => string.Equals(attribute.Key, descriptor.Name, StringComparison.OrdinalIgnoreCase)));
RenderUnboundHTMLAttributes(unboundHTMLAttributes);
RenderUnboundHTMLAttributes(unboundHtmlAttributes);
// No need to run anything in design time mode.
if (!_designTimeMode)
@ -180,22 +180,24 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
}
// Render all of the bound attribute values for the tag helper.
RenderBoundHTMLAttributes(chunk.Attributes,
tagHelperVariableName,
tagHelperDescriptor.Attributes,
htmlAttributeValues);
RenderBoundHTMLAttributes(
chunk.Attributes,
tagHelperVariableName,
tagHelperDescriptor.Attributes,
htmlAttributeValues);
}
}
private void RenderBoundHTMLAttributes(IList<KeyValuePair<string, Chunk>> chunkAttributes,
string tagHelperVariableName,
IEnumerable<TagHelperAttributeDescriptor> attributeDescriptors,
Dictionary<string, string> htmlAttributeValues)
private void RenderBoundHTMLAttributes(
IList<KeyValuePair<string, Chunk>> chunkAttributes,
string tagHelperVariableName,
IEnumerable<TagHelperAttributeDescriptor> attributeDescriptors,
Dictionary<string, string> htmlAttributeValues)
{
foreach (var attributeDescriptor in attributeDescriptors)
{
var matchingAttributes = chunkAttributes.Where(
attr => string.Equals(attr.Key, attributeDescriptor.Name, StringComparison.OrdinalIgnoreCase));
kvp => string.Equals(kvp.Key, attributeDescriptor.Name, StringComparison.OrdinalIgnoreCase));
if (matchingAttributes.Any())
{
@ -210,31 +212,15 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
continue;
}
var attributeValueRecorded = htmlAttributeValues.ContainsKey(attributeDescriptor.Name);
// Bufferable attributes are attributes that can have Razor code inside of them.
var bufferableAttribute = IsStringAttribute(attributeDescriptor);
// Plain text values are non Razor code (@DateTime.Now) values. If an attribute is bufferable it
// may be more than just a plain text value, it may also contain Razor code which is why we attempt
// to retrieve a plain text value here.
string textValue;
var isPlainTextValue = TryGetPlainTextValue(attributeValueChunk, out textValue);
// If we haven't recorded a value and we need to buffer an attribute value and the value is not
// plain text then we need to prepare the value prior to setting it below.
if (!attributeValueRecorded && bufferableAttribute && !isPlainTextValue)
{
BuildBufferedWritingScope(attributeValueChunk, htmlEncodeValues: false);
}
// We capture the tag helpers property value accessor so we can retrieve it later (if we need to).
var valueAccessor = string.Format(CultureInfo.InvariantCulture,
"{0}.{1}",
tagHelperVariableName,
attributeDescriptor.PropertyName);
var valueAccessor = string.Format(
CultureInfo.InvariantCulture,
"{0}.{1}",
tagHelperVariableName,
attributeDescriptor.PropertyName);
// If we haven't recorded this attribute value before then we need to record its value.
var attributeValueRecorded = htmlAttributeValues.ContainsKey(attributeDescriptor.Name);
if (!attributeValueRecorded)
{
// We only need to create attribute values once per HTML element (not once per tag helper).
@ -242,58 +228,16 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
// helpers that need the value.
htmlAttributeValues.Add(attributeDescriptor.Name, valueAccessor);
if (bufferableAttribute)
{
_writer.WriteStartAssignment(valueAccessor);
// Bufferable attributes are attributes that can have Razor code inside of them. Such
// attributes have string values and may be calculated using a temporary TextWriter or other
// buffer.
var bufferableAttribute = attributeDescriptor.IsStringProperty;
if (isPlainTextValue)
{
// If the attribute is bufferable but has a plain text value that means the value
// is a string which needs to be surrounded in quotes.
RenderQuotedAttributeValue(textValue, attributeDescriptor);
}
else
{
// The value contains more than plain text e.g.
// stringAttribute ="Time: @DateTime.Now"
RenderBufferedAttributeValue(attributeDescriptor);
}
_writer.WriteLine(";");
}
else
{
// Write out simple assignment for non-string property value. Try to keep the whole
// statement together and the #line pragma correct to make debugging possible.
using (var lineMapper = new CSharpLineMappingWriter(
_writer,
attributeValueChunk.Association.Start,
_context.SourceFile))
{
// Place the assignment LHS to align RHS with original attribute value's indentation.
// Unfortunately originalIndent is incorrect if original line contains tabs. Unable to
// use a CSharpPaddingBuilder because the Association has no Previous node; lost the
// original Span sequence when the parse tree was rewritten.
var originalIndent = attributeValueChunk.Start.CharacterIndex;
var generatedLength = valueAccessor.Length + " = ".Length;
var newIndent = originalIndent - generatedLength;
if (newIndent > 0)
{
_writer.Indent(newIndent);
}
_writer.WriteStartAssignment(valueAccessor);
lineMapper.MarkLineMappingStart();
// Write out bare expression for this attribute value. Property is not a string.
// So quoting or buffering are not helpful.
RenderRawAttributeValue(attributeValueChunk, attributeDescriptor, isPlainTextValue);
// End the assignment to the attribute.
lineMapper.MarkLineMappingEnd();
_writer.WriteLine(";");
}
}
RenderNewAttributeValueAssignment(
attributeDescriptor,
bufferableAttribute,
attributeValueChunk,
valueAccessor);
// Execution contexts are a runtime feature.
if (_designTimeMode)
@ -301,8 +245,8 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
continue;
}
var attributeName = firstAttribute.Key;
// We need to inform the context of the attribute value.
var attributeName = firstAttribute.Key;
_writer
.WriteStartInstanceMethodInvocation(
ExecutionContextVariableName,
@ -325,6 +269,79 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
}
}
// Render assignment of attribute value to the value accessor.
private void RenderNewAttributeValueAssignment(
TagHelperAttributeDescriptor attributeDescriptor,
bool bufferableAttribute,
Chunk attributeValueChunk,
string valueAccessor)
{
// Plain text values are non Razor code (@DateTime.Now) values. If an attribute is bufferable it
// may be more than just a plain text value, it may also contain Razor code which is why we attempt
// to retrieve a plain text value here.
string textValue;
var isPlainTextValue = TryGetPlainTextValue(attributeValueChunk, out textValue);
if (bufferableAttribute)
{
if (!isPlainTextValue)
{
// If we haven't recorded a value and we need to buffer an attribute value and the value is not
// plain text then we need to prepare the value prior to setting it below.
BuildBufferedWritingScope(attributeValueChunk, htmlEncodeValues: false);
}
_writer.WriteStartAssignment(valueAccessor);
if (isPlainTextValue)
{
// If the attribute is bufferable but has a plain text value that means the value
// is a string which needs to be surrounded in quotes.
RenderQuotedAttributeValue(textValue, attributeDescriptor);
}
else
{
// The value contains more than plain text e.g. stringAttribute ="Time: @DateTime.Now".
RenderBufferedAttributeValue(attributeDescriptor);
}
_writer.WriteLine(";");
}
else
{
// Write out simple assignment for non-string property value. Try to keep the whole
// statement together and the #line pragma correct to make debugging possible.
using (var lineMapper = new CSharpLineMappingWriter(
_writer,
attributeValueChunk.Association.Start,
_context.SourceFile))
{
// Place the assignment LHS to align RHS with original attribute value's indentation.
// Unfortunately originalIndent is incorrect if original line contains tabs. Unable to
// use a CSharpPaddingBuilder because the Association has no Previous node; lost the
// original Span sequence when the parse tree was rewritten.
var originalIndent = attributeValueChunk.Start.CharacterIndex;
var generatedLength = valueAccessor.Length + " = ".Length;
var newIndent = originalIndent - generatedLength;
if (newIndent > 0)
{
_writer.Indent(newIndent);
}
_writer.WriteStartAssignment(valueAccessor);
lineMapper.MarkLineMappingStart();
// Write out bare expression for this attribute value. Property is not a string.
// So quoting or buffering are not helpful.
RenderRawAttributeValue(attributeValueChunk, attributeDescriptor, isPlainTextValue);
// End the assignment to the attribute.
lineMapper.MarkLineMappingEnd();
_writer.WriteLine(";");
}
}
}
private void RenderUnboundHTMLAttributes(IEnumerable<KeyValuePair<string, Chunk>> unboundHTMLAttributes)
{
// Build out the unbound HTML attributes for the tag builder
@ -540,14 +557,6 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
}
}
private static bool IsStringAttribute(TagHelperAttributeDescriptor attributeDescriptor)
{
return string.Equals(
attributeDescriptor.TypeName,
typeof(string).FullName,
StringComparison.Ordinal);
}
private static bool TryGetPlainTextValue(Chunk chunk, out string plainText)
{
var chunkBlock = chunk as ChunkBlock;

View File

@ -38,15 +38,11 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
IEnumerable<TagHelperDescriptor> descriptors,
ErrorSink errorSink)
{
var attributes = new List<KeyValuePair<string, SyntaxTreeNode>>();
// Ignore all but one descriptor per type since this method uses the TagHelperDescriptors only to get the
// contained TagHelperAttributeDescriptor's.
descriptors = descriptors.Distinct(TypeBasedTagHelperDescriptorComparer.Default);
// Build a dictionary so we can easily lookup expected attribute value lookups
IReadOnlyDictionary<string, string> attributeValueTypes =
descriptors.SelectMany(descriptor => descriptor.Attributes)
.Distinct(TagHelperAttributeDescriptorComparer.Default)
.ToDictionary(descriptor => descriptor.Name,
descriptor => descriptor.TypeName,
StringComparer.OrdinalIgnoreCase);
var attributes = new List<KeyValuePair<string, SyntaxTreeNode>>();
// We skip the first child "<tagname" and take everything up to the ending portion of the tag ">" or "/>".
// The -2 accounts for both the start and end tags. If the tag does not have a valid structure then there's
@ -56,40 +52,38 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
foreach (var child in attributeChildren)
{
KeyValuePair<string, SyntaxTreeNode> attribute;
bool succeeded = true;
TryParseResult result;
if (child.IsBlock)
{
succeeded = TryParseBlock(tagName, (Block)child, attributeValueTypes, errorSink, out attribute);
result = TryParseBlock(tagName, (Block)child, descriptors, errorSink);
}
else
{
succeeded = TryParseSpan((Span)child, attributeValueTypes, errorSink, out attribute);
result = TryParseSpan((Span)child, descriptors, errorSink);
}
// Only want to track the attribute if we succeeded in parsing its corresponding Block/Span.
if (succeeded)
if (result != null)
{
// Check if it's a bound attribute that is minimized or not of type string and null or whitespace.
string attributeValueType;
if (attributeValueTypes.TryGetValue(attribute.Key, out attributeValueType) &&
(attribute.Value == null ||
!IsStringAttribute(attributeValueType) &&
IsNullOrWhitespaceAttributeValue(attribute.Value)))
// Check if it's a bound attribute that is minimized or if it's a bound non-string attribute that
// is null or whitespace.
if ((result.IsBoundAttribute && result.AttributeValueNode == null) ||
(result.IsBoundNonStringAttribute &&
IsNullOrWhitespaceAttributeValue(result.AttributeValueNode)))
{
var errorLocation = GetAttributeNameStartLocation(child);
errorSink.OnError(
errorLocation,
RazorResources.FormatRewriterError_EmptyTagHelperBoundAttribute(
attribute.Key,
result.AttributeName,
tagName,
attributeValueType),
attribute.Key.Length);
GetPropertyType(result.AttributeName, descriptors)),
result.AttributeName.Length);
}
attributes.Add(new KeyValuePair<string, SyntaxTreeNode>(attribute.Key, attribute.Value));
attributes.Add(
new KeyValuePair<string, SyntaxTreeNode>(result.AttributeName, result.AttributeValueNode));
}
}
@ -106,11 +100,10 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
// This method handles cases when the attribute is a simple span attribute such as
// class="something moresomething". This does not handle complex attributes such as
// class="@myclass". Therefore the span.Content is equivalent to the entire attribute.
private static bool TryParseSpan(
private static TryParseResult TryParseSpan(
Span span,
IReadOnlyDictionary<string, string> attributeValueTypes,
ErrorSink errorSink,
out KeyValuePair<string, SyntaxTreeNode> attribute)
IEnumerable<TagHelperDescriptor> descriptors,
ErrorSink errorSink)
{
var afterEquals = false;
var builder = new SpanBuilder
@ -235,24 +228,29 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
span.Content.Length);
}
attribute = default(KeyValuePair<string, SyntaxTreeNode>);
return false;
return null;
}
bool isBoundNonStringAttribute;
var result = new TryParseResult
{
IsBoundAttribute = IsBoundAttribute(name, descriptors, out isBoundNonStringAttribute),
AttributeName = name,
};
result.IsBoundNonStringAttribute = isBoundNonStringAttribute;
// If we're not after an equal then we should treat the value as if it were a minimized attribute.
var attributeValueBuilder = afterEquals ? builder : null;
attribute = CreateMarkupAttribute(name, attributeValueBuilder, attributeValueTypes);
result.AttributeValueNode = CreateMarkupAttribute(name, attributeValueBuilder, isBoundNonStringAttribute);
return true;
return result;
}
private static bool TryParseBlock(
private static TryParseResult TryParseBlock(
string tagName,
Block block,
IReadOnlyDictionary<string, string> attributeValueTypes,
ErrorSink errorSink,
out KeyValuePair<string, SyntaxTreeNode> attribute)
IEnumerable<TagHelperDescriptor> descriptors,
ErrorSink errorSink)
{
// TODO: Accept more than just spans: https://github.com/aspnet/Razor/issues/96.
// The first child will only ever NOT be a Span if a user is doing something like:
@ -265,9 +263,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
errorSink.OnError(block.Children.First().Start,
RazorResources.FormatTagHelpers_CannotHaveCSharpInTagDeclaration(tagName));
attribute = default(KeyValuePair<string, SyntaxTreeNode>);
return false;
return null;
}
var builder = new BlockBuilder(block);
@ -276,7 +272,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
// i.e. <div class="plain text in attribute">
if (builder.Children.Count == 1)
{
return TryParseSpan(childSpan, attributeValueTypes, errorSink, out attribute);
return TryParseSpan(childSpan, descriptors, errorSink);
}
var textSymbol = childSpan.Symbols.FirstHtmlSymbolAs(HtmlSymbolType.Text);
@ -286,12 +282,17 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
{
errorSink.OnError(childSpan.Start, RazorResources.FormatTagHelpers_AttributesMustHaveAName(tagName));
attribute = default(KeyValuePair<string, SyntaxTreeNode>);
return false;
return null;
}
// TODO: Support no attribute values: https://github.com/aspnet/Razor/issues/220
// Have a name now. Able to determine correct isBoundNonStringAttribute value.
bool isBoundNonStringAttribute;
var result = new TryParseResult
{
IsBoundAttribute = IsBoundAttribute(name, descriptors, out isBoundNonStringAttribute),
AttributeName = name,
};
result.IsBoundNonStringAttribute = isBoundNonStringAttribute;
// Remove first child i.e. foo="
builder.Children.RemoveAt(0);
@ -301,10 +302,14 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
if (!endNode.IsBlock)
{
var endSpan = (Span)endNode;
var endSymbol = (HtmlSymbol)endSpan.Symbols.Last();
// In some malformed cases e.g. <p bar="false', the last Span (false' in the ex.) may contain more
// than a single HTML symbol. Do not ignore those other symbols.
var symbolCount = endSpan.Symbols.Count();
var endSymbol = symbolCount == 1 ? (HtmlSymbol)endSpan.Symbols.First() : null;
// Checking to see if it's a quoted attribute, if so we should remove end quote
if (IsQuote(endSymbol))
if (endSymbol != null && IsQuote(endSymbol))
{
builder.Children.RemoveAt(builder.Children.Count - 1);
}
@ -323,18 +328,17 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
if (child != null)
{
// After pulling apart the block we just have a value span.
var spanBuilder = new SpanBuilder(child);
attribute = CreateMarkupAttribute(name, spanBuilder, attributeValueTypes);
result.AttributeValueNode = CreateMarkupAttribute(name, spanBuilder, isBoundNonStringAttribute);
return true;
return result;
}
}
attribute = new KeyValuePair<string, SyntaxTreeNode>(name, block);
result.AttributeValueNode = block;
return true;
return result;
}
private static Block RebuildCodeGenerators(Block block)
@ -429,22 +433,20 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
return nodeStart + firstNonWhitespaceSymbol.Start;
}
private static KeyValuePair<string, SyntaxTreeNode> CreateMarkupAttribute(
private static SyntaxTreeNode CreateMarkupAttribute(
string name,
SpanBuilder builder,
IReadOnlyDictionary<string, string> attributeValueTypes)
bool isBoundNonStringAttribute)
{
string attributeTypeName;
Span value = null;
// Builder will be null in the case of minimized attributes
if (builder != null)
{
// If the attribute was requested by the tag helper and doesn't happen to be a string then we need to treat
// its value as code. Any non-string value can be any C# value so we need to ensure the SyntaxTreeNode
// reflects that.
if (attributeValueTypes.TryGetValue(name, out attributeTypeName) &&
!IsStringAttribute(attributeTypeName))
// If the attribute was requested by a tag helper but the corresponding property was not a string,
// then treat its value as code. A non-string value can be any C# value so we need to ensure the
// SyntaxTreeNode reflects that.
if (isBoundNonStringAttribute)
{
builder.Kind = SpanKind.Code;
}
@ -452,7 +454,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
value = builder.Build();
}
return new KeyValuePair<string, SyntaxTreeNode>(name, value);
return value;
}
private static bool IsNullOrWhitespaceAttributeValue(SyntaxTreeNode attributeValue)
@ -475,9 +477,35 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
}
}
private static bool IsStringAttribute(string attributeTypeName)
// Determines the full name of the Type of the property corresponding to an attribute with the given name.
private static string GetPropertyType(string name, IEnumerable<TagHelperDescriptor> descriptors)
{
return string.Equals(attributeTypeName, StringTypeName, StringComparison.OrdinalIgnoreCase);
var firstBoundAttribute = FindFirstBoundAttribute(name, descriptors);
return firstBoundAttribute?.TypeName;
}
// Determines whether an attribute with the given name is bound to a non-string tag helper property.
private static bool IsBoundAttribute(
string name,
IEnumerable<TagHelperDescriptor> descriptors,
out bool isBoundNonStringAttribute)
{
var firstBoundAttribute = FindFirstBoundAttribute(name, descriptors);
var isBoundAttribute = firstBoundAttribute != null;
isBoundNonStringAttribute = isBoundAttribute && !firstBoundAttribute.IsStringProperty;
return isBoundAttribute;
}
// Finds first TagHelperAttributeDescriptor matching given name.
private static TagHelperAttributeDescriptor FindFirstBoundAttribute(
string name,
IEnumerable<TagHelperDescriptor> descriptors)
{
return descriptors
.SelectMany(descriptor => descriptor.Attributes)
.FirstOrDefault(attribute => string.Equals(attribute.Name, name, StringComparison.OrdinalIgnoreCase));
}
private static bool IsQuote(HtmlSymbol htmlSymbol)
@ -485,5 +513,16 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
return htmlSymbol.Type == HtmlSymbolType.DoubleQuote ||
htmlSymbol.Type == HtmlSymbolType.SingleQuote;
}
private class TryParseResult
{
public string AttributeName { get; set; }
public SyntaxTreeNode AttributeValueNode { get; set; }
public bool IsBoundAttribute { get; set; }
public bool IsBoundNonStringAttribute { get; set; }
}
}
}

View File

@ -1,7 +1,9 @@
// 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.Reflection;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Razor.TagHelpers
{
@ -10,9 +12,13 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// </summary>
public class TagHelperAttributeDescriptor
{
// Internal for testing
internal TagHelperAttributeDescriptor(string name, PropertyInfo propertyInfo)
: this(name, propertyInfo.Name, propertyInfo.PropertyType.FullName)
// Internal for testing i.e. for easy TagHelperAttributeDescriptor creation when PropertyInfo is available.
internal TagHelperAttributeDescriptor([NotNull] string name, [NotNull] PropertyInfo propertyInfo)
: this(
name,
propertyInfo.Name,
propertyInfo.PropertyType.FullName,
isStringProperty: propertyInfo.PropertyType == typeof(string))
{
}
@ -20,35 +26,58 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// Instantiates a new instance of the <see cref="TagHelperAttributeDescriptor"/> class.
/// </summary>
/// <param name="name">The HTML attribute name.</param>
/// <param name="propertyName">The name of the CLR property name that corresponds to the HTML
/// attribute.</param>
/// <param name="propertyName">The name of the CLR property that corresponds to the HTML attribute.</param>
/// <param name="typeName">
/// The full name of the named (see <paramref name="propertyName"/>) property's
/// <see cref="System.Type"/>.
/// The full name of the named (see <paramref name="propertyName"/>) property's <see cref="System.Type"/>.
/// </param>
public TagHelperAttributeDescriptor(string name,
string propertyName,
string typeName)
public TagHelperAttributeDescriptor(
[NotNull] string name,
[NotNull] string propertyName,
[NotNull] string typeName)
: this(
name,
propertyName,
typeName,
isStringProperty: string.Equals(typeName, typeof(string).FullName, StringComparison.Ordinal))
{
}
// Internal for testing i.e. for confirming above constructor sets `IsStringProperty` as expected.
internal TagHelperAttributeDescriptor(
[NotNull] string name,
[NotNull] string propertyName,
[NotNull] string typeName,
bool isStringProperty)
{
Name = name;
PropertyName = propertyName;
TypeName = typeName;
IsStringProperty = isStringProperty;
}
/// <summary>
/// Gets an indication whether this property is of type <see cref="string"/>.
/// </summary>
/// <value>
/// If <c>true</c> the <see cref="TypeName"/> is for <see cref="string"/>. This causes the Razor parser
/// to allow empty values for attributes that have names matching <see cref="Name"/>. If <c>false</c>
/// empty values for such matching attributes lead to errors.
/// </value>
public bool IsStringProperty { get; }
/// <summary>
/// The HTML attribute name.
/// </summary>
public string Name { get; private set; }
public string Name { get; }
/// <summary>
/// The name of the CLR property name that corresponds to the HTML attribute name.
/// The name of the CLR property that corresponds to the HTML attribute.
/// </summary>
public string PropertyName { get; private set; }
public string PropertyName { get; }
/// <summary>
/// The full name of the named (see <see name="PropertyName"/>) property's
/// <see cref="System.Type"/>.
/// The full name of the named (see <see name="PropertyName"/>) property's <see cref="System.Type"/>.
/// </summary>
public string TypeName { get; private set; }
public string TypeName { get; }
}
}

View File

@ -24,13 +24,17 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
return true;
}
// Normal comparer doesn't care about case, in tests we do. Also double-check IsStringProperty though
// it is inferred from TypeName.
return base.Equals(descriptorX, descriptorY) &&
// Normal comparer doesn't care about case, in tests we do.
descriptorX.IsStringProperty == descriptorY.IsStringProperty &&
string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.Ordinal);
}
public override int GetHashCode(TagHelperAttributeDescriptor descriptor)
{
// Rarely if ever hash TagHelperAttributeDescriptor. If we do, ignore IsStringProperty since it should
// not vary for a given TypeName i.e. will not change the bucket.
return HashCodeCombiner.Start()
.Add(base.GetHashCode(descriptor))
.Add(descriptor.Name, StringComparer.Ordinal)

View File

@ -289,8 +289,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
typeof(InheritedOverriddenAttributeTagHelper).FullName,
AssemblyName,
new[] {
new TagHelperAttributeDescriptor("valid-attribute1",
validProperty1),
new TagHelperAttributeDescriptor("valid-attribute1", validProperty1),
new TagHelperAttributeDescriptor("Something-Else", validProperty2)
})
};
@ -363,14 +362,18 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{
// Arrange
var errorSink = new ErrorSink();
var intProperty = typeof(InheritedSingleAttributeTagHelper).GetProperty(
nameof(InheritedSingleAttributeTagHelper.IntAttribute));
// Also confirm isStringProperty is calculated correctly.
var expectedDescriptor = new TagHelperDescriptor(
"inherited-single-attribute",
typeof(InheritedSingleAttributeTagHelper).FullName,
AssemblyName,
new[] {
new TagHelperAttributeDescriptor("int-attribute", intProperty)
new TagHelperAttributeDescriptor(
"int-attribute",
nameof(InheritedSingleAttributeTagHelper.IntAttribute),
typeof(int).FullName,
isStringProperty: false)
});
// Act
@ -450,8 +453,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
typeof(NonPublicAccessorTagHelper).FullName,
AssemblyName,
new[] {
new TagHelperAttributeDescriptor(
"valid-attribute", validProperty)
new TagHelperAttributeDescriptor("valid-attribute", validProperty)
});
// Act
@ -471,15 +473,19 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{
// Arrange
var errorSink = new ErrorSink();
var boundProperty = typeof(NotBoundAttributeTagHelper).GetProperty(
nameof(NotBoundAttributeTagHelper.BoundProperty));
// Also confirm isStringProperty is calculated correctly.
var expectedDescriptor = new TagHelperDescriptor(
"not-bound-attribute",
typeof(NotBoundAttributeTagHelper).FullName,
AssemblyName,
new[]
{
new TagHelperAttributeDescriptor("bound-property", boundProperty)
new TagHelperAttributeDescriptor(
"bound-property",
nameof(NotBoundAttributeTagHelper.BoundProperty),
typeof(object).FullName,
isStringProperty: false)
});
// Act
@ -516,21 +522,30 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{
// Arrange
var errorSink = new ErrorSink();
var validProp = typeof(MultiTagTagHelper).GetProperty(nameof(MultiTagTagHelper.ValidAttribute));
// Also confirm isStringProperty is calculated correctly.
var expectedDescriptors = new[] {
new TagHelperDescriptor(
"div",
typeof(MultiTagTagHelper).FullName,
AssemblyName,
new[] {
new TagHelperAttributeDescriptor("valid-attribute", validProp)
new TagHelperAttributeDescriptor(
"valid-attribute",
nameof(MultiTagTagHelper.ValidAttribute),
typeof(string).FullName,
isStringProperty: true)
}),
new TagHelperDescriptor(
"p",
typeof(MultiTagTagHelper).FullName,
AssemblyName,
new[] {
new TagHelperAttributeDescriptor("valid-attribute", validProp)
new TagHelperAttributeDescriptor(
"valid-attribute",
nameof(MultiTagTagHelper.ValidAttribute),
typeof(string).FullName,
isStringProperty: true)
})
};
@ -1104,7 +1119,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
private class NotBoundAttributeTagHelper
{
public string BoundProperty { get; set; }
public object BoundProperty { get; set; }
[HtmlAttributeNotBound]
public string NotBoundProperty { get; set; }

View File

@ -14,10 +14,10 @@ namespace Microsoft.AspNet.Razor.Test.Generator
{
public class CSharpTagHelperRenderingTest : TagHelperTestBase
{
private static IEnumerable<TagHelperDescriptor> DefaultPAndInputTagHelperDescriptors
=> BuildPAndInputTagHelperDescriptors(prefix: string.Empty);
private static IEnumerable<TagHelperDescriptor> PrefixedPAndInputTagHelperDescriptors
=> BuildPAndInputTagHelperDescriptors("THS");
private static IEnumerable<TagHelperDescriptor> DefaultPAndInputTagHelperDescriptors { get; }
= BuildPAndInputTagHelperDescriptors(prefix: string.Empty);
private static IEnumerable<TagHelperDescriptor> PrefixedPAndInputTagHelperDescriptors { get; }
= BuildPAndInputTagHelperDescriptors(prefix: "THS");
private static IEnumerable<TagHelperDescriptor> MinimizedTagHelpers_Descriptors
{
@ -609,6 +609,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator
var pAgePropertyInfo = typeof(TestType).GetProperty("Age");
var inputTypePropertyInfo = typeof(TestType).GetProperty("Type");
var checkedPropertyInfo = typeof(TestType).GetProperty("Checked");
return new[]
{
new TagHelperDescriptor(

View File

@ -712,13 +712,19 @@ namespace Microsoft.AspNet.Razor.TagHelpers
{
new TagHelperDescriptor(
tagName: "input",
typeName: "InputTagHelper",
typeName: "InputTagHelper1",
assemblyName: "SomeAssembly",
attributes: new TagHelperAttributeDescriptor[0],
attributes: new[]
{
new TagHelperAttributeDescriptor(
"bound-required-string",
"BoundRequiredString",
typeof(string).FullName)
},
requiredAttributes: new[] { "unbound-required" }),
new TagHelperDescriptor(
tagName: "input",
typeName: "InputTagHelper",
typeName: "InputTagHelper1",
assemblyName: "SomeAssembly",
attributes: new[]
{
@ -730,7 +736,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
requiredAttributes: new[] { "bound-required-string" }),
new TagHelperDescriptor(
tagName: "input",
typeName: "InputTagHelper",
typeName: "InputTagHelper2",
assemblyName: "SomeAssembly",
attributes: new[]
{

View File

@ -0,0 +1,193 @@
// 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.Linq;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNet.Razor.TagHelpers
{
public class TagHelperDescriptorTest
{
[Fact]
public void TagHelperDescriptor_CanBeSerialized()
{
// Arrange
var descriptor = new TagHelperDescriptor(
prefix: "prefix:",
tagName: "tag name",
typeName: "type name",
assemblyName: "assembly name",
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: new[] { "required attribute one", "required attribute two" });
var expectedSerializedDescriptor =
$"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," +
$"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," +
$"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," +
$"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," +
$"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," +
$"\"{ nameof(TagHelperDescriptor.Attributes) }\":[]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":" +
"[\"required attribute one\",\"required attribute two\"]}";
// Act
var serializedDescriptor = JsonConvert.SerializeObject(descriptor);
// Assert
Assert.Equal(expectedSerializedDescriptor, serializedDescriptor, StringComparer.Ordinal);
}
[Fact]
public void TagHelperDescriptor_WithAttributes_CanBeSerialized()
{
// Arrange
var descriptor = new TagHelperDescriptor(
prefix: "prefix:",
tagName: "tag name",
typeName: "type name",
assemblyName: "assembly name",
attributes: new[]
{
new TagHelperAttributeDescriptor(
name: "attribute one",
propertyName: "property name",
typeName: "property type name"),
new TagHelperAttributeDescriptor(
name: "attribute two",
propertyName: "property name",
typeName: typeof(string).FullName),
},
requiredAttributes: Enumerable.Empty<string>());
var expectedSerializedDescriptor =
$"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," +
$"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," +
$"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," +
$"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," +
$"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," +
$"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"}}," +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"}}]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]}}";
// Act
var serializedDescriptor = JsonConvert.SerializeObject(descriptor);
// Assert
Assert.Equal(expectedSerializedDescriptor, serializedDescriptor, StringComparer.Ordinal);
}
[Fact]
public void TagHelperDescriptor_CanBeDeserialized()
{
// Arrange
var serializedDescriptor =
$"{{\"{nameof(TagHelperDescriptor.Prefix)}\":\"prefix:\"," +
$"\"{nameof(TagHelperDescriptor.TagName)}\":\"tag name\"," +
$"\"{nameof(TagHelperDescriptor.FullTagName)}\":\"prefix:tag name\"," +
$"\"{nameof(TagHelperDescriptor.TypeName)}\":\"type name\"," +
$"\"{nameof(TagHelperDescriptor.AssemblyName)}\":\"assembly name\"," +
$"\"{nameof(TagHelperDescriptor.Attributes)}\":[]," +
$"\"{nameof(TagHelperDescriptor.RequiredAttributes)}\":" +
"[\"required attribute one\",\"required attribute two\"]}";
var expectedDescriptor = new TagHelperDescriptor(
prefix: "prefix:",
tagName: "tag name",
typeName: "type name",
assemblyName: "assembly name",
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>(),
requiredAttributes: new[] { "required attribute one", "required attribute two" });
// Act
var descriptor = JsonConvert.DeserializeObject<TagHelperDescriptor>(serializedDescriptor);
// Assert
Assert.NotNull(descriptor);
Assert.Equal(expectedDescriptor.Prefix, descriptor.Prefix, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.TagName, descriptor.TagName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.FullTagName, descriptor.FullTagName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.TypeName, descriptor.TypeName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.AssemblyName, descriptor.AssemblyName, StringComparer.Ordinal);
Assert.Empty(descriptor.Attributes);
Assert.Equal(expectedDescriptor.RequiredAttributes, descriptor.RequiredAttributes, StringComparer.Ordinal);
}
[Fact]
public void TagHelperDescriptor_WithAttributes_CanBeDeserialized()
{
// Arrange
var serializedDescriptor =
$"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," +
$"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," +
$"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," +
$"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," +
$"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," +
$"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"}}," +
$"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," +
$"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," +
$"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"}}]," +
$"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]}}";
var expectedDescriptor = new TagHelperDescriptor(
prefix: "prefix:",
tagName: "tag name",
typeName: "type name",
assemblyName: "assembly name",
attributes: new[]
{
new TagHelperAttributeDescriptor(
name: "attribute one",
propertyName: "property name",
typeName: "property type name"),
new TagHelperAttributeDescriptor(
name: "attribute two",
propertyName: "property name",
typeName: typeof(string).FullName),
},
requiredAttributes: Enumerable.Empty<string>());
// Act
var descriptor = JsonConvert.DeserializeObject<TagHelperDescriptor>(serializedDescriptor);
// Assert
Assert.NotNull(descriptor);
Assert.Equal(expectedDescriptor.Prefix, descriptor.Prefix, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.TagName, descriptor.TagName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.FullTagName, descriptor.FullTagName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.TypeName, descriptor.TypeName, StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.AssemblyName, descriptor.AssemblyName, StringComparer.Ordinal);
Assert.Equal(2, descriptor.Attributes.Count);
Assert.Equal(expectedDescriptor.Attributes[0].IsStringProperty, descriptor.Attributes[0].IsStringProperty);
Assert.Equal(expectedDescriptor.Attributes[0].Name, descriptor.Attributes[0].Name, StringComparer.Ordinal);
Assert.Equal(
expectedDescriptor.Attributes[0].PropertyName,
descriptor.Attributes[0].PropertyName,
StringComparer.Ordinal);
Assert.Equal(
expectedDescriptor.Attributes[0].TypeName,
descriptor.Attributes[0].TypeName,
StringComparer.Ordinal);
Assert.Equal(expectedDescriptor.Attributes[1].IsStringProperty, descriptor.Attributes[1].IsStringProperty);
Assert.Equal(expectedDescriptor.Attributes[1].Name, descriptor.Attributes[1].Name, StringComparer.Ordinal);
Assert.Equal(
expectedDescriptor.Attributes[1].PropertyName,
descriptor.Attributes[1].PropertyName,
StringComparer.Ordinal);
Assert.Equal(
expectedDescriptor.Attributes[1].TypeName,
descriptor.Attributes[1].TypeName,
StringComparer.Ordinal);
Assert.Empty(descriptor.RequiredAttributes);
}
}
}

View File

@ -3355,6 +3355,95 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
SourceLocation.Zero)
}
},
{
"<p bar='false <strong'",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new List<KeyValuePair<string, SyntaxTreeNode>>
{
new KeyValuePair<string, SyntaxTreeNode>(
"bar",
new MarkupBlock(
factory.Markup("false"),
factory.Markup(" <strong")))
})),
new []
{
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
SourceLocation.Zero),
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
SourceLocation.Zero)
}
},
{
"<p bar=false'",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new List<KeyValuePair<string, SyntaxTreeNode>>
{
new KeyValuePair<string, SyntaxTreeNode>(
"bar",
factory.Markup("false"))
})),
new []
{
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
SourceLocation.Zero),
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
SourceLocation.Zero),
new RazorError(
"TagHelper attributes must be welformed.",
absoluteIndex: 12,
lineIndex: 0,
columnIndex: 12)
}
},
{
"<p bar=\"false'",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new List<KeyValuePair<string, SyntaxTreeNode>>
{
new KeyValuePair<string, SyntaxTreeNode>(
"bar",
factory.Markup("false'"))
})),
new []
{
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
SourceLocation.Zero),
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
SourceLocation.Zero)
}
},
{
"<p bar=\"false' ></p>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new List<KeyValuePair<string, SyntaxTreeNode>>
{
new KeyValuePair<string, SyntaxTreeNode>(
"bar",
new MarkupBlock(
factory.Markup("false'"),
factory.Markup(" ></p>")))
})),
new []
{
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"),
SourceLocation.Zero),
new RazorError(
string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"),
SourceLocation.Zero)
}
},
{
"<p foo bar<strong>",
new MarkupBlock(