Sanitize "id" attributes for HTML 4.0.1

- #704 part 2 of 2
- change `@Html.Id()` to sanitize return value; was identical to `@Html.Name()`

Copied `TagBuilder.CreateSanitizedId()` and `TagBuilder.Html401IdUtil` from MVC 5.2
- except this `CreateSanitizedId()` returns a valid identifier if first `char` is not a letter
 - e.g. "[0].Name"

nits:
- expand variable names, use lots of `var`, put `public` members first
- add doc comments for `CreateSanitizedId()`

Note users will be able to apply different sanitization once we fix #1188.
This commit is contained in:
Doug Bunting 2014-10-17 09:12:26 -07:00
parent f9e44ff7f9
commit dd5da33a62
7 changed files with 199 additions and 106 deletions

View File

@ -679,7 +679,9 @@ namespace Microsoft.AspNet.Mvc.Rendering
protected virtual string GenerateId(string expression) protected virtual string GenerateId(string expression)
{ {
var fullName = DefaultHtmlGenerator.GetFullHtmlFieldName(ViewContext, name: 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, protected virtual HtmlString GenerateLabel([NotNull] ModelMetadata metadata,

View File

@ -49,32 +49,48 @@ namespace Microsoft.AspNet.Mvc.Rendering
} }
} }
public static string CreateSanitizedId(string originalId, [NotNull] string invalidCharReplacement) /// <summary>
/// Return valid HTML 4.01 "id" attribute for an element with the given <paramref name="name"/>.
/// </summary>
/// <param name="name">The original element name.</param>
/// <param name="invalidCharReplacement">
/// The <see cref="string"/> (normally a single <see cref="char"/>) to substitute for invalid characters in
/// <paramref name="name"/>.
/// </param>
/// <returns>
/// Valid HTML 4.01 "id" attribute for an element with the given <paramref name="name"/>.
/// </returns>
/// <remarks>Valid "id" attributes are defined in http://www.w3.org/TR/html401/types.html#type-id</remarks>
public static string CreateSanitizedId(string name, [NotNull] string invalidCharReplacement)
{ {
if (string.IsNullOrEmpty(originalId)) if (string.IsNullOrEmpty(name))
{ {
return string.Empty; return string.Empty;
} }
var firstChar = originalId[0]; var firstChar = name[0];
if (!Html401IdUtil.IsAsciiLetter(firstChar))
var sb = new StringBuilder(originalId.Length);
sb.Append(firstChar);
for (var i = 1; i < originalId.Length; i++)
{ {
var thisChar = originalId[i]; // The first character must be a letter according to the HTML 4.01 specification.
if (!char.IsWhiteSpace(thisChar)) 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 else
{ {
sb.Append(invalidCharReplacement); stringBuffer.Append(invalidCharReplacement);
} }
} }
return sb.ToString(); return stringBuffer.ToString();
} }
public void GenerateId(string name, [NotNull] string idAttributeDotReplacement) public void GenerateId(string name, [NotNull] string idAttributeDotReplacement)
@ -195,5 +211,39 @@ namespace Microsoft.AspNet.Mvc.Rendering
return sb.ToString(); 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;
}
}
}
} }
} }

View File

@ -60,8 +60,8 @@ namespace Microsoft.AspNet.Mvc.Core
var labelForResult = helper.LabelFor(m => m.Inner.Id); var labelForResult = helper.LabelFor(m => m.Inner.Id);
// Assert // Assert
Assert.Equal("<label for=\"Inner.Id\">Id</label>", labelResult.ToString()); Assert.Equal("<label for=\"Inner_Id\">Id</label>", labelResult.ToString());
Assert.Equal("<label for=\"Inner.Id\">Id</label>", labelForResult.ToString()); Assert.Equal("<label for=\"Inner_Id\">Id</label>", labelForResult.ToString());
} }
[Fact] [Fact]
@ -229,13 +229,14 @@ namespace Microsoft.AspNet.Mvc.Core
} }
[Theory] [Theory]
[InlineData("A", "A")] [InlineData("A", "A", "A")]
[InlineData("A[23]", "A[23]")] [InlineData("A[23]", "A[23]", "A_23_")]
[InlineData("A[0].B", "B")] [InlineData("A[0].B", "B", "A_0__B")]
[InlineData("A.B.C.D", "D")] [InlineData("A.B.C.D", "D", "A_B_C_D")]
public void Label_DisplaysRightmostExpressionSegment_IfPropertiesNotFound( public void Label_DisplaysRightmostExpressionSegment_IfPropertiesNotFound(
string expression, string expression,
string expectedResult) string expectedText,
string expectedId)
{ {
// Arrange // Arrange
var metadataHelper = new MetadataHelper(); var metadataHelper = new MetadataHelper();
@ -246,7 +247,7 @@ namespace Microsoft.AspNet.Mvc.Core
// Assert // Assert
// Label() falls back to expression name when DisplayName and PropertyName are null. // Label() falls back to expression name when DisplayName and PropertyName are null.
Assert.Equal("<label for=\"" + expression + "\">" + expectedResult + "</label>", result.ToString()); Assert.Equal("<label for=\"" + expectedId + "\">" + expectedText + "</label>", result.ToString());
} }
[Fact] [Fact]

View File

@ -46,12 +46,12 @@ namespace Microsoft.AspNet.Mvc.Core
} }
[Theory] [Theory]
[InlineData("")] [InlineData("", "")]
[InlineData("A")] [InlineData("A", "A")]
[InlineData("A[23]")] [InlineData("A[23]", "A_23_")]
[InlineData("A[0].B")] [InlineData("A[0].B", "A_0__B")]
[InlineData("A.B.C.D")] [InlineData("A.B.C.D", "A_B_C_D")]
public void IdAndNameHelpers_ReturnPrefixForModel(string prefix) public void IdAndNameHelpers_ReturnPrefixForModel(string prefix, string expectedId)
{ {
// Arrange // Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper(); var helper = DefaultTemplatesUtilities.GetHtmlHelper();
@ -66,9 +66,9 @@ namespace Microsoft.AspNet.Mvc.Core
var nameForModelResult = helper.NameForModel(); var nameForModelResult = helper.NameForModel();
// Assert // Assert
Assert.Equal(prefix, idResult); Assert.Equal(expectedId, idResult);
Assert.Equal(prefix, idForResult); Assert.Equal(expectedId, idForResult);
Assert.Equal(prefix, idForModelResult); Assert.Equal(expectedId, idForModelResult);
Assert.Equal(prefix, nameResult); Assert.Equal(prefix, nameResult);
Assert.Equal(prefix, nameForResult); Assert.Equal(prefix, nameForResult);
Assert.Equal(prefix, nameForModelResult); Assert.Equal(prefix, nameForModelResult);
@ -94,16 +94,17 @@ namespace Microsoft.AspNet.Mvc.Core
} }
[Theory] [Theory]
[InlineData(null, "Property1")] [InlineData(null, "Property1", "Property1")]
[InlineData("", "Property1")] [InlineData("", "Property1", "Property1")]
[InlineData("A", "A.Property1")] [InlineData("A", "A.Property1", "A_Property1")]
[InlineData("A[23]", "A[23].Property1")] [InlineData("A[23]", "A[23].Property1", "A_23__Property1")]
[InlineData("A[0].B", "A[0].B.Property1")] [InlineData("A[0].B", "A[0].B.Property1", "A_0__B_Property1")]
[InlineData("A.B.C.D", "A.B.C.D.Property1")] [InlineData("A.B.C.D", "A.B.C.D.Property1", "A_B_C_D_Property1")]
public void IdAndNameHelpers_ReturnPrefixAndPropertyName(string prefix, string expectedResult) public void IdAndNameHelpers_ReturnPrefixAndPropertyName(string prefix, string expectedName, string expectedId)
{ {
// Arrange // Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper(); var helper = DefaultTemplatesUtilities.GetHtmlHelper();
helper.ViewData.TemplateInfo.HtmlFieldPrefix = prefix;
// Act // Act
var idResult = helper.Id("Property1"); var idResult = helper.Id("Property1");
@ -112,10 +113,10 @@ namespace Microsoft.AspNet.Mvc.Core
var nameForResult = helper.NameFor(m => m.Property1); var nameForResult = helper.NameFor(m => m.Property1);
// Assert // Assert
Assert.Equal("Property1", idResult); Assert.Equal(expectedId, idResult);
Assert.Equal("Property1", idForResult); Assert.Equal(expectedId, idForResult);
Assert.Equal("Property1", nameResult); Assert.Equal(expectedName, nameResult);
Assert.Equal("Property1", nameForResult); Assert.Equal(expectedName, nameForResult);
} }
[Fact] [Fact]
@ -131,8 +132,8 @@ namespace Microsoft.AspNet.Mvc.Core
var nameForResult = helper.NameFor(m => m.Inner.Id); var nameForResult = helper.NameFor(m => m.Inner.Id);
// Assert // Assert
Assert.Equal("Inner.Id", idResult); Assert.Equal("Inner_Id", idResult);
Assert.Equal("Inner.Id", idForResult); Assert.Equal("Inner_Id", idForResult);
Assert.Equal("Inner.Id", nameResult); Assert.Equal("Inner.Id", nameResult);
Assert.Equal("Inner.Id", nameForResult); Assert.Equal("Inner.Id", nameForResult);
} }
@ -194,10 +195,10 @@ namespace Microsoft.AspNet.Mvc.Core
} }
[Theory] [Theory]
[InlineData("A")] [InlineData("A", "A")]
[InlineData("A[0].B")] [InlineData("A[0].B", "A_0__B")]
[InlineData("A.B.C.D")] [InlineData("A.B.C.D", "A_B_C_D")]
public void IdAndName_ReturnExpression_EvenIfExpressionNotFound(string expression) public void IdAndName_ReturnExpression_EvenIfExpressionNotFound(string expression, string expectedId)
{ {
// Arrange // Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper(); var helper = DefaultTemplatesUtilities.GetHtmlHelper();
@ -207,7 +208,7 @@ namespace Microsoft.AspNet.Mvc.Core
var nameResult = helper.Name(expression); var nameResult = helper.Name(expression);
// Assert // Assert
Assert.Equal(expression, idResult); Assert.Equal(expectedId, idResult);
Assert.Equal(expression, nameResult); Assert.Equal(expression, nameResult);
} }

View File

@ -14,8 +14,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
public class InputTagHelperTest public class InputTagHelperTest
{ {
// Model (List<Model> or Model instance), container type (Model or NestModel), model accessor, // Model (List<Model> or Model instance), container type (Model or NestModel), model accessor,
// property path, expected value. // property path / id, expected value.
public static TheoryData<object, Type, Func<object>, string, string> TestDataSet public static TheoryData<object, Type, Func<object>, NameAndId, string> TestDataSet
{ {
get get
{ {
@ -41,32 +41,32 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
modelWithText, modelWithText,
}; };
return new TheoryData<object, Type, Func<object>, string, string> return new TheoryData<object, Type, Func<object>, NameAndId, string>
{ {
{ null, typeof(Model), () => null, "Text", { null, typeof(Model), () => null, new NameAndId("Text", "Text"),
string.Empty }, string.Empty },
{ modelWithNull, typeof(Model), () => modelWithNull.Text, "Text", { modelWithNull, typeof(Model), () => modelWithNull.Text, new NameAndId("Text", "Text"),
string.Empty }, string.Empty },
{ modelWithText, typeof(Model), () => modelWithText.Text, "Text", { modelWithText, typeof(Model), () => modelWithText.Text, new NameAndId("Text", "Text"),
"outer text" }, "outer text" },
{ modelWithNull, typeof(NestedModel), () => modelWithNull.NestedModel.Text, "NestedModel.Text", { modelWithNull, typeof(NestedModel), () => modelWithNull.NestedModel.Text,
string.Empty }, new NameAndId("NestedModel.Text", "NestedModel_Text"), string.Empty },
{ modelWithText, typeof(NestedModel), () => modelWithText.NestedModel.Text, "NestedModel.Text", { modelWithText, typeof(NestedModel), () => modelWithText.NestedModel.Text,
"inner text" }, new NameAndId("NestedModel.Text", "NestedModel_Text"), "inner text" },
// Top-level indexing does not work end-to-end due to code generation issue #1345. // Top-level indexing does not work end-to-end due to code generation issue #1345.
// TODO: Remove above comment when #1345 is fixed. // TODO: Remove above comment when #1345 is fixed.
{ models, typeof(Model), () => models[0].Text, "[0].Text", { models, typeof(Model), () => models[0].Text,
string.Empty }, new NameAndId("[0].Text", "z0__Text"), string.Empty },
{ models, typeof(Model), () => models[1].Text, "[1].Text", { models, typeof(Model), () => models[1].Text,
"outer text" }, new NameAndId("[1].Text", "z1__Text"), "outer text" },
{ models, typeof(NestedModel), () => models[0].NestedModel.Text, "[0].NestedModel.Text", { models, typeof(NestedModel), () => models[0].NestedModel.Text,
string.Empty }, new NameAndId("[0].NestedModel.Text", "z0__NestedModel_Text"), string.Empty },
{ models, typeof(NestedModel), () => models[1].NestedModel.Text, "[1].NestedModel.Text", { models, typeof(NestedModel), () => models[1].NestedModel.Text,
"inner text" }, new NameAndId("[1].NestedModel.Text", "z1__NestedModel_Text"), "inner text" },
}; };
} }
} }
@ -77,7 +77,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
object model, object model,
Type containerType, Type containerType,
Func<object> modelAccessor, Func<object> modelAccessor,
string propertyPath, NameAndId nameAndId,
string expectedValue) string expectedValue)
{ {
// Arrange // Arrange
@ -85,8 +85,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{ {
{ "class", "form-control" }, { "class", "form-control" },
{ "type", "text" }, { "type", "text" },
{ "id", propertyPath }, { "id", nameAndId.Id },
{ "name", propertyPath }, { "name", nameAndId.Name },
{ "valid", "from validation attributes" }, { "valid", "from validation attributes" },
{ "value", expectedValue }, { "value", expectedValue },
}; };
@ -97,7 +97,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
// Property name is either nameof(Model.Text) or nameof(NestedModel.Text). // Property name is either nameof(Model.Text) or nameof(NestedModel.Text).
var metadata = metadataProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName: "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<string, object>()); var tagHelperContext = new TagHelperContext(new Dictionary<string, object>());
var htmlAttributes = new Dictionary<string, string> var htmlAttributes = new Dictionary<string, string>
@ -185,6 +185,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.Equal(expectedTagName, output.TagName); 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 private class Model
{ {
public string Text { get; set; } public string Text { get; set; }

View File

@ -44,45 +44,45 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
return new TheoryData<object, Type, Func<object>, string, TagHelperOutputContent> return new TheoryData<object, Type, Func<object>, string, TagHelperOutputContent>
{ {
{ null, typeof(Model), () => null, "Text", { null, typeof(Model), () => null, "Text",
new TagHelperOutputContent(Environment.NewLine, "Text") }, new TagHelperOutputContent(Environment.NewLine, "Text", "Text") },
{ modelWithNull, typeof(Model), () => modelWithNull.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", { modelWithText, typeof(Model), () => modelWithText.Text, "Text",
new TagHelperOutputContent(Environment.NewLine, "Text") }, new TagHelperOutputContent(Environment.NewLine, "Text", "Text") },
{ modelWithText, typeof(Model), () => modelWithNull.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", { 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", { 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", { 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", { 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", { 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 // 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. // into ModelExpressions. Will be fixed in https://github.com/aspnet/Mvc/issues/1345.
{ models, typeof(Model), () => models[0].Text, "[0].Text", { 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", { 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", { 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", { 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", { 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", { 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", { 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", { 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<string, string> var expectedAttributes = new Dictionary<string, string>
{ {
{ "class", "form-control" }, { "class", "form-control" },
{ "for", propertyPath } { "for", tagHelperOutputContent.ExpectedId }
}; };
var metadataProvider = new DataAnnotationsModelMetadataProvider(); var metadataProvider = new DataAnnotationsModelMetadataProvider();
@ -173,14 +173,18 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
public class TagHelperOutputContent public class TagHelperOutputContent
{ {
public TagHelperOutputContent(string outputContent, string expectedContent) public TagHelperOutputContent(string outputContent, string expectedContent, string expectedId)
{ {
OriginalContent = outputContent; OriginalContent = outputContent;
ExpectedContent = expectedContent; ExpectedContent = expectedContent;
ExpectedId = expectedId;
} }
public string OriginalContent { get; set; } public string OriginalContent { get; set; }
public string ExpectedContent { get; set; } public string ExpectedContent { get; set; }
public string ExpectedId { get; set; }
} }
private class Model private class Model

View File

@ -14,8 +14,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
public class TextAreaTagHelperTest public class TextAreaTagHelperTest
{ {
// Model (List<Model> or Model instance), container type (Model or NestModel), model accessor, // Model (List<Model> or Model instance), container type (Model or NestModel), model accessor,
// property path, expected content. // property path / id, expected content.
public static TheoryData<object, Type, Func<object>, string, string> TestDataSet public static TheoryData<object, Type, Func<object>, NameAndId, string> TestDataSet
{ {
get get
{ {
@ -41,31 +41,40 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
modelWithText, modelWithText,
}; };
return new TheoryData<object, Type, Func<object>, string, string> return new TheoryData<object, Type, Func<object>, NameAndId, string>
{ {
{ null, typeof(Model), () => null, "Text", { null, typeof(Model), () => null,
new NameAndId("Text", "Text"),
Environment.NewLine }, Environment.NewLine },
{ modelWithNull, typeof(Model), () => modelWithNull.Text, "Text", { modelWithNull, typeof(Model), () => modelWithNull.Text,
new NameAndId("Text", "Text"),
Environment.NewLine }, Environment.NewLine },
{ modelWithText, typeof(Model), () => modelWithText.Text, "Text", { modelWithText, typeof(Model), () => modelWithText.Text,
new NameAndId("Text", "Text"),
Environment.NewLine + "outer 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 }, 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" }, Environment.NewLine + "inner text" },
// Top-level indexing does not work end-to-end due to code generation issue #1345. // Top-level indexing does not work end-to-end due to code generation issue #1345.
// TODO: Remove above comment when #1345 is fixed. // 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 }, 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" }, 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 }, 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" }, Environment.NewLine + "inner text" },
}; };
} }
@ -77,15 +86,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
object model, object model,
Type containerType, Type containerType,
Func<object> modelAccessor, Func<object> modelAccessor,
string propertyPath, NameAndId nameAndId,
string expectedContent) string expectedContent)
{ {
// Arrange // Arrange
var expectedAttributes = new Dictionary<string, string> var expectedAttributes = new Dictionary<string, string>
{ {
{ "class", "form-control" }, { "class", "form-control" },
{ "id", propertyPath }, { "id", nameAndId.Id },
{ "name", propertyPath }, { "name", nameAndId.Name },
{ "valid", "from validation attributes" }, { "valid", "from validation attributes" },
}; };
var expectedTagName = "textarea"; var expectedTagName = "textarea";
@ -94,7 +103,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
// Property name is either nameof(Model.Text) or nameof(NestedModel.Text). // Property name is either nameof(Model.Text) or nameof(NestedModel.Text).
var metadata = metadataProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName: "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 var tagHelper = new TextAreaTagHelper
{ {
For = modelExpression, For = modelExpression,
@ -178,6 +187,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.Equal(expectedTagName, output.TagName); 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 private class Model
{ {
public string Text { get; set; } public string Text { get; set; }