Correct evaluation of expression result in `GenerateSelect()`

- #1468
- Always use `ModelExplorer` in `<select/>`, `DropDownListFor()` and `ListBoxFor()` cases
 - allows evaluation of more-complex expressions
- Use `ViewData.Model` in `DropDownList()` and `ListBox()` template cases
 - `ViewData` was previously ignored in these cases

nit: change `ViewDataDictionary.Eval()` to return `Model` if `expression` is `null` or empty
- now `throw` on `null` or empty `expression` name in `ViewDataEvaluator.Eval()`
- simplifies some of the higher-level code
 - no change to `selectList` fallback; `Model` incorrect for that case
 - no change to `GenerateRadioButton()`; would change behaviour unrelated to #1468
  - this helper uses incorrect `ViewData` lookup text, see #1487
This commit is contained in:
Doug Bunting 2015-03-01 21:06:47 -08:00
parent e447546e4e
commit ae4cafc002
7 changed files with 482 additions and 34 deletions

View File

@ -1,16 +1,23 @@
// 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.Reflection;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc.Rendering.Expressions
{
public static class ViewDataEvaluator
{
public static ViewDataInfo Eval([NotNull] ViewDataDictionary viewData, [NotNull] string expression)
public static ViewDataInfo Eval([NotNull] ViewDataDictionary viewData, string expression)
{
if (string.IsNullOrEmpty(expression))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(expression));
}
// Given an expression "one.two.three.four" we look up the following (pseudocode):
// this["one.two.three.four"]
// this["one.two.three"]["four"]
@ -24,8 +31,13 @@ namespace Microsoft.AspNet.Mvc.Rendering.Expressions
return EvalComplexExpression(viewData, expression);
}
public static ViewDataInfo Eval(object indexableObject, [NotNull] string expression)
public static ViewDataInfo Eval(object indexableObject, string expression)
{
if (string.IsNullOrEmpty(expression))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(expression));
}
// Run through same cases as other Eval() overload but allow a null container.
return (indexableObject == null) ? null : EvalComplexExpression(indexableObject, expression);
}

View File

@ -409,9 +409,9 @@ namespace Microsoft.AspNet.Mvc.Rendering
{
if (string.IsNullOrEmpty(expression))
{
// Avoid ViewData.Eval() throwing an ArgumentException with a different parameter name. Note this
// is an extreme case since users must pass a non-null selectList to use CheckBox() or ListBox()
// in a template, where a null or empty name has meaning.
// Do not call ViewData.Eval(); that would return ViewData.Model, which is not correct here.
// Note this case has a simple workaround: users must pass a non-null selectList to use
// DropDownList() or ListBox() in a template, where a null or empty name has meaning.
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(expression));
}
@ -422,16 +422,21 @@ namespace Microsoft.AspNet.Mvc.Rendering
var type = allowMultiple ? typeof(string[]) : typeof(string);
var defaultValue = GetModelStateValue(viewContext, fullName, type);
// If we haven't already used ViewData to get the entire list of items then we need to
// use the ViewData-supplied value before using the parameter-supplied value.
if (defaultValue == null && !string.IsNullOrEmpty(expression))
// If ModelState did not contain a current value, fall back to ViewData- or ModelExplorer-supplied value.
if (defaultValue == null)
{
if (!usedViewData)
if (modelExplorer == null)
{
defaultValue = viewContext.ViewData.Eval(expression);
// Html.DropDownList() and Html.ListBox() helper case.
// Cannot use ViewData if it contains the select list.
if (!usedViewData)
{
defaultValue = viewContext.ViewData.Eval(expression);
}
}
else if (modelExplorer != null)
else
{
// <select/>, Html.DropDownListFor() and Html.ListBoxFor() helper case. Do not use ViewData.
defaultValue = modelExplorer.Model;
}
}

View File

@ -971,16 +971,8 @@ namespace Microsoft.AspNet.Mvc.Rendering
}
else if (useViewData)
{
if (string.IsNullOrEmpty(expression))
{
// case 2(a): format the value from ViewData for the current model
resolvedValue = FormatValue(ViewData.Model, format);
}
else
{
// case 2(b): format the value from ViewData
resolvedValue = DefaultHtmlGenerator.EvalString(ViewContext, expression, format);
}
// case 2: format the value from ViewData
resolvedValue = DefaultHtmlGenerator.EvalString(ViewContext, expression, format);
}
else
{

View File

@ -333,7 +333,8 @@ namespace Microsoft.AspNet.Mvc
{
if (string.IsNullOrEmpty(expression))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, "expression");
// Null or empty expression name means current model.
return new ViewDataInfo(container: null, value: Model);
}
return ViewDataEvaluator.Eval(this, expression);

View File

@ -325,6 +325,26 @@ namespace Microsoft.AspNet.Mvc.Rendering
Assert.Equal(savedValue, selectList.Select(item => item.Value));
}
[Theory]
[MemberData(nameof(DropDownListDataSet))]
public void DropDownList_WithNullSelectList_GeneratesExpectedValue(
IEnumerable<SelectListItem> selectList,
string expectedHtml,
string ignoredHtml)
{
// Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper();
helper.ViewData["Property1"] = selectList;
var savedSelected = selectList.Select(item => item.Selected).ToList();
// Act
var html = helper.DropDownList("Property1", selectList: null, optionLabel: null, htmlAttributes: null);
// Assert
Assert.Equal(expectedHtml, html.ToString());
Assert.Equal(savedSelected, selectList.Select(item => item.Selected));
}
[Theory]
[MemberData(nameof(DropDownListDataSet))]
public void DropDownList_WithModelValue_GeneratesExpectedValue(
@ -368,6 +388,30 @@ namespace Microsoft.AspNet.Mvc.Rendering
Assert.Equal(savedSelected, selectList.Select(item => item.Selected));
}
[Theory]
[MemberData(nameof(DropDownListDataSet))]
public void DropDownListFor_WithNullModelAndNullSelectList_GeneratesExpectedValue(
IEnumerable<SelectListItem> selectList,
string expectedHtml,
string ignoredHtml)
{
// Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper();
helper.ViewData["Property1"] = selectList;
var savedSelected = selectList.Select(item => item.Selected).ToList();
// Act
var html = helper.DropDownListFor(
value => value.Property1,
selectList: null,
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(
@ -392,6 +436,82 @@ namespace Microsoft.AspNet.Mvc.Rendering
Assert.Equal(savedSelected, selectList.Select(item => item.Selected));
}
[Theory]
[MemberData(nameof(DropDownListDataSet))]
public void DropDownListFor_WithModelValueAndNullSelectList_GeneratesExpectedValue(
IEnumerable<SelectListItem> selectList,
string ignoredHtml,
string expectedHtml)
{
// Arrange
var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "2" };
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model);
helper.ViewData["Property1"] = selectList;
var savedSelected = selectList.Select(item => item.Selected).ToList();
// Act
var html = helper.DropDownListFor(
value => value.Property1,
selectList: null,
optionLabel: null,
htmlAttributes: null);
// Assert
Assert.Equal(expectedHtml, html.ToString());
Assert.Equal(savedSelected, selectList.Select(item => item.Selected));
}
[Fact]
public void DropDownListFor_WithIndexerExpression_GeneratesExpectedValue()
{
// Arrange
var model = new ModelContainingList { Property1 = { "0", "1", "2" } };
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model);
var selectList = SomeDisabledOneSelectedSelectList;
var savedSelected = selectList.Select(item => item.Selected).ToList();
var expectedHtml =
"<select id=\"Property1_2_\" name=\"Property1[2]\"><option value=\"0\">Zero</option>" +
Environment.NewLine +
"<option disabled=\"disabled\" value=\"1\">One</option>" + Environment.NewLine +
"<option selected=\"selected\" value=\"2\">Two</option>" + Environment.NewLine +
"<option disabled=\"disabled\" value=\"3\">Three</option>" + Environment.NewLine +
"</select>";
// Act
var html = helper.DropDownListFor(
value => value.Property1[2],
selectList,
optionLabel: null,
htmlAttributes: null);
// Assert
Assert.Equal(expectedHtml, html.ToString());
Assert.Equal(savedSelected, selectList.Select(item => item.Selected));
}
[Fact]
public void DropDownListFor_WithUnrelatedExpression_GeneratesExpectedValue()
{
// Arrange
var unrelated = "2";
var helper = DefaultTemplatesUtilities.GetHtmlHelper();
var selectList = SomeDisabledOneSelectedSelectList;
var savedSelected = selectList.Select(item => item.Selected).ToList();
var expectedHtml =
"<select id=\"unrelated\" name=\"unrelated\"><option value=\"0\">Zero</option>" + Environment.NewLine +
"<option disabled=\"disabled\" value=\"1\">One</option>" + Environment.NewLine +
"<option selected=\"selected\" value=\"2\">Two</option>" + Environment.NewLine +
"<option disabled=\"disabled\" value=\"3\">Three</option>" + Environment.NewLine +
"</select>";
// Act
var html = helper.DropDownListFor(value => unrelated, 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(
@ -503,6 +623,30 @@ namespace Microsoft.AspNet.Mvc.Rendering
Assert.Equal(savedSelected, selectList.Select(item => item.Selected));
}
[Fact]
public void ListBoxFor_WithUnreleatedExpression_GeneratesExpectedValue()
{
// Arrange
var unrelated = new[] { "2" };
var helper = DefaultTemplatesUtilities.GetHtmlHelper();
var selectList = SomeDisabledOneSelectedSelectList;
var savedSelected = selectList.Select(item => item.Selected).ToList();
var expectedHtml =
"<select id=\"unrelated\" multiple=\"multiple\" name=\"unrelated\"><option value=\"0\">Zero</option>" +
Environment.NewLine +
"<option disabled=\"disabled\" value=\"1\">One</option>" + Environment.NewLine +
"<option selected=\"selected\" value=\"2\">Two</option>" + Environment.NewLine +
"<option disabled=\"disabled\" value=\"3\">Three</option>" + Environment.NewLine +
"</select>";
// Act
var html = helper.ListBoxFor(value => unrelated, 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(
@ -524,6 +668,196 @@ namespace Microsoft.AspNet.Mvc.Rendering
Assert.Equal(savedSelected, selectList.Select(item => item.Selected));
}
[Theory]
[MemberData(nameof(DropDownListDataSet))]
public void DropDownListInTemplate_WithNullModel_GeneratesExpectedValue_DoesNotChangeSelectList(
IEnumerable<SelectListItem> selectList,
string expectedHtml,
string ignoredHtml)
{
// Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper<string>(model: null);
helper.ViewData.TemplateInfo.HtmlFieldPrefix = "Property1";
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(
expression: string.Empty,
selectList: 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 DropDownListInTemplate_WithModelValue_GeneratesExpectedValue(
IEnumerable<SelectListItem> selectList,
string ignoredHtml,
string expectedHtml)
{
// Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper("2");
helper.ViewData.TemplateInfo.HtmlFieldPrefix = "Property1";
var savedSelected = selectList.Select(item => item.Selected).ToList();
// Act
var html = helper.DropDownList(
expression: string.Empty,
selectList: 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 DropDownListForInTemplate_WithNullModel_GeneratesExpectedValue(
IEnumerable<SelectListItem> selectList,
string expectedHtml,
string ignoredHtml)
{
// Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper<string>(model: null);
helper.ViewData.TemplateInfo.HtmlFieldPrefix = "Property1";
var savedSelected = selectList.Select(item => item.Selected).ToList();
// Act
var html = helper.DropDownListFor(value => value, 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 DropDownListForInTemplate_WithModelValue_GeneratesExpectedValue(
IEnumerable<SelectListItem> selectList,
string ignoredHtml,
string expectedHtml)
{
// Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper("2");
helper.ViewData.TemplateInfo.HtmlFieldPrefix = "Property1";
var savedSelected = selectList.Select(item => item.Selected).ToList();
// Act
var html = helper.DropDownListFor(value => value, 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 ListBoxInTemplate_WithNullModel_GeneratesExpectedValue_DoesNotChangeSelectList(
IEnumerable<SelectListItem> selectList,
string expectedHtml,
string ignoredHtml1,
string ignoredHtml2)
{
// Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper<List<string>>(model: null);
helper.ViewData.TemplateInfo.HtmlFieldPrefix = "Property1";
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(expression: string.Empty, selectList: 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 ListBoxInTemplate_WithModelValue_GeneratesExpectedValue(
IEnumerable<SelectListItem> selectList,
string ignoredHtml1,
string expectedHtml,
string ignoredHtml2)
{
// Arrange
var model = new List<string> { "2" };
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model);
helper.ViewData.TemplateInfo.HtmlFieldPrefix = "Property1";
var savedSelected = selectList.Select(item => item.Selected).ToList();
// Act
var html = helper.ListBox(expression: string.Empty, selectList: selectList, htmlAttributes: null);
// Assert
Assert.Equal(expectedHtml, html.ToString());
Assert.Equal(savedSelected, selectList.Select(item => item.Selected));
}
[Theory]
[MemberData(nameof(ListBoxDataSet))]
public void ListBoxForInTemplate_WithNullModel_GeneratesExpectedValue(
IEnumerable<SelectListItem> selectList,
string expectedHtml,
string ignoredHtml1,
string ignoredHtml2)
{
// Arrange
var helper = DefaultTemplatesUtilities.GetHtmlHelper<List<string>>(model: null);
helper.ViewData.TemplateInfo.HtmlFieldPrefix = "Property1";
var savedSelected = selectList.Select(item => item.Selected).ToList();
// Act
var html = helper.ListBoxFor(value => value, selectList, htmlAttributes: null);
// Assert
Assert.Equal(expectedHtml, html.ToString());
Assert.Equal(savedSelected, selectList.Select(item => item.Selected));
}
[Theory]
[MemberData(nameof(ListBoxDataSet))]
public void ListBoxForInTemplate_WithModelValue_GeneratesExpectedValue(
IEnumerable<SelectListItem> selectList,
string ignoredHtml1,
string expectedHtml,
string ignoredHtml2)
{
// Arrange
var model = new List<string> { "2" };
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model);
helper.ViewData.TemplateInfo.HtmlFieldPrefix = "Property1";
var savedSelected = selectList.Select(item => item.Selected).ToList();
// Act
var html = helper.ListBoxFor(value => value, selectList, htmlAttributes: null);
// Assert
Assert.Equal(expectedHtml, html.ToString());
Assert.Equal(savedSelected, selectList.Select(item => item.Selected));
}
private class ModelContainingList
{
public List<string> Property1 { get; } = new List<string>();

View File

@ -6,7 +6,6 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Testing;
using Microsoft.Framework.Internal;
using Moq;
using Xunit;
@ -605,13 +604,18 @@ namespace Microsoft.AspNet.Mvc.Core
[Theory]
[InlineData(null)]
[InlineData("")]
public void EvalThrowsIfExpressionIsNullOrEmpty(string expression)
public void Eval_ReturnsModel_IfExpressionIsNullOrEmpty(string expression)
{
// Arrange
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider());
var model = new object();
viewData = new ViewDataDictionary(viewData, model);
// Act & Assert
ExceptionAssert.ThrowsArgumentNullOrEmpty(() => viewData.Eval(expression), "expression");
// Act
var result = viewData.Eval(expression);
// Assert
Assert.Same(model, result);
}
[Fact]

View File

@ -60,6 +60,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{
{ null, typeof(Model), () => null, new NameAndId("Text", "Text"), noneSelected },
// Imitate a temporary variable set from somewhere else in the view model.
{ null, typeof(Model), () => modelWithText.NestedModel.Text,
new NameAndId("item.Text", "item_Text"), innerSelected },
{ modelWithNull, typeof(Model), () => modelWithNull.Text,
new NameAndId("Text", "Text"), noneSelected },
{ modelWithText, typeof(Model), () => modelWithText.Text,
@ -75,14 +79,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{ models, typeof(NestedModel), () => models[0].NestedModel.Text,
new NameAndId("[0].NestedModel.Text", "z0__NestedModel_Text"), noneSelected },
// TODO: https://github.com/aspnet/Mvc/issues/1468
// 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. See #1468...
////{ models, typeof(Model), () => models[1].Text,
//// new NameAndId("[1].Text", "z1__Text"), outerSelected },
////{ models, typeof(NestedModel), () => models[1].NestedModel.Text,
//// new NameAndId("[1].NestedModel.Text", "z1__NestedModel_Text"), innerSelected },
{ models, typeof(Model), () => models[1].Text,
new NameAndId("[1].Text", "z1__Text"), outerSelected },
{ models, typeof(NestedModel), () => models[1].NestedModel.Text,
new NameAndId("[1].NestedModel.Text", "z1__NestedModel_Text"), innerSelected },
};
}
}
@ -344,6 +344,106 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.Equal(savedValue, items.Select(item => item.Value));
}
[Theory]
[MemberData(nameof(GeneratesExpectedDataSet))]
public async Task ProcessAsyncInTemplate_WithItems_GeneratesExpectedOutput_DoesNotChangeSelectList(
object model,
Type containerType,
Func<object> modelAccessor,
NameAndId nameAndId,
string expectedOptions)
{
// Arrange
var originalAttributes = new Dictionary<string, string>
{
{ "class", "form-control" },
};
var originalPostContent = "original content";
var expectedAttributes = new Dictionary<string, string>(originalAttributes)
{
{ "id", nameAndId.Id },
{ "name", nameAndId.Name },
{ "valid", "from validation attributes" },
};
var expectedPreContent = "original pre-content";
var expectedContent = "original content";
var expectedPostContent = originalPostContent + expectedOptions;
var expectedTagName = "select";
var metadataProvider = new DataAnnotationsModelMetadataProvider();
var containerMetadata = metadataProvider.GetMetadataForType(containerType);
var containerExplorer = metadataProvider.GetModelExplorerForType(containerType, model);
var propertyMetadata = metadataProvider.GetMetadataForProperty(containerType, "Text");
var modelExplorer = containerExplorer.GetExplorerForExpression(propertyMetadata, modelAccessor());
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
var tagHelperContext = new TagHelperContext(
allAttributes: new Dictionary<string, object>(),
items: new Dictionary<object, object>(),
uniqueId: "test",
getChildContentAsync: () => Task.FromResult("Something"));
var output = new TagHelperOutput(expectedTagName, originalAttributes)
{
PreContent = expectedPreContent,
Content = expectedContent,
PostContent = originalPostContent,
SelfClosing = true,
};
var htmlGenerator = new TestableHtmlGenerator(metadataProvider)
{
ValidationAttributes =
{
{ "valid", "from validation attributes" },
}
};
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
viewContext.ViewData.TemplateInfo.HtmlFieldPrefix = nameAndId.Name;
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,
Generator = htmlGenerator,
Items = items,
ViewContext = viewContext,
};
// Act
await tagHelper.ProcessAsync(tagHelperContext, output);
// Assert
Assert.True(output.SelfClosing);
Assert.Equal(expectedAttributes, output.Attributes);
Assert.Equal(expectedPreContent, output.PreContent);
Assert.Equal(expectedContent, output.Content);
Assert.Equal(expectedPostContent, output.PostContent);
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);
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]
[MemberData(nameof(ItemsAndMultipleDataSet))]
public async Task ProcessAsync_CallsGeneratorWithExpectedValues_ItemsAndAttribute(