SelectTagHelper 不显示模型中没有显式 ID 属性 的所选项目

SelectTagHelper not displaying selected item without explicit Id property in Model

我正在尝试在 MVC Core (2.2) 中为编辑操作创建下拉列表。

我想使用 SelectListItems 的 Enumerable 作为我的数据源,而不向我的 ViewModel 添加额外的 属性 来保存当前选择的值,因为这些值已经存储在 SelectListItems 中。

我使用的是自定义的 SelectTagHelper(删除了 multiple 属性),我认为这可能是问题所在,但我已经使用标准 SelectTagHelper 进行了测试,但我找不到捕获所选项目的方法来自 SelectListItems。

有没有办法让默认的 SelectTagHelper 像这样工作,或者我是否需要扩展自定义的 SelectTagHelper?

我尝试过使用 SelectList 和 SelectListItems 的 IEnumerable,但都没有按照我想象的方式工作。

调试时,项目正确返回(在下面的示例中有一个选择值为 true)。我假设标准 SelectTagHelper 中有一些东西阻止从 Items 集合中解析所选项目。

代码如下:

模型(带有 SelectListItems):

public class TaskEditViewModel
{
    public int Id { get; set; }
    public string Description { get; set; }
    public IEnumerable<SelectListItem> ProjectId { get; set; }
    public string Week { get; set; }
}

控制器:

[HttpGet]
public IActionResult Edit(int Id)
{
    var model = _repo.GetTaskEdit(Id);                       

    if (model != null)
    {
        return View(model);
    }

    return NotFound();            
}

存储库:

public TaskEditViewModel GetTaskEdit(int Id)
{
    var query = _context.Task.Where(t => t.Id == Id);

    var model = query
                .ProjectTo<TaskEditViewModel>(_mapper.ConfigurationProvider)
                .Single();

    if (model != null)
    {
        var selected = query.First().ProjectId; //gets currently selected value as int
        var list = _context.Project.Select(x => new SelectListItem(x.Name, x.Id.ToString(), true ? x.Id == selected : false));

        model.ProjectId = list;

        return model;
    }

    return null;
}

HTML:

    <div class="form-group">
        <label asp-for="ProjectId" class="control-label"></label>
        <selectOne asp-for="ProjectId" class="form-control" asp-items="Model.ProjectId"></selectOne>
        <span asp-validation-for="ProjectId" class="text-danger"></span>
    </div>

调试器:

生成HTML:

<div class="form-group">
   <label class="control-label" for="ProjectId">Project</label>
     <select class="form-control" data-val="true" data-val-required="Please select a project." id="ProjectId" multiple="multiple" name="ProjectId">
       <option value="1">7 Day SAT</option>
       <option value="2">UEC</option>
     </select>
   <span class="text-danger field-validation-valid" data-valmsg-for="ProjectId" data-valmsg-replace="true"></span>
</div>

正如我在评论中所写,select 标签助手将使用 asp-for 元素绑定到视图模型。这意味着指定 属性 中模型的值用于匹配标签助手的可用项目的值,以确定当前 selected 的项目。无论 SelectListItem 将其 Selected 属性 设置为 true.

,此过程都有效

也就是说,如果你不使用asp-for,那么你完全可以随心所欲地工作:

// in the controller action
return View(new TaskEditViewModel
{
    ProjectId = new List<SelectListItem>()
    {
        new SelectListItem { Text = "Item 1", Value = "value-1" },
        new SelectListItem { Text = "Item 2", Value = "value-2", Selected = false },
        new SelectListItem { Text = "Item 3", Value = "value-3" },
        new SelectListItem { Text = "Item 4", Value = "value-4" },
    },
});

// in the view
<select class="form-control" asp-items="Model.ProjectId"></select>

如果你执行这个,这将是渲染输出:

<select class="form-control">
    <option value="value-1">Item 1</option>
    <option value="value-2">Item 2</option>
    <option selected="selected" value="value-3">Item 3</option>
    <option value="value-4">Item 4</option>
</select>

只有添加 asp-for 才会停止工作。

<select class="form-control" id="ProjectId" multiple="multiple" name="ProjectId">
    <option value="value-1">Item 1</option>
    <option value="value-2">Item 2</option>
    <option value="value-3">Item 3</option>
    <option value="value-4">Item 4</option>
</select>

这是因为现在,表单正在呈现 属性 的表单控件,这是一个列表。所以标签助手假定它必须显示一个 select 允许 多个 select 离子。此外,该逻辑现在将尝试将 SelectListItem.ValueModel.ProjectId 的值相匹配,并且仅使用它来确定某些内容是否被 selected.

如评论中所述,这不是您通常在这里使用 select 标签助手的方式。相反,您将有一个单独的 属性 用于 selected 和一个单独的 属性 用于可用项目:

public class TaskEditViewModel
{
    // …
    public string ProjectId { get; set; }
    public IEnumerable<SelectListItem> AvailableProjects { get; set; }
}

// in the controller
return View(new TaskEditViewModel
{
    ProjectId = "value-3",
    AvailableProjects = new List<SelectListItem>()
    {
        new SelectListItem { Text = "Item 1", Value = "value-1" },
        new SelectListItem { Text = "Item 2", Value = "value-2" },
        new SelectListItem { Text = "Item 3", Value = "value-3" },
        new SelectListItem { Text = "Item 4", Value = "value-4" },
    },
});

// in the view
<select asp-for="ProjectId" class="form-control" asp-items="Model.AvailableProjects"></select>

现在,这是 HTML 你将得到:

<select class="form-control" id="ProjectId" name="ProjectId">
    <option value="value-1">Item 1</option>
    <option value="value-2">Item 2</option>
    <option selected="selected" value="value-3">Item 3</option>
    <option value="value-4">Item 4</option>
</select>

请注意,尽管第 3 项 SelectListItem 没有设置 Selected 属性,但现在已隐式 selected。这是因为模型中 ProjectId 的当前值恰好等于 SelectListItemValue。这正是它使用的逻辑。

与在 SelectListItem 中使用 Selected 属性 相比,这种方法有一个巨大的好处:现在,提交表单时发送的数据非常清楚。由于 selected option 的值是要发送的内容,而 select 标记的 name 是该值用于的键,因此提交表单现在将有效地发送 ProjectId=value-3

然后当该模型被绑定为控制器中 POST 操作的一部分时,该值可以正确地反序列化到模型的 ProjectId 属性 中:

[HttpPost]
public IActionResult Edit(TaskEditViewModel model)
{
    var selectedProject = model.ProjectId; // "value-3"
    // …
}

我决定扩展我使用的自定义 TagHelper 来解决这个问题,因为我想提供一个 TagHelper 来涵盖 Select 输入的创建和编辑实例。

此自定义 TagHelper 删除了 multiple 属性,并使用从通过模型传递的 SelectListItems 中获取的选定值创建了一个选项列表。

标签助手:

[HtmlTargetElement("selectOne", Attributes = "asp-for")]
    public class SingleSelectTagHelper : SelectTagHelper
    {
        public SingleSelectTagHelper(IHtmlGenerator generator)
            : base (generator)
        {

        }       

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            base.Process(context, output);

            output.TagName = "select";           

            var index = output.Attributes.IndexOfName("multiple");
            output.Attributes.RemoveAt(index);

            output.PreContent.AppendHtml("<option value=\"\">Please select an option</option>");          

            output.PostContent.Reinitialize();

            foreach(var item in Items)
            {
                if(item.Selected)
                {
                    output.PostContent.AppendHtml($"<option selected='selected' value=" + item.Value + ">" + item.Text + "</option>");
                }
                else
                {
                    output.PostContent.AppendHtml($"<option value=" + item.Value + ">" + item.Text + "</option>");
                }                
            }  
        }
    }

用法:

    <div class="form-group">
        <label asp-for="ProjectId" class="control-label"></label>
        <selectOne asp-for="ProjectId" class="form-control" asp-items="Model.ProjectId"></selectOne>
        <span asp-validation-for="ProjectId" class="text-danger"></span>
    </div>

呈现为:

        <div class="form-group">
            <label class="control-label" for="ProjectId">Project</label>
            <select class="form-control" data-val="true" data-val-required="Please select a project." id="ProjectId" name="ProjectId">
                <option value="">Please select an option</option>
                <option value=1>Option 1</option>
                <option selected='selected' value=2>Option 2</option>
            </select>
            <span class="text-danger field-validation-valid" data-valmsg-for="ProjectId" data-valmsg-replace="true"></span>
        </div>