diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs
index 15a859bfd2..8c9b0c59cd 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs
@@ -679,7 +679,9 @@ namespace Microsoft.AspNet.Mvc.Rendering
protected virtual string GenerateId(string expression)
{
var fullName = DefaultHtmlGenerator.GetFullHtmlFieldName(ViewContext, name: expression);
- return fullName;
+ var id = TagBuilder.CreateSanitizedId(fullName, IdAttributeDotReplacement);
+
+ return id;
}
protected virtual HtmlString GenerateLabel([NotNull] ModelMetadata metadata,
diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TagBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TagBuilder.cs
index 963af37d37..0d14fc70dc 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TagBuilder.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TagBuilder.cs
@@ -49,32 +49,48 @@ namespace Microsoft.AspNet.Mvc.Rendering
}
}
- public static string CreateSanitizedId(string originalId, [NotNull] string invalidCharReplacement)
+ ///
+ /// Return valid HTML 4.01 "id" attribute for an element with the given .
+ ///
+ /// The original element name.
+ ///
+ /// The (normally a single ) to substitute for invalid characters in
+ /// .
+ ///
+ ///
+ /// Valid HTML 4.01 "id" attribute for an element with the given .
+ ///
+ /// Valid "id" attributes are defined in http://www.w3.org/TR/html401/types.html#type-id
+ public static string CreateSanitizedId(string name, [NotNull] string invalidCharReplacement)
{
- if (string.IsNullOrEmpty(originalId))
+ if (string.IsNullOrEmpty(name))
{
return string.Empty;
}
- var firstChar = originalId[0];
-
- var sb = new StringBuilder(originalId.Length);
- sb.Append(firstChar);
-
- for (var i = 1; i < originalId.Length; i++)
+ var firstChar = name[0];
+ if (!Html401IdUtil.IsAsciiLetter(firstChar))
{
- var thisChar = originalId[i];
- if (!char.IsWhiteSpace(thisChar))
+ // The first character must be a letter according to the HTML 4.01 specification.
+ firstChar = 'z';
+ }
+
+ var stringBuffer = new StringBuilder(name.Length);
+ stringBuffer.Append(firstChar);
+ for (var index = 1; index < name.Length; index++)
+ {
+ var thisChar = name[index];
+ if (Html401IdUtil.IsValidIdCharacter(thisChar))
{
- sb.Append(thisChar);
+ stringBuffer.Append(thisChar);
}
else
{
- sb.Append(invalidCharReplacement);
+ stringBuffer.Append(invalidCharReplacement);
}
}
- return sb.ToString();
+ return stringBuffer.ToString();
}
public void GenerateId(string name, [NotNull] string idAttributeDotReplacement)
@@ -195,5 +211,39 @@ namespace Microsoft.AspNet.Mvc.Rendering
return sb.ToString();
}
+
+ private static class Html401IdUtil
+ {
+ public static bool IsAsciiLetter(char testChar)
+ {
+ return (('A' <= testChar && testChar <= 'Z') || ('a' <= testChar && testChar <= 'z'));
+ }
+
+ public static bool IsValidIdCharacter(char testChar)
+ {
+ return (IsAsciiLetter(testChar) || IsAsciiDigit(testChar) || IsAllowableSpecialCharacter(testChar));
+ }
+
+ private static bool IsAsciiDigit(char testChar)
+ {
+ return ('0' <= testChar && testChar <= '9');
+ }
+
+ private static bool IsAllowableSpecialCharacter(char testChar)
+ {
+ switch (testChar)
+ {
+ case '-':
+ case '_':
+ case ':':
+ // Note '.' is valid according to the HTML 4.01 specification. Disallowed here to avoid confusion
+ // with CSS class selectors or when using jQuery.
+ return true;
+
+ default:
+ return false;
+ }
+ }
+ }
}
}
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperLabelExtensionsTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperLabelExtensionsTest.cs
index 342379932c..4860ab1bd6 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperLabelExtensionsTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperLabelExtensionsTest.cs
@@ -60,8 +60,8 @@ namespace Microsoft.AspNet.Mvc.Core
var labelForResult = helper.LabelFor(m => m.Inner.Id);
// Assert
- Assert.Equal("", labelResult.ToString());
- Assert.Equal("", labelForResult.ToString());
+ Assert.Equal("", labelResult.ToString());
+ Assert.Equal("", labelForResult.ToString());
}
[Fact]
@@ -229,13 +229,14 @@ namespace Microsoft.AspNet.Mvc.Core
}
[Theory]
- [InlineData("A", "A")]
- [InlineData("A[23]", "A[23]")]
- [InlineData("A[0].B", "B")]
- [InlineData("A.B.C.D", "D")]
+ [InlineData("A", "A", "A")]
+ [InlineData("A[23]", "A[23]", "A_23_")]
+ [InlineData("A[0].B", "B", "A_0__B")]
+ [InlineData("A.B.C.D", "D", "A_B_C_D")]
public void Label_DisplaysRightmostExpressionSegment_IfPropertiesNotFound(
string expression,
- string expectedResult)
+ string expectedText,
+ string expectedId)
{
// Arrange
var metadataHelper = new MetadataHelper();
@@ -246,7 +247,7 @@ namespace Microsoft.AspNet.Mvc.Core
// Assert
// Label() falls back to expression name when DisplayName and PropertyName are null.
- Assert.Equal("", result.ToString());
+ Assert.Equal("", result.ToString());
}
[Fact]
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperNameExtensionsTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperNameExtensionsTest.cs
index 5944e8ae66..8bc953561f 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperNameExtensionsTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperNameExtensionsTest.cs
@@ -46,12 +46,12 @@ namespace Microsoft.AspNet.Mvc.Core
}
[Theory]
- [InlineData("")]
- [InlineData("A")]
- [InlineData("A[23]")]
- [InlineData("A[0].B")]
- [InlineData("A.B.C.D")]
- public void IdAndNameHelpers_ReturnPrefixForModel(string prefix)
+ [InlineData("", "")]
+ [InlineData("A", "A")]
+ [InlineData("A[23]", "A_23_")]
+ [InlineData("A[0].B", "A_0__B")]
+ [InlineData("A.B.C.D", "A_B_C_D")]
+ public void IdAndNameHelpers_ReturnPrefixForModel(string prefix, string expectedId)
{
// Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper();
@@ -66,9 +66,9 @@ namespace Microsoft.AspNet.Mvc.Core
var nameForModelResult = helper.NameForModel();
// Assert
- Assert.Equal(prefix, idResult);
- Assert.Equal(prefix, idForResult);
- Assert.Equal(prefix, idForModelResult);
+ Assert.Equal(expectedId, idResult);
+ Assert.Equal(expectedId, idForResult);
+ Assert.Equal(expectedId, idForModelResult);
Assert.Equal(prefix, nameResult);
Assert.Equal(prefix, nameForResult);
Assert.Equal(prefix, nameForModelResult);
@@ -94,16 +94,17 @@ namespace Microsoft.AspNet.Mvc.Core
}
[Theory]
- [InlineData(null, "Property1")]
- [InlineData("", "Property1")]
- [InlineData("A", "A.Property1")]
- [InlineData("A[23]", "A[23].Property1")]
- [InlineData("A[0].B", "A[0].B.Property1")]
- [InlineData("A.B.C.D", "A.B.C.D.Property1")]
- public void IdAndNameHelpers_ReturnPrefixAndPropertyName(string prefix, string expectedResult)
+ [InlineData(null, "Property1", "Property1")]
+ [InlineData("", "Property1", "Property1")]
+ [InlineData("A", "A.Property1", "A_Property1")]
+ [InlineData("A[23]", "A[23].Property1", "A_23__Property1")]
+ [InlineData("A[0].B", "A[0].B.Property1", "A_0__B_Property1")]
+ [InlineData("A.B.C.D", "A.B.C.D.Property1", "A_B_C_D_Property1")]
+ public void IdAndNameHelpers_ReturnPrefixAndPropertyName(string prefix, string expectedName, string expectedId)
{
// Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper();
+ helper.ViewData.TemplateInfo.HtmlFieldPrefix = prefix;
// Act
var idResult = helper.Id("Property1");
@@ -112,10 +113,10 @@ namespace Microsoft.AspNet.Mvc.Core
var nameForResult = helper.NameFor(m => m.Property1);
// Assert
- Assert.Equal("Property1", idResult);
- Assert.Equal("Property1", idForResult);
- Assert.Equal("Property1", nameResult);
- Assert.Equal("Property1", nameForResult);
+ Assert.Equal(expectedId, idResult);
+ Assert.Equal(expectedId, idForResult);
+ Assert.Equal(expectedName, nameResult);
+ Assert.Equal(expectedName, nameForResult);
}
[Fact]
@@ -131,8 +132,8 @@ namespace Microsoft.AspNet.Mvc.Core
var nameForResult = helper.NameFor(m => m.Inner.Id);
// Assert
- Assert.Equal("Inner.Id", idResult);
- Assert.Equal("Inner.Id", idForResult);
+ Assert.Equal("Inner_Id", idResult);
+ Assert.Equal("Inner_Id", idForResult);
Assert.Equal("Inner.Id", nameResult);
Assert.Equal("Inner.Id", nameForResult);
}
@@ -194,10 +195,10 @@ namespace Microsoft.AspNet.Mvc.Core
}
[Theory]
- [InlineData("A")]
- [InlineData("A[0].B")]
- [InlineData("A.B.C.D")]
- public void IdAndName_ReturnExpression_EvenIfExpressionNotFound(string expression)
+ [InlineData("A", "A")]
+ [InlineData("A[0].B", "A_0__B")]
+ [InlineData("A.B.C.D", "A_B_C_D")]
+ public void IdAndName_ReturnExpression_EvenIfExpressionNotFound(string expression, string expectedId)
{
// Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper();
@@ -207,7 +208,7 @@ namespace Microsoft.AspNet.Mvc.Core
var nameResult = helper.Name(expression);
// Assert
- Assert.Equal(expression, idResult);
+ Assert.Equal(expectedId, idResult);
Assert.Equal(expression, nameResult);
}
diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/InputTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/InputTagHelperTest.cs
index fb1171d9e8..c0366d1299 100644
--- a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/InputTagHelperTest.cs
+++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/InputTagHelperTest.cs
@@ -14,8 +14,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
public class InputTagHelperTest
{
// Model (List or Model instance), container type (Model or NestModel), model accessor,
- // property path, expected value.
- public static TheoryData