Provide selected values to `<option/>` tag helpers

- value may remain in the `FormContext` beyond `</select>` end tag but will
  be cleaned up at the `</form>` end tag of the containing `<form/>` element
 - `SelectTagHelper` called prior to helpers for contained `<option/>`s and
   not again later
- adjust mock setups to handle new `GenerateSelect()` call
- add assertions for expected `FormContext.FormData` entry

nit: mention #1468 in a test comment
This commit is contained in:
Doug Bunting 2014-11-05 14:21:58 -08:00
parent 3d84b528e5
commit 30f25fec99
2 changed files with 52 additions and 4 deletions

View File

@ -18,6 +18,16 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
[ContentBehavior(ContentBehavior.Append)] [ContentBehavior(ContentBehavior.Append)]
public class SelectTagHelper : TagHelper public class SelectTagHelper : TagHelper
{ {
/// <summary>
/// Key used for selected values in <see cref="FormContext.FormData"/>.
/// </summary>
/// <remarks>
/// Value for this dictionary entry will either be <c>null</c> (indicating no <see cref="SelectTagHelper"/> has
/// executed within this &lt;form/&gt;) or an <see cref="ICollection{string}"/> instance. Elements of the
/// collection are based on current <see cref="ViewDataDictionary.Model"/>.
/// </remarks>
public static readonly string SelectedValuesFormDataKey = nameof(SelectTagHelper) + "-SelectedValues";
// Protected to ensure subclasses are correctly activated. Internal for ease of use when testing. // Protected to ensure subclasses are correctly activated. Internal for ease of use when testing.
[Activate] [Activate]
protected internal IHtmlGenerator Generator { get; set; } protected internal IHtmlGenerator Generator { get; set; }
@ -114,6 +124,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
// Ensure GenerateSelect() _never_ looks anything up in ViewData. // Ensure GenerateSelect() _never_ looks anything up in ViewData.
var items = Items ?? Enumerable.Empty<SelectListItem>(); var items = Items ?? Enumerable.Empty<SelectListItem>();
ICollection<string> selectedValues;
var tagBuilder = Generator.GenerateSelect( var tagBuilder = Generator.GenerateSelect(
ViewContext, ViewContext,
For.Metadata, For.Metadata,
@ -121,13 +132,18 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
name: For.Name, name: For.Name,
selectList: items, selectList: items,
allowMultiple: allowMultiple, allowMultiple: allowMultiple,
htmlAttributes: null); htmlAttributes: null,
selectedValues: out selectedValues);
if (tagBuilder != null) if (tagBuilder != null)
{ {
output.SelfClosing = false; output.SelfClosing = false;
output.Merge(tagBuilder); output.Merge(tagBuilder);
} }
// Whether or not (not being highly unlikely) we generate anything, could update contained <option/>
// elements. Provide selected values for <option/> tag helpers. They'll run next.
ViewContext.FormContext.FormData[SelectedValuesFormDataKey] = selectedValues;
} }
} }
} }

View File

@ -79,7 +79,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
// Skip last two test cases because DefaultHtmlGenerator evaluates expression name against // Skip last two test cases because DefaultHtmlGenerator evaluates expression name against
// ViewData, not using ModelMetadata.Model. ViewData.Eval() handles simple property paths and some // ViewData, not using ModelMetadata.Model. ViewData.Eval() handles simple property paths and some
// dictionary lookups, but not indexing into an array or list. Will file a follow-up bug on this... // dictionary lookups, but not indexing into an array or list. See #1468...
////{ models, typeof(Model), () => models[1].Text, ////{ models, typeof(Model), () => models[1].Text,
//// new NameAndId("[1].Text", "z1__Text"), outerSelected }, //// new NameAndId("[1].Text", "z1__Text"), outerSelected },
////{ models, typeof(NestedModel), () => models[1].NestedModel.Text, ////{ models, typeof(NestedModel), () => models[1].NestedModel.Text,
@ -213,6 +213,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.Equal(expectedContent, output.Content); Assert.Equal(expectedContent, output.Content);
Assert.False(output.SelfClosing); Assert.False(output.SelfClosing);
Assert.Equal(expectedTagName, output.TagName); Assert.Equal(expectedTagName, output.TagName);
Assert.NotNull(viewContext.FormContext?.FormData);
var keyValuePair = Assert.Single(
viewContext.FormContext.FormData,
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
Assert.NotNull(keyValuePair.Value);
var selectedValues = Assert.IsAssignableFrom<ICollection<string>>(keyValuePair.Value);
Assert.InRange(selectedValues.Count, 0, 1);
} }
[Theory] [Theory]
@ -278,6 +286,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.Equal(expectedContent, output.Content); Assert.Equal(expectedContent, output.Content);
Assert.False(output.SelfClosing); Assert.False(output.SelfClosing);
Assert.Equal(expectedTagName, output.TagName); Assert.Equal(expectedTagName, output.TagName);
Assert.NotNull(viewContext.FormContext?.FormData);
var keyValuePair = Assert.Single(
viewContext.FormContext.FormData,
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
Assert.NotNull(keyValuePair.Value);
var selectedValues = Assert.IsAssignableFrom<ICollection<string>>(keyValuePair.Value);
Assert.InRange(selectedValues.Count, 0, 1);
} }
[Theory] [Theory]
@ -312,6 +328,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict); var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator.Object, metadataProvider); var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator.Object, metadataProvider);
ICollection<string> selectedValues = new string[0];
htmlGenerator htmlGenerator
.Setup(real => real.GenerateSelect( .Setup(real => real.GenerateSelect(
viewContext, viewContext,
@ -320,7 +337,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
propertyName, // name propertyName, // name
expectedItems, expectedItems,
expectedAllowMultiple, expectedAllowMultiple,
null)) // htmlAttributes null, // htmlAttributes
out selectedValues))
.Returns((TagBuilder)null) .Returns((TagBuilder)null)
.Verifiable(); .Verifiable();
@ -338,6 +356,12 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
// Assert // Assert
htmlGenerator.Verify(); htmlGenerator.Verify();
Assert.NotNull(viewContext.FormContext?.FormData);
var keyValuePair = Assert.Single(
viewContext.FormContext.FormData,
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
Assert.Same(selectedValues, keyValuePair.Value);
} }
[Theory] [Theory]
@ -363,6 +387,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict); var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator.Object, metadataProvider); var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator.Object, metadataProvider);
ICollection<string> selectedValues = new string[0];
htmlGenerator htmlGenerator
.Setup(real => real.GenerateSelect( .Setup(real => real.GenerateSelect(
viewContext, viewContext,
@ -371,7 +396,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
propertyName, // name propertyName, // name
It.IsAny<IEnumerable<SelectListItem>>(), It.IsAny<IEnumerable<SelectListItem>>(),
allowMultiple, allowMultiple,
null)) // htmlAttributes null, // htmlAttributes
out selectedValues))
.Returns((TagBuilder)null) .Returns((TagBuilder)null)
.Verifiable(); .Verifiable();
@ -387,6 +413,12 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
// Assert // Assert
htmlGenerator.Verify(); htmlGenerator.Verify();
Assert.NotNull(viewContext.FormContext?.FormData);
var keyValuePair = Assert.Single(
viewContext.FormContext.FormData,
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
Assert.Same(selectedValues, keyValuePair.Value);
} }
[Theory] [Theory]