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)]
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.
[Activate]
protected internal IHtmlGenerator Generator { get; set; }
@ -114,6 +124,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
// Ensure GenerateSelect() _never_ looks anything up in ViewData.
var items = Items ?? Enumerable.Empty<SelectListItem>();
ICollection<string> selectedValues;
var tagBuilder = Generator.GenerateSelect(
ViewContext,
For.Metadata,
@ -121,13 +132,18 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
name: For.Name,
selectList: items,
allowMultiple: allowMultiple,
htmlAttributes: null);
htmlAttributes: null,
selectedValues: out selectedValues);
if (tagBuilder != null)
{
output.SelfClosing = false;
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
// 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,
//// new NameAndId("[1].Text", "z1__Text"), outerSelected },
////{ models, typeof(NestedModel), () => models[1].NestedModel.Text,
@ -213,6 +213,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.Equal(expectedContent, output.Content);
Assert.False(output.SelfClosing);
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]
@ -278,6 +286,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.Equal(expectedContent, output.Content);
Assert.False(output.SelfClosing);
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]
@ -312,6 +328,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator.Object, metadataProvider);
ICollection<string> selectedValues = new string[0];
htmlGenerator
.Setup(real => real.GenerateSelect(
viewContext,
@ -320,7 +337,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
propertyName, // name
expectedItems,
expectedAllowMultiple,
null)) // htmlAttributes
null, // htmlAttributes
out selectedValues))
.Returns((TagBuilder)null)
.Verifiable();
@ -338,6 +356,12 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
// Assert
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]
@ -363,6 +387,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var htmlGenerator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator.Object, metadataProvider);
ICollection<string> selectedValues = new string[0];
htmlGenerator
.Setup(real => real.GenerateSelect(
viewContext,
@ -371,7 +396,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
propertyName, // name
It.IsAny<IEnumerable<SelectListItem>>(),
allowMultiple,
null)) // htmlAttributes
null, // htmlAttributes
out selectedValues))
.Returns((TagBuilder)null)
.Verifiable();
@ -387,6 +413,12 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
// Assert
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]