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:
parent
f9e44ff7f9
commit
dd5da33a62
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue