diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultHtmlGenerator.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultHtmlGenerator.cs index 51c4d30eeb..c77196895d 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultHtmlGenerator.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultHtmlGenerator.cs @@ -1059,12 +1059,20 @@ namespace Microsoft.AspNet.Mvc.Rendering values = values.Concat(enumValues); selectedValues = new HashSet(values, StringComparer.OrdinalIgnoreCase); + + // Perform deep copy of selectList to avoid changing user's Selected property values. var newSelectList = new List(); foreach (SelectListItem item in selectList) { - item.Selected = - (item.Value != null) ? selectedValues.Contains(item.Value) : selectedValues.Contains(item.Text); - newSelectList.Add(item); + var newItem = new SelectListItem + { + Disabled = item.Disabled, + Group = item.Group, + Selected = selectedValues.Contains(item.Value ?? item.Text), + Text = item.Text, + Value = item.Value, + }; + newSelectList.Add(newItem); } return newSelectList; diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperSelectTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperSelectTest.cs new file mode 100644 index 0000000000..ff75ae4d19 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperSelectTest.cs @@ -0,0 +1,532 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + public class HtmlHelperSelectTest + { + private static readonly SelectListGroup GroupOne = new SelectListGroup { Name = "Group One", }; + private static readonly SelectListGroup GroupTwo = new SelectListGroup { Name = "Group Two", }; + private static readonly SelectListGroup DisabledGroup = new SelectListGroup + { + Disabled = true, + Name = "Disabled Group", + }; + + private static readonly List BasicSelectList = new List + { + new SelectListItem { Text = "Zero", Value = "0"}, + new SelectListItem { Text = "One", Value = "1"}, + new SelectListItem { Text = "Two", Value = "2"}, + new SelectListItem { Text = "Three", Value = "3"}, + }; + private static readonly List SomeDisabledOneSelectedSelectList = new List + { + new SelectListItem { Disabled = false, Selected = false, Text = "Zero", Value = "0"}, + new SelectListItem { Disabled = true, Selected = true, Text = "One", Value = "1"}, + new SelectListItem { Disabled = false, Selected = false, Text = "Two", Value = "2"}, + new SelectListItem { Disabled = true, Selected = false, Text = "Three", Value = "3"}, + }; + private static readonly List SomeGroupedSomeSelectedSelectList = new List + { + new SelectListItem { Group = GroupOne, Selected = true, Text = "Zero", Value = "0"}, + new SelectListItem { Group = GroupTwo, Selected = false, Text = "One", Value = "1"}, + new SelectListItem { Group = GroupOne, Selected = true, Text = "Two", Value = "2"}, + new SelectListItem { Group = null, Selected = false, Text = "Three", Value = "3"}, + }; + private static readonly List OneGroupSomeSelectedSelectList = new List + { + new SelectListItem { Group = GroupOne, Selected = true, Text = "Zero", Value = "0"}, + new SelectListItem { Group = GroupOne, Selected = true, Text = "One", Value = "1"}, + new SelectListItem { Group = GroupOne, Selected = false, Text = "Two", Value = "2"}, + new SelectListItem { Group = GroupOne, Selected = false, Text = "Three", Value = "3"}, + }; + private static readonly List OneDisabledGroupAllSelectedSelectList = new List + { + new SelectListItem { Group = DisabledGroup, Selected = true, Text = "Zero", Value = "0"}, + new SelectListItem { Group = DisabledGroup, Selected = true, Text = "One", Value = "1"}, + new SelectListItem { Group = DisabledGroup, Selected = true, Text = "Two", Value = "2"}, + new SelectListItem { Group = DisabledGroup, Selected = true, Text = "Three", Value = "3"}, + }; + + // Select list -> expected HTML with null model, expected HTML with model containing "2". + public static TheoryData, string, string> DropDownListDataSet + { + get + { + return new TheoryData, string, string> + { + { + BasicSelectList, + "", + "" + }, + { + SomeDisabledOneSelectedSelectList, + "", + "" + }, + { + SomeGroupedSomeSelectedSelectList, + "", + "" + }, + { + OneGroupSomeSelectedSelectList, + "", + "" + }, + { + OneDisabledGroupAllSelectedSelectList, + "", + "" + }, + }; + } + } + + // Select list -> expected HTML with null model, with model containing "2", and with model containing "1", "3". + public static TheoryData, string, string, string> ListBoxDataSet + { + get + { + return new TheoryData, string, string, string> + { + { + BasicSelectList, + "", + "", + "" + }, + { + SomeDisabledOneSelectedSelectList, + "", + "", + "" + }, + { + SomeGroupedSomeSelectedSelectList, + "", + "", + "" + }, + { + OneGroupSomeSelectedSelectList, + "", + "", + "" + }, + { + OneDisabledGroupAllSelectedSelectList, + "", + "", + "" + }, + }; + } + } + + [Theory] + [MemberData(nameof(DropDownListDataSet))] + public void DropDownList_WithNullModel_GeneratesExpectedValue_DoesNotChangeSelectList( + IEnumerable selectList, + string expectedHtml, + string ignoredHtml) + { + // Arrange + var helper = DefaultTemplatesUtilities.GetHtmlHelper(); + var savedDisabled = selectList.Select(item => item.Disabled).ToList(); + var savedGroup = selectList.Select(item => item.Group).ToList(); + var savedSelected = selectList.Select(item => item.Selected).ToList(); + var savedText = selectList.Select(item => item.Text).ToList(); + var savedValue = selectList.Select(item => item.Value).ToList(); + + // Act + var html = helper.DropDownList("Property1", selectList, optionLabel: null, htmlAttributes: null); + + // Assert + Assert.Equal(expectedHtml, html.ToString()); + Assert.Equal(savedDisabled, selectList.Select(item => item.Disabled)); + Assert.Equal(savedGroup, selectList.Select(item => item.Group)); + Assert.Equal(savedSelected, selectList.Select(item => item.Selected)); + Assert.Equal(savedText, selectList.Select(item => item.Text)); + Assert.Equal(savedValue, selectList.Select(item => item.Value)); + } + + [Theory] + [MemberData(nameof(DropDownListDataSet))] + public void DropDownList_WithModelValue_GeneratesExpectedValue( + IEnumerable selectList, + string ignoredHtml, + string expectedHtml) + { + // Arrange + var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "2" }; + var helper = DefaultTemplatesUtilities.GetHtmlHelper(model); + var savedSelected = selectList.Select(item => item.Selected).ToList(); + + // Act + var html = helper.DropDownList("Property1", selectList, optionLabel: null, htmlAttributes: null); + + // Assert + Assert.Equal(expectedHtml, html.ToString()); + Assert.Equal(savedSelected, selectList.Select(item => item.Selected)); + } + + [Theory] + [MemberData(nameof(DropDownListDataSet))] + public void DropDownListFor_WithNullModel_GeneratesExpectedValue( + IEnumerable selectList, + string expectedHtml, + string ignoredHtml) + { + // Arrange + var helper = DefaultTemplatesUtilities.GetHtmlHelper(); + var savedSelected = selectList.Select(item => item.Selected).ToList(); + + // Act + var html = helper.DropDownListFor( + value => value.Property1, + selectList, + optionLabel: null, + htmlAttributes: null); + + // Assert + Assert.Equal(expectedHtml, html.ToString()); + Assert.Equal(savedSelected, selectList.Select(item => item.Selected)); + } + + [Theory] + [MemberData(nameof(DropDownListDataSet))] + public void DropDownListFor_WithModelValue_GeneratesExpectedValue( + IEnumerable selectList, + string ignoredHtml, + string expectedHtml) + { + // Arrange + var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "2" }; + var helper = DefaultTemplatesUtilities.GetHtmlHelper(model); + var savedSelected = selectList.Select(item => item.Selected).ToList(); + + // Act + var html = helper.DropDownListFor( + value => value.Property1, + selectList, + optionLabel: null, + htmlAttributes: null); + + // Assert + Assert.Equal(expectedHtml, html.ToString()); + Assert.Equal(savedSelected, selectList.Select(item => item.Selected)); + } + + [Theory] + [MemberData(nameof(ListBoxDataSet))] + public void ListBox_WithNullModel_GeneratesExpectedValue_DoesNotChangeSelectList( + IEnumerable selectList, + string expectedHtml, + string ignoredHtml1, + string ignoredHtml2) + { + // Arrange + var helper = DefaultTemplatesUtilities.GetHtmlHelper(model: null); + var savedDisabled = selectList.Select(item => item.Disabled).ToList(); + var savedGroup = selectList.Select(item => item.Group).ToList(); + var savedSelected = selectList.Select(item => item.Selected).ToList(); + var savedText = selectList.Select(item => item.Text).ToList(); + var savedValue = selectList.Select(item => item.Value).ToList(); + + // Act + var html = helper.ListBox("Property1", selectList, htmlAttributes: null); + + // Assert + Assert.Equal(expectedHtml, html.ToString()); + Assert.Equal(savedDisabled, selectList.Select(item => item.Disabled)); + Assert.Equal(savedGroup, selectList.Select(item => item.Group)); + Assert.Equal(savedSelected, selectList.Select(item => item.Selected)); + Assert.Equal(savedText, selectList.Select(item => item.Text)); + Assert.Equal(savedValue, selectList.Select(item => item.Value)); + } + + [Theory] + [MemberData(nameof(ListBoxDataSet))] + public void ListBox_WithModelValue_GeneratesExpectedValue( + IEnumerable selectList, + string ignoredHtml1, + string expectedHtml, + string ignoredHtml2) + { + // Arrange + var model = new ModelContainingList { Property1 = { "2" } }; + var helper = DefaultTemplatesUtilities.GetHtmlHelper(model); + var savedSelected = selectList.Select(item => item.Selected).ToList(); + + // Act + var html = helper.ListBox("Property1", selectList, htmlAttributes: null); + + // Assert + Assert.Equal(expectedHtml, html.ToString()); + Assert.Equal(savedSelected, selectList.Select(item => item.Selected)); + } + + [Theory] + [MemberData(nameof(ListBoxDataSet))] + public void ListBox_WithMultipleModelValues_GeneratesExpectedValue( + IEnumerable selectList, + string ignoredHtml1, + string ignoredHtml2, + string expectedHtml) + { + // Arrange + var model = new ModelContainingList { Property1 = { "1", "3" } }; + var helper = DefaultTemplatesUtilities.GetHtmlHelper(model); + var savedSelected = selectList.Select(item => item.Selected).ToList(); + + // Act + var html = helper.ListBox("Property1", selectList, htmlAttributes: null); + + // Assert + Assert.Equal(expectedHtml, html.ToString()); + Assert.Equal(savedSelected, selectList.Select(item => item.Selected)); + } + + [Theory] + [MemberData(nameof(ListBoxDataSet))] + public void ListBoxFor_WithNullModel_GeneratesExpectedValue( + IEnumerable selectList, + string expectedHtml, + string ignoredHtml1, + string ignoredHtml2) + { + // Arrange + var helper = DefaultTemplatesUtilities.GetHtmlHelper(model: null); + var savedSelected = selectList.Select(item => item.Selected).ToList(); + + // Act + var html = helper.ListBoxFor(value => value.Property1, selectList, htmlAttributes: null); + + // Assert + Assert.Equal(expectedHtml, html.ToString()); + Assert.Equal(savedSelected, selectList.Select(item => item.Selected)); + } + + [Theory] + [MemberData(nameof(ListBoxDataSet))] + public void ListBoxFor_WithModelValue_GeneratesExpectedValue( + IEnumerable selectList, + string ignoredHtml1, + string expectedHtml, + string ignoredHtml2) + { + // Arrange + var model = new ModelContainingList { Property1 = { "2" } }; + var helper = DefaultTemplatesUtilities.GetHtmlHelper(model); + var savedSelected = selectList.Select(item => item.Selected).ToList(); + + // Act + var html = helper.ListBoxFor(value => value.Property1, selectList, htmlAttributes: null); + + // Assert + Assert.Equal(expectedHtml, html.ToString()); + Assert.Equal(savedSelected, selectList.Select(item => item.Selected)); + } + + [Theory] + [MemberData(nameof(ListBoxDataSet))] + public void ListBoxFor_WithMultipleModelValues_GeneratesExpectedValue( + IEnumerable selectList, + string ignoredHtml1, + string ignoredHtml2, + string expectedHtml) + { + // Arrange + var model = new ModelContainingList { Property1 = { "1", "3" } }; + var helper = DefaultTemplatesUtilities.GetHtmlHelper(model); + var savedSelected = selectList.Select(item => item.Selected).ToList(); + + // Act + var html = helper.ListBoxFor(value => value.Property1, selectList, htmlAttributes: null); + + // Assert + Assert.Equal(expectedHtml, html.ToString()); + Assert.Equal(savedSelected, selectList.Select(item => item.Selected)); + } + + private class ModelContainingList + { + public List Property1 { get; } = new List(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Order.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Order.html index a9e3d25b38..6f80cb84a0 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Order.html +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Order.html @@ -19,7 +19,15 @@
- + + + +
+
+ + + diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.OrderUsingHtmlHelpers.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.OrderUsingHtmlHelpers.html index f617c0eb65..550a5fb8a7 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.OrderUsingHtmlHelpers.html +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.OrderUsingHtmlHelpers.html @@ -19,7 +19,15 @@
- + + + +
+
+ + + diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/SelectTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/SelectTagHelperTest.cs index cb14e38953..fd380a81e2 100644 --- a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/SelectTagHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/SelectTagHelperTest.cs @@ -243,7 +243,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers [Theory] [MemberData(nameof(GeneratesExpectedDataSet))] - public async Task ProcessAsync_GeneratesExpectedOutput_WithItems( + public async Task ProcessAsync_WithItems_GeneratesExpectedOutput_DoesNotChangeSelectList( object model, Type containerType, Func modelAccessor, @@ -286,7 +286,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers SelfClosing = true, }; - var items = new SelectList(new[] { "", "outer text", "inner text", "other text" }); var htmlGenerator = new TestableHtmlGenerator(metadataProvider) { ValidationAttributes = @@ -295,6 +294,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers } }; var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider); + + var items = new SelectList(new[] { "", "outer text", "inner text", "other text" }); + var savedDisabled = items.Select(item => item.Disabled).ToList(); + var savedGroup = items.Select(item => item.Group).ToList(); + var savedSelected = items.Select(item => item.Selected).ToList(); + var savedText = items.Select(item => item.Text).ToList(); + var savedValue = items.Select(item => item.Value).ToList(); var tagHelper = new SelectTagHelper { For = modelExpression, @@ -321,6 +327,12 @@ namespace Microsoft.AspNet.Mvc.TagHelpers Assert.NotNull(keyValuePair.Value); var selectedValues = Assert.IsAssignableFrom>(keyValuePair.Value); Assert.InRange(selectedValues.Count, 0, 1); + + Assert.Equal(savedDisabled, items.Select(item => item.Disabled)); + Assert.Equal(savedGroup, items.Select(item => item.Group)); + Assert.Equal(savedSelected, items.Select(item => item.Selected)); + Assert.Equal(savedText, items.Select(item => item.Text)); + Assert.Equal(savedValue, items.Select(item => item.Value)); } [Theory] diff --git a/test/WebSites/MvcTagHelpersWebSite/Controllers/MvcTagHelper_HomeController.cs b/test/WebSites/MvcTagHelpersWebSite/Controllers/MvcTagHelper_HomeController.cs index 5025db10aa..391e29270f 100644 --- a/test/WebSites/MvcTagHelpersWebSite/Controllers/MvcTagHelper_HomeController.cs +++ b/test/WebSites/MvcTagHelpersWebSite/Controllers/MvcTagHelper_HomeController.cs @@ -45,6 +45,7 @@ namespace MvcTagHelpersWebSite.Controllers }, NeedSpecialHandle = true, PaymentMethod = new List { "Check" }, + Products = new List { 0, 1 }, }; public MvcTagHelper_HomeController() diff --git a/test/WebSites/MvcTagHelpersWebSite/Models/Order.cs b/test/WebSites/MvcTagHelpersWebSite/Models/Order.cs index 67c17dad60..d0f348aa2b 100644 --- a/test/WebSites/MvcTagHelpersWebSite/Models/Order.cs +++ b/test/WebSites/MvcTagHelpersWebSite/Models/Order.cs @@ -44,6 +44,12 @@ namespace MvcTagHelpersWebSite.Models set; } + public IEnumerable SubstituteProducts + { + get; + set; + } + public Customer Customer { get; diff --git a/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Order.cshtml b/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Order.cshtml index a380d5d4e4..dbf3daf534 100644 --- a/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Order.cshtml +++ b/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Order.cshtml @@ -32,6 +32,11 @@ @{ var @object = "multiple"; } +
+
+ @Html.LabelFor(m => m.SubstituteProducts, htmlAttributes: new { @class = "order" }) + @* Use same select list as Products. Selection when Products is non-null is not used here. *@ + @Html.ListBoxFor(m => m.SubstituteProducts, productSelectList) +
@Html.LabelFor(m => m.OrderDate, htmlAttributes: new { @class = "order" }) @Html.TextBoxFor(m => m.OrderDate, format: "{0:yyyy/MM/dd/ g}", htmlAttributes: new { type = "datetime" })