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, string, string> TestDataSet + // property path / id, expected value. + public static TheoryData, NameAndId, string> TestDataSet { get { @@ -41,32 +41,32 @@ namespace Microsoft.AspNet.Mvc.TagHelpers modelWithText, }; - return new TheoryData, string, string> + return new TheoryData, NameAndId, string> { - { null, typeof(Model), () => null, "Text", + { null, typeof(Model), () => null, new NameAndId("Text", "Text"), string.Empty }, - { modelWithNull, typeof(Model), () => modelWithNull.Text, "Text", + { modelWithNull, typeof(Model), () => modelWithNull.Text, new NameAndId("Text", "Text"), string.Empty }, - { modelWithText, typeof(Model), () => modelWithText.Text, "Text", + { modelWithText, typeof(Model), () => modelWithText.Text, new NameAndId("Text", "Text"), "outer text" }, - { modelWithNull, typeof(NestedModel), () => modelWithNull.NestedModel.Text, "NestedModel.Text", - string.Empty }, - { modelWithText, typeof(NestedModel), () => modelWithText.NestedModel.Text, "NestedModel.Text", - "inner text" }, + { modelWithNull, typeof(NestedModel), () => modelWithNull.NestedModel.Text, + new NameAndId("NestedModel.Text", "NestedModel_Text"), string.Empty }, + { modelWithText, typeof(NestedModel), () => modelWithText.NestedModel.Text, + new NameAndId("NestedModel.Text", "NestedModel_Text"), "inner text" }, // Top-level indexing does not work end-to-end due to code generation issue #1345. // TODO: Remove above comment when #1345 is fixed. - { models, typeof(Model), () => models[0].Text, "[0].Text", - string.Empty }, - { models, typeof(Model), () => models[1].Text, "[1].Text", - "outer text" }, + { models, typeof(Model), () => models[0].Text, + new NameAndId("[0].Text", "z0__Text"), string.Empty }, + { models, typeof(Model), () => models[1].Text, + new NameAndId("[1].Text", "z1__Text"), "outer text" }, - { models, typeof(NestedModel), () => models[0].NestedModel.Text, "[0].NestedModel.Text", - string.Empty }, - { models, typeof(NestedModel), () => models[1].NestedModel.Text, "[1].NestedModel.Text", - "inner text" }, + { models, typeof(NestedModel), () => models[0].NestedModel.Text, + new NameAndId("[0].NestedModel.Text", "z0__NestedModel_Text"), string.Empty }, + { models, typeof(NestedModel), () => models[1].NestedModel.Text, + new NameAndId("[1].NestedModel.Text", "z1__NestedModel_Text"), "inner text" }, }; } } @@ -77,7 +77,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers object model, Type containerType, Func modelAccessor, - string propertyPath, + NameAndId nameAndId, string expectedValue) { // Arrange @@ -85,8 +85,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers { { "class", "form-control" }, { "type", "text" }, - { "id", propertyPath }, - { "name", propertyPath }, + { "id", nameAndId.Id }, + { "name", nameAndId.Name }, { "valid", "from validation attributes" }, { "value", expectedValue }, }; @@ -97,7 +97,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers // Property name is either nameof(Model.Text) or nameof(NestedModel.Text). var metadata = metadataProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName: "Text"); - var modelExpression = new ModelExpression(propertyPath, metadata); + var modelExpression = new ModelExpression(nameAndId.Name, metadata); var tagHelperContext = new TagHelperContext(new Dictionary()); var htmlAttributes = new Dictionary @@ -185,6 +185,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers Assert.Equal(expectedTagName, output.TagName); } + public class NameAndId + { + public NameAndId(string name, string id) + { + Name = name; + Id = id; + } + + public string Name { get; private set; } + + public string Id { get; private set; } + } + private class Model { public string Text { get; set; } diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LabelTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LabelTagHelperTest.cs index 2371398e1c..4dab736b23 100644 --- a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LabelTagHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LabelTagHelperTest.cs @@ -44,45 +44,45 @@ namespace Microsoft.AspNet.Mvc.TagHelpers return new TheoryData, string, TagHelperOutputContent> { { null, typeof(Model), () => null, "Text", - new TagHelperOutputContent(Environment.NewLine, "Text") }, + new TagHelperOutputContent(Environment.NewLine, "Text", "Text") }, { modelWithNull, typeof(Model), () => modelWithNull.Text, "Text", - new TagHelperOutputContent(Environment.NewLine, "Text") }, + new TagHelperOutputContent(Environment.NewLine, "Text", "Text") }, { modelWithText, typeof(Model), () => modelWithText.Text, "Text", - new TagHelperOutputContent(Environment.NewLine, "Text") }, + new TagHelperOutputContent(Environment.NewLine, "Text", "Text") }, { modelWithText, typeof(Model), () => modelWithNull.Text, "Text", - new TagHelperOutputContent("Hello World", "Hello World") }, + new TagHelperOutputContent("Hello World", "Hello World", "Text") }, { modelWithText, typeof(Model), () => modelWithText.Text, "Text", - new TagHelperOutputContent("Hello World", "Hello World") }, + new TagHelperOutputContent("Hello World", "Hello World", "Text") }, { modelWithNull, typeof(NestedModel), () => modelWithNull.NestedModel.Text, "NestedModel.Text", - new TagHelperOutputContent(Environment.NewLine, "Text") }, + new TagHelperOutputContent(Environment.NewLine, "Text", "NestedModel_Text") }, { modelWithText, typeof(NestedModel), () => modelWithText.NestedModel.Text, "NestedModel.Text", - new TagHelperOutputContent(Environment.NewLine, "Text") }, + new TagHelperOutputContent(Environment.NewLine, "Text", "NestedModel_Text") }, { modelWithNull, typeof(NestedModel), () => modelWithNull.NestedModel.Text, "NestedModel.Text", - new TagHelperOutputContent("Hello World", "Hello World") }, + new TagHelperOutputContent("Hello World", "Hello World", "NestedModel_Text") }, { modelWithText, typeof(NestedModel), () => modelWithText.NestedModel.Text, "NestedModel.Text", - new TagHelperOutputContent("Hello World", "Hello World") }, + new TagHelperOutputContent("Hello World", "Hello World", "NestedModel_Text") }, // Note: Tests cases below here will not work in practice due to current limitations on indexing // into ModelExpressions. Will be fixed in https://github.com/aspnet/Mvc/issues/1345. { models, typeof(Model), () => models[0].Text, "[0].Text", - new TagHelperOutputContent(Environment.NewLine, "Text") }, + new TagHelperOutputContent(Environment.NewLine, "Text", "z0__Text") }, { models, typeof(Model), () => models[1].Text, "[1].Text", - new TagHelperOutputContent(Environment.NewLine, "Text") }, + new TagHelperOutputContent(Environment.NewLine, "Text", "z1__Text") }, { models, typeof(Model), () => models[0].Text, "[0].Text", - new TagHelperOutputContent("Hello World", "Hello World") }, + new TagHelperOutputContent("Hello World", "Hello World", "z0__Text") }, { models, typeof(Model), () => models[1].Text, "[1].Text", - new TagHelperOutputContent("Hello World", "Hello World") }, + new TagHelperOutputContent("Hello World", "Hello World", "z1__Text") }, { models, typeof(NestedModel), () => models[0].NestedModel.Text, "[0].NestedModel.Text", - new TagHelperOutputContent(Environment.NewLine, "Text") }, + new TagHelperOutputContent(Environment.NewLine, "Text", "z0__NestedModel_Text") }, { models, typeof(NestedModel), () => models[1].NestedModel.Text, "[1].NestedModel.Text", - new TagHelperOutputContent(Environment.NewLine, "Text") }, + new TagHelperOutputContent(Environment.NewLine, "Text", "z1__NestedModel_Text") }, { models, typeof(NestedModel), () => models[0].NestedModel.Text, "[0].NestedModel.Text", - new TagHelperOutputContent("Hello World", "Hello World") }, + new TagHelperOutputContent("Hello World", "Hello World", "z0__NestedModel_Text") }, { models, typeof(NestedModel), () => models[1].NestedModel.Text, "[1].NestedModel.Text", - new TagHelperOutputContent("Hello World", "Hello World") }, + new TagHelperOutputContent("Hello World", "Hello World", "z1__NestedModel_Text") }, }; } } @@ -100,7 +100,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var expectedAttributes = new Dictionary { { "class", "form-control" }, - { "for", propertyPath } + { "for", tagHelperOutputContent.ExpectedId } }; var metadataProvider = new DataAnnotationsModelMetadataProvider(); @@ -173,14 +173,18 @@ namespace Microsoft.AspNet.Mvc.TagHelpers public class TagHelperOutputContent { - public TagHelperOutputContent(string outputContent, string expectedContent) + public TagHelperOutputContent(string outputContent, string expectedContent, string expectedId) { OriginalContent = outputContent; ExpectedContent = expectedContent; + ExpectedId = expectedId; } public string OriginalContent { get; set; } + public string ExpectedContent { get; set; } + + public string ExpectedId { get; set; } } private class Model diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/TextAreaTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/TextAreaTagHelperTest.cs index 2e8c19c91a..61a69b0a9b 100644 --- a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/TextAreaTagHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/TextAreaTagHelperTest.cs @@ -14,8 +14,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers public class TextAreaTagHelperTest { // Model (List or Model instance), container type (Model or NestModel), model accessor, - // property path, expected content. - public static TheoryData, string, string> TestDataSet + // property path / id, expected content. + public static TheoryData, NameAndId, string> TestDataSet { get { @@ -41,31 +41,40 @@ namespace Microsoft.AspNet.Mvc.TagHelpers modelWithText, }; - return new TheoryData, string, string> + return new TheoryData, NameAndId, string> { - { null, typeof(Model), () => null, "Text", + { null, typeof(Model), () => null, + new NameAndId("Text", "Text"), Environment.NewLine }, - { modelWithNull, typeof(Model), () => modelWithNull.Text, "Text", + { modelWithNull, typeof(Model), () => modelWithNull.Text, + new NameAndId("Text", "Text"), Environment.NewLine }, - { modelWithText, typeof(Model), () => modelWithText.Text, "Text", + { modelWithText, typeof(Model), () => modelWithText.Text, + new NameAndId("Text", "Text"), Environment.NewLine + "outer text" }, - { modelWithNull, typeof(NestedModel), () => modelWithNull.NestedModel.Text, "NestedModel.Text", + { modelWithNull, typeof(NestedModel), () => modelWithNull.NestedModel.Text, + new NameAndId("NestedModel.Text", "NestedModel_Text"), Environment.NewLine }, - { modelWithText, typeof(NestedModel), () => modelWithText.NestedModel.Text, "NestedModel.Text", + { modelWithText, typeof(NestedModel), () => modelWithText.NestedModel.Text, + new NameAndId("NestedModel.Text", "NestedModel_Text"), Environment.NewLine + "inner text" }, // Top-level indexing does not work end-to-end due to code generation issue #1345. // TODO: Remove above comment when #1345 is fixed. - { models, typeof(Model), () => models[0].Text, "[0].Text", + { models, typeof(Model), () => models[0].Text, + new NameAndId("[0].Text", "z0__Text"), Environment.NewLine }, - { models, typeof(Model), () => models[1].Text, "[1].Text", + { models, typeof(Model), () => models[1].Text, + new NameAndId("[1].Text", "z1__Text"), Environment.NewLine + "outer text" }, - { models, typeof(NestedModel), () => models[0].NestedModel.Text, "[0].NestedModel.Text", + { models, typeof(NestedModel), () => models[0].NestedModel.Text, + new NameAndId("[0].NestedModel.Text", "z0__NestedModel_Text"), Environment.NewLine }, - { models, typeof(NestedModel), () => models[1].NestedModel.Text, "[1].NestedModel.Text", + { models, typeof(NestedModel), () => models[1].NestedModel.Text, + new NameAndId("[1].NestedModel.Text", "z1__NestedModel_Text"), Environment.NewLine + "inner text" }, }; } @@ -77,15 +86,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers object model, Type containerType, Func modelAccessor, - string propertyPath, + NameAndId nameAndId, string expectedContent) { // Arrange var expectedAttributes = new Dictionary { { "class", "form-control" }, - { "id", propertyPath }, - { "name", propertyPath }, + { "id", nameAndId.Id }, + { "name", nameAndId.Name }, { "valid", "from validation attributes" }, }; var expectedTagName = "textarea"; @@ -94,7 +103,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers // Property name is either nameof(Model.Text) or nameof(NestedModel.Text). var metadata = metadataProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName: "Text"); - var modelExpression = new ModelExpression(propertyPath, metadata); + var modelExpression = new ModelExpression(nameAndId.Name, metadata); var tagHelper = new TextAreaTagHelper { For = modelExpression, @@ -178,6 +187,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers Assert.Equal(expectedTagName, output.TagName); } + public class NameAndId + { + public NameAndId(string name, string id) + { + Name = name; + Id = id; + } + + public string Name { get; private set; } + + public string Id { get; private set; } + } + private class Model { public string Text { get; set; }