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");
}
/// <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>
/// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} contains a '{4}' character.
/// </summary>
@ -283,19 +299,67 @@ namespace Microsoft.AspNet.Razor.Runtime
}
/// <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>
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>
/// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'.
/// </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>

View File

@ -159,6 +159,9 @@
<data name="TagHelperDescriptorFactory_Tag" xml:space="preserve">
<value>Tag</value>
</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">
<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>
@ -168,7 +171,16 @@
<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>
</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>
</data>
<data name="TagHelperAttributeList_CannotAddWithNullName" xml:space="preserve">

View File

@ -13,23 +13,41 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{
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>
/// Instantiates a new instance of the <see cref="HtmlAttributeNameAttribute"/> class.
/// </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)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(name));
}
Name = name;
}
/// <summary>
/// HTML attribute name of the associated property.
/// </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; }
/// <summary>
@ -42,9 +60,14 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// <see cref="string"/>.
/// </remarks>
/// <value>
/// <para>
/// If associated property is compatible with
/// <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>.
/// </para>
/// </value>
public string DictionaryAttributePrefix
{

View File

@ -256,45 +256,72 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
bool designTime,
ErrorSink errorSink)
{
var accessibleProperties = type.GetRuntimeProperties().Where(IsAccessibleProperty);
var attributeDescriptors = new List<TagHelperAttributeDescriptor>();
// Keep indexer descriptors separate to avoid sorting the combined list later.
var indexerDescriptors = new List<TagHelperAttributeDescriptor>();
var accessibleProperties = type.GetRuntimeProperties().Where(IsAccessibleProperty);
foreach (var property in accessibleProperties)
{
var attributeNameAttribute = property.GetCustomAttribute<HtmlAttributeNameAttribute>(inherit: false);
var descriptor = ToAttributeDescriptor(property, attributeNameAttribute, designTime);
if (ValidateTagHelperAttributeDescriptor(descriptor, type, errorSink))
var hasExplicitName =
attributeNameAttribute != null && !string.IsNullOrEmpty(attributeNameAttribute.Name);
var attributeName = hasExplicitName ? attributeNameAttribute.Name : ToHtmlCase(property.Name);
TagHelperAttributeDescriptor mainDescriptor = null;
if (property.SetMethod?.IsPublic == true)
{
bool isInvalid;
var indexerDescriptor = ToIndexerAttributeDescriptor(
property,
attributeNameAttribute,
parentType: type,
errorSink: errorSink,
defaultPrefix: descriptor.Name + "-",
designTime: designTime,
isInvalid: out isInvalid);
if (indexerDescriptor != null &&
!ValidateTagHelperAttributeDescriptor(indexerDescriptor, type, errorSink))
mainDescriptor = ToAttributeDescriptor(property, attributeName, designTime);
if (!ValidateTagHelperAttributeDescriptor(mainDescriptor, type, errorSink))
{
isInvalid = true;
}
if (isInvalid)
{
// HtmlAttributeNameAttribute was not valid. Ignore this property completely.
// HtmlAttributeNameAttribute.Name is invalid. Ignore this property completely.
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);
if (indexerDescriptor != null)
{
indexerDescriptors.Add(indexerDescriptor);
}
bool isInvalid;
var indexerDescriptor = ToIndexerAttributeDescriptor(
property,
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,
ErrorSink errorSink)
{
var nameOrPrefix = attributeDescriptor.IsIndexer ?
Resources.TagHelperDescriptorFactory_Prefix :
Resources.TagHelperDescriptorFactory_Name;
string nameOrPrefix;
if (attributeDescriptor.IsIndexer)
{
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(
attributeDescriptor.Name,
parentType,
@ -329,8 +373,9 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{
if (string.IsNullOrEmpty(attributeNameOrPrefix))
{
// HtmlAttributeNameAttribute validates Name is non-null and non-empty. Both are valid for
// DictionaryAttributePrefix. (Empty DictionaryAttributePrefix is a corner case which would bind every
// ValidateTagHelperAttributeDescriptor validates Name is non-null and non-empty. The empty string is
// 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
// should be required and what scenarios a minimum length would break.)
return true;
@ -388,13 +433,9 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
private static TagHelperAttributeDescriptor ToAttributeDescriptor(
PropertyInfo property,
HtmlAttributeNameAttribute attributeNameAttribute,
string attributeName,
bool designTime)
{
var attributeName = attributeNameAttribute != null ?
attributeNameAttribute.Name :
ToHtmlCase(property.Name);
return ToAttributeDescriptor(
property,
attributeName,
@ -413,6 +454,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
out bool isInvalid)
{
isInvalid = false;
var hasPublicSetter = property.SetMethod?.IsPublic == true;
var dictionaryTypeArguments = ClosedGenericMatcher.ExtractGenericInterface(
property.PropertyType,
typeof(IDictionary<,>))
@ -426,13 +468,44 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
isInvalid = true;
errorSink.OnError(
SourceLocation.Zero,
Resources.FormatTagHelperDescriptorFactory_InvalidAttributePrefix(
Resources.FormatTagHelperDescriptorFactory_InvalidAttributePrefixNotNull(
parentType.FullName,
property.Name,
nameof(HtmlAttributeNameAttribute),
nameof(HtmlAttributeNameAttribute.DictionaryAttributePrefix),
"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;
}
@ -482,9 +555,8 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
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 &&
property.SetMethod?.IsPublic == true &&
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);
}
/// <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>
/// Directive '{0}' must have a value.
/// </summary>

View File

@ -958,7 +958,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
isStringProperty: false,
designTimeDescriptor: null),
new TagHelperAttributeDescriptor(
name: "valid-prefix",
name: "valid-name-",
propertyName: nameof(SingleValidHtmlAttributePrefix.DictionaryProperty),
typeName: typeof(string).FullName,
isIndexer: true,
@ -1048,6 +1048,20 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
isIndexer: true,
isStringProperty: false,
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]
},
@ -1087,6 +1101,14 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
onError(
typeof(MultipleInvalidHtmlAttributePrefix).FullName,
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);
}
public static TheoryData<string> ValidAttributeNameOrPrefixData
public static TheoryData<string> ValidAttributeNameData
{
get
{
return new TheoryData<string>
{
null,
string.Empty,
"data",
"dataa-",
"ValidName",
@ -1147,7 +1167,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
}
[Theory]
[MemberData(nameof(ValidAttributeNameOrPrefixData))]
[MemberData(nameof(ValidAttributeNameData))]
public void ValidateTagHelperAttributeDescriptor_WithValidName_ReturnsTrue(string name)
{
// Arrange
@ -1170,8 +1190,25 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
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]
[MemberData(nameof(ValidAttributeNameOrPrefixData))]
[MemberData(nameof(ValidAttributePrefixData))]
public void ValidateTagHelperAttributeDescriptor_WithValidPrefix_ReturnsTrue(string prefix)
{
// Arrange
@ -1732,7 +1769,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
private class SingleValidHtmlAttributePrefix : TagHelper
{
[HtmlAttributeName("valid-name", DictionaryAttributePrefix = "valid-prefix")]
[HtmlAttributeName("valid-name")]
public IDictionary<string, string> DictionaryProperty { get; set; }
}
@ -1755,6 +1792,11 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
[HtmlAttributeName("valid-name6")]
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
@ -1779,6 +1821,12 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
[HtmlAttributeName("valid-name5", DictionaryAttributePrefix = "valid-prefix5-")]
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>