Transition `SelectTagHelper` and `OptionTagHelper` to use `context.Items`.

- Added functional tests to validate data created from a `SelectTagHelper` does not impact following `<select>` tags.
- Also moved the new `SelectTagHelper` communication flow into `TagHelper.Init`.

#3347
This commit is contained in:
N. Taylor Mullen 2015-10-21 16:20:05 -07:00
parent c267ef3904
commit 911dfc57b0
11 changed files with 86 additions and 66 deletions

View File

@ -54,8 +54,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <inheritdoc />
/// <remarks>
/// Does nothing unless <see cref="FormContext.FormData"/> contains a
/// <see cref="SelectTagHelper.SelectedValuesFormDataKey"/> entry and that entry is a non-empty
/// Does nothing unless <see cref="TagHelperContext.Items"/> contains a
/// <see cref="SelectTagHelper"/> <see cref="Type"/> entry and that entry is a non-empty
/// <see cref="ICollection{string}"/> instance. Also does nothing if the associated &lt;option&gt; is already
/// selected.
/// </remarks>
@ -82,9 +82,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{
// Is this <option/> element a child of a <select/> element the SelectTagHelper targeted?
object formDataEntry;
ViewContext.FormContext.FormData.TryGetValue(
SelectTagHelper.SelectedValuesFormDataKey,
out formDataEntry);
context.Items.TryGetValue(typeof(SelectTagHelper), out formDataEntry);
// ... And did the SelectTagHelper determine any selected values?
var selectedValues = formDataEntry as ICollection<string>;

View File

@ -27,6 +27,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <inheritdoc />
public override void Init(TagHelperContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// Push the new FormContext.
ViewContext.FormContext = new FormContext
{

View File

@ -21,16 +21,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{
private const string ForAttributeName = "asp-for";
private const string ItemsAttributeName = "asp-items";
/// <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";
private bool _allowMultiple;
private IReadOnlyCollection<string> _currentValues;
/// <summary>
/// Creates a new <see cref="SelectTagHelper"/>.
@ -69,6 +61,42 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
[HtmlAttributeName(ItemsAttributeName)]
public IEnumerable<SelectListItem> Items { get; set; }
/// <inheritdoc />
public override void Init(TagHelperContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// Note null or empty For.Name is allowed because TemplateInfo.HtmlFieldPrefix may be sufficient.
// IHtmlGenerator will enforce name requirements.
if (For.Metadata == null)
{
throw new InvalidOperationException(Resources.FormatTagHelpers_NoProvidedMetadata(
"<select>",
ForAttributeName,
nameof(IModelMetadataProvider),
For.Name));
}
// Base allowMultiple on the instance or declared type of the expression to avoid a
// "SelectExpressionNotEnumerable" InvalidOperationException during generation.
// Metadata.IsEnumerableType is similar but does not take runtime type into account.
var realModelType = For.ModelExplorer.ModelType;
_allowMultiple = typeof(string) != realModelType &&
typeof(IEnumerable).GetTypeInfo().IsAssignableFrom(realModelType.GetTypeInfo());
_currentValues = Generator.GetCurrentValues(
ViewContext,
For.ModelExplorer,
expression: For.Name,
allowMultiple: _allowMultiple);
// Whether or not (not being highly unlikely) we generate anything, could update contained <option/>
// elements. Provide selected values for <option/> tag helpers.
context.Items[typeof(SelectTagHelper)] = _currentValues;
}
/// <inheritdoc />
/// <remarks>Does nothing if <see cref="For"/> is <c>null</c>.</remarks>
/// <exception cref="InvalidOperationException">
@ -86,41 +114,17 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
throw new ArgumentNullException(nameof(output));
}
// Note null or empty For.Name is allowed because TemplateInfo.HtmlFieldPrefix may be sufficient.
// IHtmlGenerator will enforce name requirements.
var metadata = For.Metadata;
if (metadata == null)
{
throw new InvalidOperationException(Resources.FormatTagHelpers_NoProvidedMetadata(
"<select>",
ForAttributeName,
nameof(IModelMetadataProvider),
For.Name));
}
// Base allowMultiple on the instance or declared type of the expression to avoid a
// "SelectExpressionNotEnumerable" InvalidOperationException during generation.
// Metadata.IsEnumerableType is similar but does not take runtime type into account.
var realModelType = For.ModelExplorer.ModelType;
var allowMultiple = typeof(string) != realModelType &&
typeof(IEnumerable).GetTypeInfo().IsAssignableFrom(realModelType.GetTypeInfo());
// Ensure GenerateSelect() _never_ looks anything up in ViewData.
var items = Items ?? Enumerable.Empty<SelectListItem>();
var currentValues = Generator.GetCurrentValues(
ViewContext,
For.ModelExplorer,
expression: For.Name,
allowMultiple: allowMultiple);
var tagBuilder = Generator.GenerateSelect(
ViewContext,
For.ModelExplorer,
optionLabel: null,
expression: For.Name,
selectList: items,
currentValues: currentValues,
allowMultiple: allowMultiple,
currentValues: _currentValues,
allowMultiple: _allowMultiple,
htmlAttributes: null);
if (tagBuilder != null)
@ -128,10 +132,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
output.MergeAttributes(tagBuilder);
output.PostContent.Append(tagBuilder.InnerHtml);
}
// 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] = currentValues;
}
}
}

View File

@ -41,6 +41,9 @@
<option value="HtmlEncode[[Credit]]">Credit</option>
<option value="HtmlEncode[[Check]]" selected="HtmlEncode[[selected]]">Check</option>
</select>
<select>
<option value="HtmlEncode[[Check]]">Check</option>
</select>
</div>
<div>
<label class="order" for="HtmlEncode[[Customer_Number]]">HtmlEncode[[Number]]</label>

View File

@ -41,6 +41,9 @@
<option value="Credit">Credit</option>
<option value="Check" selected="selected">Check</option>
</select>
<select>
<option value="Check">Check</option>
</select>
</div>
<div>
<label class="order" for="Customer_Number">Number</label>

View File

@ -40,6 +40,9 @@
<select id="HtmlEncode[[PaymentMethod]]" multiple="HtmlEncode[[multiple]]" name="HtmlEncode[[PaymentMethod]]"><option value="HtmlEncode[[Credit]]">HtmlEncode[[Credit]]</option>
<option selected="HtmlEncode[[selected]]" value="HtmlEncode[[Check]]">HtmlEncode[[Check]]</option>
</select>
<select>
<option value="Check">Check</option>
</select>
</div>
<div>
<label class="HtmlEncode[[order]]" for="HtmlEncode[[Customer_Number]]">HtmlEncode[[Number]]</label>

View File

@ -40,6 +40,9 @@
<select id="PaymentMethod" multiple="multiple" name="PaymentMethod"><option value="Credit">Credit</option>
<option selected="selected" value="Check">Check</option>
</select>
<select>
<option value="Check">Check</option>
</select>
</div>
<div>
<label class="order" for="Customer_Number">Number</label>

View File

@ -12,7 +12,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{
public class OptionTagHelperTest
{
// Original content, selected attribute, value attribute, selected values (to place in FormContext.FormData)
// Original content, selected attribute, value attribute, selected values (to place in TagHelperContext.Items)
// and expected tag helper output.
public static TheoryData<string, string, string, IEnumerable<string>, TagHelperOutput> GeneratesExpectedDataSet
{
@ -346,7 +346,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
}
}
// Original content, selected attribute, value attribute, selected values (to place in FormContext.FormData)
// Original content, selected attribute, value attribute, selected values (to place in TagHelperContext.Items)
// and expected output (concatenation of TagHelperOutput generations). Excludes non-null selected attribute,
// null selected values, and empty selected values cases.
public static IEnumerable<object[]> DoesNotUseGeneratorDataSet
@ -358,7 +358,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
}
}
// Original content, selected attribute, value attribute, selected values (to place in FormContext.FormData)
// Original content, selected attribute, value attribute, selected values (to place in TagHelperContext.Items)
// and expected output (concatenation of TagHelperOutput generations). Excludes non-null selected attribute
// cases.
public static IEnumerable<object[]> DoesNotUseViewContextDataSet
@ -420,7 +420,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
model: null,
htmlGenerator: htmlGenerator,
metadataProvider: metadataProvider);
viewContext.FormContext.FormData[SelectTagHelper.SelectedValuesFormDataKey] = selectedValues;
tagHelperContext.Items[typeof(SelectTagHelper)] = selectedValues;
var tagHelper = new OptionTagHelper(htmlGenerator)
{
Value = value,
@ -491,7 +491,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
model: null,
htmlGenerator: htmlGenerator,
metadataProvider: metadataProvider);
viewContext.FormContext.FormData[SelectTagHelper.SelectedValuesFormDataKey] = selectedValues;
tagHelperContext.Items[typeof(SelectTagHelper)] = selectedValues;
var tagHelper = new OptionTagHelper(htmlGenerator)
{
Value = value,

View File

@ -235,6 +235,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
};
// Act
tagHelper.Init(tagHelperContext);
await tagHelper.ProcessAsync(tagHelperContext, output);
// Assert
@ -245,10 +246,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.Equal(expectedPostContent, output.PostContent.GetContent());
Assert.Equal(expectedTagName, output.TagName);
Assert.NotNull(viewContext.FormContext?.FormData);
Assert.Single(
viewContext.FormContext.FormData,
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
tagHelperContext.Items,
entry => (Type)entry.Key == typeof(SelectTagHelper));
}
[Theory]
@ -333,6 +333,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
};
// Act
tagHelper.Init(tagHelperContext);
await tagHelper.ProcessAsync(tagHelperContext, output);
// Assert
@ -343,10 +344,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.Equal(expectedPostContent, HtmlContentUtilities.HtmlContentToString(output.PostContent));
Assert.Equal(expectedTagName, output.TagName);
Assert.NotNull(viewContext.FormContext?.FormData);
Assert.Single(
viewContext.FormContext.FormData,
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
tagHelperContext.Items,
entry => (Type)entry.Key == typeof(SelectTagHelper));
Assert.Equal(savedDisabled, items.Select(item => item.Disabled));
Assert.Equal(savedGroup, items.Select(item => item.Group));
@ -429,7 +429,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
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(htmlGenerator)
{
For = modelExpression,
@ -438,6 +437,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
};
// Act
tagHelper.Init(tagHelperContext);
await tagHelper.ProcessAsync(tagHelperContext, output);
// Assert
@ -448,10 +448,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.Equal(expectedPostContent, HtmlContentUtilities.HtmlContentToString(output.PostContent));
Assert.Equal(expectedTagName, output.TagName);
Assert.NotNull(viewContext.FormContext?.FormData);
Assert.Single(
viewContext.FormContext.FormData,
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
tagHelperContext.Items,
entry => (Type)entry.Key == typeof(SelectTagHelper));
Assert.Equal(savedDisabled, items.Select(item => item.Disabled));
Assert.Equal(savedGroup, items.Select(item => item.Group));
@ -536,15 +535,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
};
// Act
tagHelper.Init(tagHelperContext);
await tagHelper.ProcessAsync(tagHelperContext, output);
// Assert
htmlGenerator.Verify();
Assert.NotNull(viewContext.FormContext?.FormData);
var keyValuePair = Assert.Single(
viewContext.FormContext.FormData,
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
tagHelperContext.Items,
entry => (Type)entry.Key == typeof(SelectTagHelper));
Assert.Same(currentValues, keyValuePair.Value);
}
@ -610,15 +609,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
};
// Act
tagHelper.Init(tagHelperContext);
await tagHelper.ProcessAsync(tagHelperContext, output);
// Assert
htmlGenerator.Verify();
Assert.NotNull(viewContext.FormContext?.FormData);
var keyValuePair = Assert.Single(
viewContext.FormContext.FormData,
entry => entry.Key == SelectTagHelper.SelectedValuesFormDataKey);
tagHelperContext.Items,
entry => (Type)entry.Key == typeof(SelectTagHelper));
Assert.Same(currentValues, keyValuePair.Value);
}

View File

@ -52,6 +52,9 @@
<option value="Credit">Credit</option>
<option value="Check">Check</option>
</select>
<select>
<option value="Check">Check</option>
</select>
</div>
<div>
<label asp-for="Customer.Number" class="order"></label>

View File

@ -43,6 +43,9 @@ Html.BeginForm(actionName: "Submit", controllerName: "HtmlGeneration_Order"))
<div>
@Html.LabelFor(m => m.PaymentMethod, htmlAttributes: new { @class = "order" })
@Html.ListBoxFor(m => m.PaymentMethod, selectList: new SelectList(new[] { new { value = "Credit" }, new { value = "Check" } }, dataValueField: "value", dataTextField: "value"))
<select>
<option value="Check">Check</option>
</select>
</div>
<div>
@Html.LabelFor(m => m.Customer.Number, htmlAttributes: new { @class = "order" })