Add support for get-only indexer properties

- #399
- move invalid `HtmlAttributeNameAttribute.Name` checking to `TagHelperDescriptorFactory`
- add a few new error cases
 - but does not cover all the new error cases e.g. `[HtmlAttributeName(...)]` on a get-only `int` property

nit:
- `resx` target removed some older resources from `RazorResources.Designer.cs`
This commit is contained in:
Doug Bunting 2015-06-27 16:35:52 -07:00
parent 1f480386f4
commit 69d8e52bf9
6 changed files with 277 additions and 74 deletions

View File

@ -234,6 +234,22 @@ namespace Microsoft.AspNet.Razor.Runtime
return GetString("TagHelperDescriptorFactory_Tag"); return GetString("TagHelperDescriptorFactory_Tag");
} }
/// <summary>
/// Invalid tag helper bound property '{0}.{1}'. An '{2}' must not be associated with a property with no public setter unless its type implements '{3}'.
/// </summary>
internal static string TagHelperDescriptorFactory_InvalidAttributeNameAttribute
{
get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameAttribute"); }
}
/// <summary>
/// Invalid tag helper bound property '{0}.{1}'. An '{2}' must not be associated with a property with no public setter unless its type implements '{3}'.
/// </summary>
internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameAttribute(object p0, object p1, object p2, object p3)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameAttribute"), p0, p1, p2, p3);
}
/// <summary> /// <summary>
/// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} contains a '{4}' character. /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} contains a '{4}' character.
/// </summary> /// </summary>
@ -283,19 +299,67 @@ namespace Microsoft.AspNet.Razor.Runtime
} }
/// <summary> /// <summary>
/// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'. /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a null or empty name.
/// </summary> /// </summary>
internal static string TagHelperDescriptorFactory_InvalidAttributePrefix internal static string TagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty
{ {
get { return GetString("TagHelperDescriptorFactory_InvalidAttributePrefix"); } get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty"); }
}
/// <summary>
/// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a null or empty name.
/// </summary>
internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty"), p0, p1);
}
/// <summary>
/// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null or empty if property has no public setter.
/// </summary>
internal static string TagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty
{
get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty"); }
}
/// <summary>
/// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null or empty if property has no public setter.
/// </summary>
internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty(object p0, object p1, object p2, object p3)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty"), p0, p1, p2, p3);
}
/// <summary>
/// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must not be null if property has no public setter and its type implements '{4}'.
/// </summary>
internal static string TagHelperDescriptorFactory_InvalidAttributePrefixNull
{
get { return GetString("TagHelperDescriptorFactory_InvalidAttributePrefixNull"); }
}
/// <summary>
/// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must not be null if property has no public setter and its type implements '{4}'.
/// </summary>
internal static string FormatTagHelperDescriptorFactory_InvalidAttributePrefixNull(object p0, object p1, object p2, object p3, object p4)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributePrefixNull"), p0, p1, p2, p3, p4);
} }
/// <summary> /// <summary>
/// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'. /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'.
/// </summary> /// </summary>
internal static string FormatTagHelperDescriptorFactory_InvalidAttributePrefix(object p0, object p1, object p2, object p3, object p4) internal static string TagHelperDescriptorFactory_InvalidAttributePrefixNotNull
{ {
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributePrefix"), p0, p1, p2, p3, p4); get { return GetString("TagHelperDescriptorFactory_InvalidAttributePrefixNotNull"); }
}
/// <summary>
/// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'.
/// </summary>
internal static string FormatTagHelperDescriptorFactory_InvalidAttributePrefixNotNull(object p0, object p1, object p2, object p3, object p4)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributePrefixNotNull"), p0, p1, p2, p3, p4);
} }
/// <summary> /// <summary>

View File

@ -159,6 +159,9 @@
<data name="TagHelperDescriptorFactory_Tag" xml:space="preserve"> <data name="TagHelperDescriptorFactory_Tag" xml:space="preserve">
<value>Tag</value> <value>Tag</value>
</data> </data>
<data name="TagHelperDescriptorFactory_InvalidAttributeNameAttribute" xml:space="preserve">
<value>Invalid tag helper bound property '{0}.{1}'. An '{2}' must not be associated with a property with no public setter unless its type implements '{3}'.</value>
</data>
<data name="TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter" xml:space="preserve"> <data name="TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter" xml:space="preserve">
<value>Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} contains a '{4}' character.</value> <value>Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} contains a '{4}' character.</value>
</data> </data>
@ -168,7 +171,16 @@
<data name="TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace" xml:space="preserve"> <data name="TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace" xml:space="preserve">
<value>Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a whitespace {2}.</value> <value>Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a whitespace {2}.</value>
</data> </data>
<data name="TagHelperDescriptorFactory_InvalidAttributePrefix" xml:space="preserve"> <data name="TagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty" xml:space="preserve">
<value>Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a null or empty name.</value>
</data>
<data name="TagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty" xml:space="preserve">
<value>Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null or empty if property has no public setter.</value>
</data>
<data name="TagHelperDescriptorFactory_InvalidAttributePrefixNull" xml:space="preserve">
<value>Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must not be null if property has no public setter and its type implements '{4}'.</value>
</data>
<data name="TagHelperDescriptorFactory_InvalidAttributePrefixNotNull" xml:space="preserve">
<value>Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'.</value> <value>Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'.</value>
</data> </data>
<data name="TagHelperAttributeList_CannotAddWithNullName" xml:space="preserve"> <data name="TagHelperAttributeList_CannotAddWithNullName" xml:space="preserve">

View File

@ -13,23 +13,41 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{ {
private string _dictionaryAttributePrefix; private string _dictionaryAttributePrefix;
/// <summary>
/// Instantiates a new instance of the <see cref="HtmlAttributeNameAttribute"/> class with <see cref="Name"/>
/// equal to <c>null</c>.
/// </summary>
/// <remarks>
/// Associated property must not have a public setter and must be compatible with
/// <see cref="System.Collections.Generic.IDictionary{TKey, TValue}"/> where <c>TKey</c> is
/// <see cref="string"/>.
/// </remarks>
public HtmlAttributeNameAttribute()
{
}
/// <summary> /// <summary>
/// Instantiates a new instance of the <see cref="HtmlAttributeNameAttribute"/> class. /// Instantiates a new instance of the <see cref="HtmlAttributeNameAttribute"/> class.
/// </summary> /// </summary>
/// <param name="name">HTML attribute name for the associated property.</param> /// <param name="name">
/// HTML attribute name for the associated property. Must be <c>null</c> or empty if associated property does
/// not have a public setter and is compatible with
/// <see cref="System.Collections.Generic.IDictionary{TKey, TValue}"/> where <c>TKey</c> is
/// <see cref="string"/>. Otherwise must not be <c>null</c> or empty.
/// </param>
public HtmlAttributeNameAttribute(string name) public HtmlAttributeNameAttribute(string name)
{ {
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(name));
}
Name = name; Name = name;
} }
/// <summary> /// <summary>
/// HTML attribute name of the associated property. /// HTML attribute name of the associated property.
/// </summary> /// </summary>
/// <value>
/// <c>null</c> or empty if and only if associated property does not have a public setter and is compatible
/// with <see cref="System.Collections.Generic.IDictionary{TKey, TValue}"/> where <c>TKey</c> is
/// <see cref="string"/>.
/// </value>
public string Name { get; } public string Name { get; }
/// <summary> /// <summary>
@ -42,9 +60,14 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// <see cref="string"/>. /// <see cref="string"/>.
/// </remarks> /// </remarks>
/// <value> /// <value>
/// <para>
/// If associated property is compatible with /// If associated property is compatible with
/// <see cref="System.Collections.Generic.IDictionary{TKey, TValue}"/>, default value is <c>Name + "-"</c>. /// <see cref="System.Collections.Generic.IDictionary{TKey, TValue}"/>, default value is <c>Name + "-"</c>.
/// <see cref="Name"/> must not be <c>null</c> or empty in this case.
/// </para>
/// <para>
/// Otherwise default value is <c>null</c>. /// Otherwise default value is <c>null</c>.
/// </para>
/// </value> /// </value>
public string DictionaryAttributePrefix public string DictionaryAttributePrefix
{ {

View File

@ -256,45 +256,72 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
bool designTime, bool designTime,
ErrorSink errorSink) ErrorSink errorSink)
{ {
var accessibleProperties = type.GetRuntimeProperties().Where(IsAccessibleProperty);
var attributeDescriptors = new List<TagHelperAttributeDescriptor>(); var attributeDescriptors = new List<TagHelperAttributeDescriptor>();
// Keep indexer descriptors separate to avoid sorting the combined list later. // Keep indexer descriptors separate to avoid sorting the combined list later.
var indexerDescriptors = new List<TagHelperAttributeDescriptor>(); var indexerDescriptors = new List<TagHelperAttributeDescriptor>();
var accessibleProperties = type.GetRuntimeProperties().Where(IsAccessibleProperty);
foreach (var property in accessibleProperties) foreach (var property in accessibleProperties)
{ {
var attributeNameAttribute = property.GetCustomAttribute<HtmlAttributeNameAttribute>(inherit: false); var attributeNameAttribute = property.GetCustomAttribute<HtmlAttributeNameAttribute>(inherit: false);
var descriptor = ToAttributeDescriptor(property, attributeNameAttribute, designTime); var hasExplicitName =
if (ValidateTagHelperAttributeDescriptor(descriptor, type, errorSink)) attributeNameAttribute != null && !string.IsNullOrEmpty(attributeNameAttribute.Name);
var attributeName = hasExplicitName ? attributeNameAttribute.Name : ToHtmlCase(property.Name);
TagHelperAttributeDescriptor mainDescriptor = null;
if (property.SetMethod?.IsPublic == true)
{ {
bool isInvalid; mainDescriptor = ToAttributeDescriptor(property, attributeName, designTime);
var indexerDescriptor = ToIndexerAttributeDescriptor( if (!ValidateTagHelperAttributeDescriptor(mainDescriptor, type, errorSink))
property,
attributeNameAttribute,
parentType: type,
errorSink: errorSink,
defaultPrefix: descriptor.Name + "-",
designTime: designTime,
isInvalid: out isInvalid);
if (indexerDescriptor != null &&
!ValidateTagHelperAttributeDescriptor(indexerDescriptor, type, errorSink))
{ {
isInvalid = true; // HtmlAttributeNameAttribute.Name is invalid. Ignore this property completely.
}
if (isInvalid)
{
// HtmlAttributeNameAttribute was not valid. Ignore this property completely.
continue; continue;
} }
}
else if (hasExplicitName)
{
// Specified HtmlAttributeNameAttribute.Name though property has no public setter.
errorSink.OnError(
SourceLocation.Zero,
Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty(
type.FullName,
property.Name,
typeof(HtmlAttributeNameAttribute).FullName,
nameof(HtmlAttributeNameAttribute.Name)));
continue;
}
attributeDescriptors.Add(descriptor); bool isInvalid;
if (indexerDescriptor != null) var indexerDescriptor = ToIndexerAttributeDescriptor(
{ property,
indexerDescriptors.Add(indexerDescriptor); attributeNameAttribute,
} parentType: type,
errorSink: errorSink,
defaultPrefix: attributeName + "-",
designTime: designTime,
isInvalid: out isInvalid);
if (indexerDescriptor != null &&
!ValidateTagHelperAttributeDescriptor(indexerDescriptor, type, errorSink))
{
isInvalid = true;
}
if (isInvalid)
{
// The property type or HtmlAttributeNameAttribute.DictionaryAttributePrefix (or perhaps the
// HTML-casing of the property name) is invalid. Ignore this property completely.
continue;
}
if (mainDescriptor != null)
{
attributeDescriptors.Add(mainDescriptor);
}
if (indexerDescriptor != null)
{
indexerDescriptors.Add(indexerDescriptor);
} }
} }
@ -309,9 +336,26 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
Type parentType, Type parentType,
ErrorSink errorSink) ErrorSink errorSink)
{ {
var nameOrPrefix = attributeDescriptor.IsIndexer ? string nameOrPrefix;
Resources.TagHelperDescriptorFactory_Prefix : if (attributeDescriptor.IsIndexer)
Resources.TagHelperDescriptorFactory_Name; {
nameOrPrefix = Resources.TagHelperDescriptorFactory_Prefix;
}
else if (string.IsNullOrEmpty(attributeDescriptor.Name))
{
errorSink.OnError(
SourceLocation.Zero,
Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty(
parentType.FullName,
attributeDescriptor.PropertyName));
return false;
}
else
{
nameOrPrefix = Resources.TagHelperDescriptorFactory_Name;
}
return ValidateTagHelperAttributeNameOrPrefix( return ValidateTagHelperAttributeNameOrPrefix(
attributeDescriptor.Name, attributeDescriptor.Name,
parentType, parentType,
@ -329,8 +373,9 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{ {
if (string.IsNullOrEmpty(attributeNameOrPrefix)) if (string.IsNullOrEmpty(attributeNameOrPrefix))
{ {
// HtmlAttributeNameAttribute validates Name is non-null and non-empty. Both are valid for // ValidateTagHelperAttributeDescriptor validates Name is non-null and non-empty. The empty string is
// DictionaryAttributePrefix. (Empty DictionaryAttributePrefix is a corner case which would bind every // valid for DictionaryAttributePrefix and null is impossible at this point because it means "don't
// create a descriptor". (Empty DictionaryAttributePrefix is a corner case which would bind every
// attribute of a target element. Likely not particularly useful but unclear what minimum length // attribute of a target element. Likely not particularly useful but unclear what minimum length
// should be required and what scenarios a minimum length would break.) // should be required and what scenarios a minimum length would break.)
return true; return true;
@ -388,13 +433,9 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
private static TagHelperAttributeDescriptor ToAttributeDescriptor( private static TagHelperAttributeDescriptor ToAttributeDescriptor(
PropertyInfo property, PropertyInfo property,
HtmlAttributeNameAttribute attributeNameAttribute, string attributeName,
bool designTime) bool designTime)
{ {
var attributeName = attributeNameAttribute != null ?
attributeNameAttribute.Name :
ToHtmlCase(property.Name);
return ToAttributeDescriptor( return ToAttributeDescriptor(
property, property,
attributeName, attributeName,
@ -413,6 +454,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
out bool isInvalid) out bool isInvalid)
{ {
isInvalid = false; isInvalid = false;
var hasPublicSetter = property.SetMethod?.IsPublic == true;
var dictionaryTypeArguments = ClosedGenericMatcher.ExtractGenericInterface( var dictionaryTypeArguments = ClosedGenericMatcher.ExtractGenericInterface(
property.PropertyType, property.PropertyType,
typeof(IDictionary<,>)) typeof(IDictionary<,>))
@ -426,13 +468,44 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
isInvalid = true; isInvalid = true;
errorSink.OnError( errorSink.OnError(
SourceLocation.Zero, SourceLocation.Zero,
Resources.FormatTagHelperDescriptorFactory_InvalidAttributePrefix( Resources.FormatTagHelperDescriptorFactory_InvalidAttributePrefixNotNull(
parentType.FullName, parentType.FullName,
property.Name, property.Name,
nameof(HtmlAttributeNameAttribute), nameof(HtmlAttributeNameAttribute),
nameof(HtmlAttributeNameAttribute.DictionaryAttributePrefix), nameof(HtmlAttributeNameAttribute.DictionaryAttributePrefix),
"IDictionary<string, TValue>")); "IDictionary<string, TValue>"));
} }
else if (attributeNameAttribute != null && !hasPublicSetter)
{
// Associated an HtmlAttributeNameAttribute with a non-dictionary property that lacks a public
// setter.
isInvalid = true;
errorSink.OnError(
SourceLocation.Zero,
Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameAttribute(
parentType.FullName,
property.Name,
nameof(HtmlAttributeNameAttribute),
"IDictionary<string, TValue>"));
}
return null;
}
else if (!hasPublicSetter &&
attributeNameAttribute != null &&
!attributeNameAttribute.DictionaryAttributePrefixSet)
{
// Must set DictionaryAttributePrefix when using HtmlAttributeNameAttribute with a dictionary property
// that lacks a public setter.
isInvalid = true;
errorSink.OnError(
SourceLocation.Zero,
Resources.FormatTagHelperDescriptorFactory_InvalidAttributePrefixNull(
parentType.FullName,
property.Name,
nameof(HtmlAttributeNameAttribute),
nameof(HtmlAttributeNameAttribute.DictionaryAttributePrefix),
"IDictionary<string, TValue>"));
return null; return null;
} }
@ -482,9 +555,8 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
private static bool IsAccessibleProperty(PropertyInfo property) private static bool IsAccessibleProperty(PropertyInfo property)
{ {
// Accessible properties are those with public getters and setters and without [HtmlAttributeNotBound]. // Accessible properties are those with public getters and without [HtmlAttributeNotBound].
return property.GetMethod?.IsPublic == true && return property.GetMethod?.IsPublic == true &&
property.SetMethod?.IsPublic == true &&
property.GetCustomAttribute<HtmlAttributeNotBoundAttribute>(inherit: false) == null; property.GetCustomAttribute<HtmlAttributeNotBoundAttribute>(inherit: false) == null;
} }

View File

@ -1286,22 +1286,6 @@ namespace Microsoft.AspNet.Razor
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpers_CannotHaveCSharpInTagDeclaration"), p0); return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpers_CannotHaveCSharpInTagDeclaration"), p0);
} }
/// <summary>
/// A TagHelperChunkGenerator must only be used with TagHelperBlocks.
/// </summary>
internal static string TagHelpers_TagHelperCodeGeneartorMustBeAssociatedWithATagHelperBlock
{
get { return GetString("TagHelpers_TagHelperCodeGeneartorMustBeAssociatedWithATagHelperBlock"); }
}
/// <summary>
/// A TagHelperChunkGenerator must only be used with TagHelperBlocks.
/// </summary>
internal static string FormatTagHelpers_TagHelperCodeGeneartorMustBeAssociatedWithATagHelperBlock()
{
return GetString("TagHelpers_TagHelperCodeGeneartorMustBeAssociatedWithATagHelperBlock");
}
/// <summary> /// <summary>
/// Directive '{0}' must have a value. /// Directive '{0}' must have a value.
/// </summary> /// </summary>

View File

@ -958,7 +958,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
isStringProperty: false, isStringProperty: false,
designTimeDescriptor: null), designTimeDescriptor: null),
new TagHelperAttributeDescriptor( new TagHelperAttributeDescriptor(
name: "valid-prefix", name: "valid-name-",
propertyName: nameof(SingleValidHtmlAttributePrefix.DictionaryProperty), propertyName: nameof(SingleValidHtmlAttributePrefix.DictionaryProperty),
typeName: typeof(string).FullName, typeName: typeof(string).FullName,
isIndexer: true, isIndexer: true,
@ -1048,6 +1048,20 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
isIndexer: true, isIndexer: true,
isStringProperty: false, isStringProperty: false,
designTimeDescriptor: null), designTimeDescriptor: null),
new TagHelperAttributeDescriptor(
name: "get-only-dictionary-property-",
propertyName: nameof(MultipleValidHtmlAttributePrefix.GetOnlyDictionaryProperty),
typeName: typeof(int).FullName,
isIndexer: true,
isStringProperty: false,
designTimeDescriptor: null),
new TagHelperAttributeDescriptor(
name: "valid-prefix6",
propertyName: nameof(MultipleValidHtmlAttributePrefix.GetOnlyDictionaryPropertyWithAttributePrefix),
typeName: typeof(string).FullName,
isIndexer: true,
isStringProperty: true,
designTimeDescriptor: null),
}, },
new string[0] new string[0]
}, },
@ -1087,6 +1101,14 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
onError( onError(
typeof(MultipleInvalidHtmlAttributePrefix).FullName, typeof(MultipleInvalidHtmlAttributePrefix).FullName,
nameof(MultipleInvalidHtmlAttributePrefix.DictionaryOfIntSubclassProperty)), nameof(MultipleInvalidHtmlAttributePrefix.DictionaryOfIntSubclassProperty)),
onError(
typeof(MultipleInvalidHtmlAttributePrefix).FullName,
nameof(MultipleInvalidHtmlAttributePrefix.GetOnlyDictionaryAttributePrefix)),
$"Invalid tag helper bound property '{ typeof(MultipleInvalidHtmlAttributePrefix).FullName }." +
$"{ nameof(MultipleInvalidHtmlAttributePrefix.GetOnlyDictionaryPropertyWithAttributeName) }'. " +
$"'{ typeof(HtmlAttributeNameAttribute).FullName }." +
$"{ nameof(HtmlAttributeNameAttribute.Name) }' must be null or empty if property has " +
"no public setter.",
} }
}, },
}; };
@ -1128,14 +1150,12 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
TagHelperAttributeDescriptorComparer.Default); TagHelperAttributeDescriptorComparer.Default);
} }
public static TheoryData<string> ValidAttributeNameOrPrefixData public static TheoryData<string> ValidAttributeNameData
{ {
get get
{ {
return new TheoryData<string> return new TheoryData<string>
{ {
null,
string.Empty,
"data", "data",
"dataa-", "dataa-",
"ValidName", "ValidName",
@ -1147,7 +1167,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
} }
[Theory] [Theory]
[MemberData(nameof(ValidAttributeNameOrPrefixData))] [MemberData(nameof(ValidAttributeNameData))]
public void ValidateTagHelperAttributeDescriptor_WithValidName_ReturnsTrue(string name) public void ValidateTagHelperAttributeDescriptor_WithValidName_ReturnsTrue(string name)
{ {
// Arrange // Arrange
@ -1170,8 +1190,25 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
Assert.Empty(errorSink.Errors); Assert.Empty(errorSink.Errors);
} }
public static TheoryData<string> ValidAttributePrefixData
{
get
{
return new TheoryData<string>
{
string.Empty,
"data",
"dataa-",
"ValidName",
"valid-name",
"--valid--name--",
",,--__..oddly.valid::;;",
};
}
}
[Theory] [Theory]
[MemberData(nameof(ValidAttributeNameOrPrefixData))] [MemberData(nameof(ValidAttributePrefixData))]
public void ValidateTagHelperAttributeDescriptor_WithValidPrefix_ReturnsTrue(string prefix) public void ValidateTagHelperAttributeDescriptor_WithValidPrefix_ReturnsTrue(string prefix)
{ {
// Arrange // Arrange
@ -1732,7 +1769,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
private class SingleValidHtmlAttributePrefix : TagHelper private class SingleValidHtmlAttributePrefix : TagHelper
{ {
[HtmlAttributeName("valid-name", DictionaryAttributePrefix = "valid-prefix")] [HtmlAttributeName("valid-name")]
public IDictionary<string, string> DictionaryProperty { get; set; } public IDictionary<string, string> DictionaryProperty { get; set; }
} }
@ -1755,6 +1792,11 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
[HtmlAttributeName("valid-name6")] [HtmlAttributeName("valid-name6")]
public string StringProperty { get; set; } public string StringProperty { get; set; }
public IDictionary<string, int> GetOnlyDictionaryProperty { get; }
[HtmlAttributeName(DictionaryAttributePrefix = "valid-prefix6")]
public IDictionary<string, string> GetOnlyDictionaryPropertyWithAttributePrefix { get; }
} }
private class SingleInvalidHtmlAttributePrefix : TagHelper private class SingleInvalidHtmlAttributePrefix : TagHelper
@ -1779,6 +1821,12 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
[HtmlAttributeName("valid-name5", DictionaryAttributePrefix = "valid-prefix5-")] [HtmlAttributeName("valid-name5", DictionaryAttributePrefix = "valid-prefix5-")]
public DictionaryOfIntSubclass DictionaryOfIntSubclassProperty { get; set; } public DictionaryOfIntSubclass DictionaryOfIntSubclassProperty { get; set; }
[HtmlAttributeName(DictionaryAttributePrefix = "valid-prefix6")]
public IDictionary<int, string> GetOnlyDictionaryAttributePrefix { get; }
[HtmlAttributeName("invalid-name7")]
public IDictionary<string, object> GetOnlyDictionaryPropertyWithAttributeName { get; }
} }
private class DictionarySubclass : Dictionary<string, string> private class DictionarySubclass : Dictionary<string, string>