Change TagBuilder Attributes and HTML Helper dependencies to be case insensitive.

- This involved adding the StringComparer.OrdinalIgnoreCase comparer to the TagBuilder's Attributes dictionary.
- Added tests to validate that all methods that made use of TagBuilder.Attributes abide by the new ignore case mechanic.
- Added two sets of tests to validate the new functionality of Object => Dictionary HTML helper tests.
- Modified a functional test that utilizes HTML Helpers to provide same attribute-different case objects.
- Fixed existing HTML helper tests to account for new ordering of attrbutes (dictionary no longer adds key value pairs, it sets them).

#1328
This commit is contained in:
N. Taylor Mullen 2014-10-20 13:52:05 -07:00 committed by NTaylorMullen
parent 1e3828eb7d
commit 09af7bb77b
8 changed files with 243 additions and 21 deletions

View File

@ -53,7 +53,7 @@ namespace Microsoft.AspNet.Mvc
{
foreach (var helper in PropertyHelper.GetProperties(value))
{
dictionary.Add(helper.Name, helper.GetValue(value));
dictionary[helper.Name] = helper.GetValue(value);
}
}

View File

@ -155,7 +155,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
{
foreach (var helper in HtmlAttributePropertyHelper.GetProperties(htmlAttributes))
{
dictionary.Add(helper.Name, helper.GetValue(htmlAttributes));
dictionary[helper.Name] = helper.GetValue(htmlAttributes);
}
}

View File

@ -22,7 +22,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
}
TagName = tagName;
Attributes = new SortedDictionary<string, string>(StringComparer.Ordinal);
Attributes = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
public IDictionary<string, string> Attributes { get; private set; }
@ -110,7 +110,8 @@ namespace Microsoft.AspNet.Mvc.Rendering
foreach (var attribute in Attributes)
{
var key = attribute.Key;
if (string.Equals(key, "id", StringComparison.Ordinal) && string.IsNullOrEmpty(attribute.Value))
if (string.Equals(key, "id", StringComparison.OrdinalIgnoreCase) &&
string.IsNullOrEmpty(attribute.Value))
{
continue;
}

View File

@ -0,0 +1,58 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Xunit;
namespace Microsoft.AspNet.Mvc.Internal
{
public class TypeHelperTest
{
public static TheoryData<object, KeyValuePair<string, object>> IgnoreCaseTestData
{
get
{
return new TheoryData<object, KeyValuePair<string, object>>
{
{
new
{
selected = true,
SeLeCtEd = false
},
new KeyValuePair<string, object>("selected", false)
},
{
new
{
SeLeCtEd = false,
selected = true
},
new KeyValuePair<string, object>("SeLeCtEd", true)
},
{
new
{
SelECTeD = false,
SeLECTED = true
},
new KeyValuePair<string, object>("SelECTeD", true)
}
};
}
}
[Theory]
[MemberData(nameof(IgnoreCaseTestData))]
public void ObjectToDictionary_IgnoresPropertyCase(object testObject,
KeyValuePair<string, object> expectedEntry)
{
// Act
var result = TypeHelper.ObjectToDictionary(testObject);
// Assert
var entry = Assert.Single(result);
Assert.Equal(expectedEntry, entry);
}
}
}

View File

@ -131,9 +131,9 @@ namespace Microsoft.AspNet.Mvc.Rendering
public void CheckBoxReplacesUnderscoresInHtmlAttributesWithDashes()
{
// Arrange
var expected = @"<input Property1-Property3=""Property3ObjValue"" checked=""checked"" id=""Property1"" " +
@"name=""Property1"" type=""checkbox"" value=""true"" />" +
@"<input name=""Property1"" type=""hidden"" value=""false"" />";
var expected = @"<input checked=""checked"" id=""Property1"" name=""Property1"" " +
@"Property1-Property3=""Property3ObjValue"" type=""checkbox"" value=""true"" /><input " +
@"name=""Property1"" type=""hidden"" value=""false"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetTestModelViewData());
var htmlAttributes = new { Property1_Property3 = "Property3ObjValue" };
@ -148,8 +148,8 @@ namespace Microsoft.AspNet.Mvc.Rendering
public void CheckBoxWithPrefix_ReplaceDotsInIdByDefaultWithUnderscores()
{
// Arrange
var expected = @"<input Property3=""Property3Value"" id=""MyPrefix_Property1"" " +
@"name=""MyPrefix.Property1"" type=""checkbox"" value=""true"" /><input " +
var expected = @"<input id=""MyPrefix_Property1"" name=""MyPrefix.Property1"" " +
@"Property3=""Property3Value"" type=""checkbox"" value=""true"" /><input " +
@"name=""MyPrefix.Property1"" type=""hidden"" value=""false"" />";
var dictionary = new RouteValueDictionary(new { Property3 = "Property3Value" });
var helper = DefaultTemplatesUtilities.GetHtmlHelper();
@ -166,8 +166,8 @@ namespace Microsoft.AspNet.Mvc.Rendering
public void CheckBoxWithPrefix_ReplacesDotsInIdWithIdDotReplacement()
{
// Arrange
var expected = @"<input Property3=""Property3Value"" id=""MyPrefix!!!Property1"" " +
@"name=""MyPrefix.Property1"" type=""checkbox"" value=""true"" /><input " +
var expected = @"<input id=""MyPrefix!!!Property1"" name=""MyPrefix.Property1"" " +
@"Property3=""Property3Value"" type=""checkbox"" value=""true"" /><input " +
@"name=""MyPrefix.Property1"" type=""hidden"" value=""false"" />";
var dictionary = new Dictionary<string, object> { { "Property3", "Property3Value" } };
var helper = DefaultTemplatesUtilities.GetHtmlHelper();
@ -185,7 +185,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
public void CheckBoxWithPrefixAndEmptyName()
{
// Arrange
var expected = @"<input Property3=""Property3Value"" id=""MyPrefix"" name=""MyPrefix"" " +
var expected = @"<input id=""MyPrefix"" name=""MyPrefix"" Property3=""Property3Value"" " +
@"type=""checkbox"" value=""true"" /><input name=""MyPrefix"" type=""hidden"" " +
@"value=""false"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model: false);
@ -288,9 +288,9 @@ namespace Microsoft.AspNet.Mvc.Rendering
public void CheckBoxForWithObjectAttributeWithUnderscores()
{
// Arrange
var expected = @"<input Property1-Property3=""Property3ObjValue"" checked=""checked"" id=""Property1"" " +
@"name=""Property1"" type=""checkbox"" value=""true"" />" +
@"<input name=""Property1"" type=""hidden"" value=""false"" />";
var expected = @"<input checked=""checked"" id=""Property1"" name=""Property1"" " +
@"Property1-Property3=""Property3ObjValue"" type=""checkbox"" value=""true"" /><input " +
@"name=""Property1"" type=""hidden"" value=""false"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelperForViewData(GetTestModelViewData());
var htmlAttributes = new { Property1_Property3 = "Property3ObjValue" };
@ -305,9 +305,9 @@ namespace Microsoft.AspNet.Mvc.Rendering
public void CheckBoxForWithAttributeDictionary()
{
// Arrange
var expected = @"<input Property3=""Property3Value"" checked=""checked"" id=""Property1"" " +
@"name=""Property1"" type=""checkbox"" value=""true"" /><input name=""Property1"" " +
@"type=""hidden"" value=""false"" />";
var expected = @"<input checked=""checked"" id=""Property1"" name=""Property1"" " +
@"Property3=""Property3Value"" type=""checkbox"" value=""true"" /><input " +
@"name=""Property1"" type=""hidden"" value=""false"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelperForViewData(GetTestModelViewData());
var attributes = new Dictionary<string, object> { { "Property3", "Property3Value" } };
@ -322,7 +322,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
public void CheckBoxForWithPrefix()
{
// Arrange
var expected = @"<input Property3=""PropValue"" id=""MyPrefix_Property1"" name=""MyPrefix.Property1"" " +
var expected = @"<input id=""MyPrefix_Property1"" name=""MyPrefix.Property1"" Property3=""PropValue"" " +
@"type=""checkbox"" value=""true"" /><input name=""MyPrefix.Property1"" type=""hidden"" " +
@"value=""false"" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelperForViewData(GetTestModelViewData());

View File

@ -0,0 +1,59 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Rendering
{
public class HtmlHelperTest
{
public static TheoryData<object, KeyValuePair<string, object>> IgnoreCaseTestData
{
get
{
return new TheoryData<object, KeyValuePair<string, object>>
{
{
new
{
selected = true,
SeLeCtEd = false
},
new KeyValuePair<string, object>("selected", false)
},
{
new
{
SeLeCtEd = false,
selected = true
},
new KeyValuePair<string, object>("SeLeCtEd", true)
},
{
new
{
SelECTeD = false,
SeLECTED = true
},
new KeyValuePair<string, object>("SelECTeD", true)
}
};
}
}
[Theory]
[MemberData(nameof(IgnoreCaseTestData))]
public void AnonymousObjectToHtmlAttributes_IgnoresPropertyCase(object htmlAttributeObject,
KeyValuePair<string, object> expectedEntry)
{
// Act
var result = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributeObject);
// Assert
var entry = Assert.Single(result);
Assert.Equal(expectedEntry, entry);
}
}
}

View File

@ -0,0 +1,104 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNet.Mvc.Rendering;
using Xunit;
namespace Microsoft.AspNet.Mvc.Core.Rendering
{
public class TagBuilderTest
{
public static TheoryData<TagRenderMode, string> RenderingTestingData
{
get
{
return new TheoryData<TagRenderMode, string>
{
{ TagRenderMode.StartTag, "<p>" },
{ TagRenderMode.SelfClosing, "<p />" },
{ TagRenderMode.Normal, "<p></p>" }
};
}
}
[Theory]
[InlineData(false, "Hello", "World")]
[InlineData(true, "Hello", "something else")]
public void MergeAttribute_IgnoresCase(bool replaceExisting, string expectedKey, string expectedValue)
{
// Arrange
var tagBuilder = new TagBuilder("p");
tagBuilder.Attributes.Add("Hello", "World");
// Act
tagBuilder.MergeAttribute("hello", "something else", replaceExisting);
// Assert
var attribute = Assert.Single(tagBuilder.Attributes);
Assert.Equal(new KeyValuePair<string, string>(expectedKey, expectedValue), attribute);
}
[Fact]
public void AddCssClass_IgnoresCase()
{
// Arrange
var tagBuilder = new TagBuilder("p");
tagBuilder.Attributes.Add("ClaSs", "btn");
// Act
tagBuilder.AddCssClass("success");
// Assert
var attribute = Assert.Single(tagBuilder.Attributes);
Assert.Equal(new KeyValuePair<string, string>("ClaSs", "success btn"), attribute);
}
[Fact]
public void GenerateId_IgnoresCase()
{
// Arrange
var tagBuilder = new TagBuilder("p");
tagBuilder.Attributes.Add("ID", "something");
// Act
tagBuilder.GenerateId("else", idAttributeDotReplacement: "-");
// Assert
var attribute = Assert.Single(tagBuilder.Attributes);
Assert.Equal(new KeyValuePair<string, string>("ID", "something"), attribute);
}
[MemberData(nameof(RenderingTestingData))]
public void ToString_IgnoresIdAttributeCase(TagRenderMode renderingMode, string expectedOutput)
{
// Arrange
var tagBuilder = new TagBuilder("p");
// An empty value id attribute should not be rendered via ToString.
tagBuilder.Attributes.Add("ID", string.Empty);
// Act
var value = tagBuilder.ToString(renderingMode);
// Assert
Assert.Equal(expectedOutput, value);
}
[MemberData(nameof(RenderingTestingData))]
public void ToHtmlString_IgnoresIdAttributeCase(TagRenderMode renderingMode, string expectedOutput)
{
// Arrange
var tagBuilder = new TagBuilder("p");
// An empty value id attribute should not be rendered via ToHtmlString.
tagBuilder.Attributes.Add("ID", string.Empty);
// Act
var value = tagBuilder.ToHtmlString(renderingMode);
// Assert
Assert.Equal(expectedOutput, value.ToString());
}
}
}

View File

@ -15,9 +15,9 @@
<hr />
@Html.ValidationSummary(true)
<div class="form-group">
@Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" })
@Html.LabelFor(m => m.UserName, new { @class = "col-md-2", ClAsS = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.UserName, new { @class = "form-control" })
@Html.TextBoxFor(m => m.UserName, new { @class = "...", cLass = "form-control" })
@Html.ValidationMessageFor(m => m.UserName)
</div>
</div>