Reputation: 1053
I'm trying to create a dropdown list in MVC Core (2.2) for an Edit action.
I want to use an Enumerable of SelectListItems as my data source without adding an extra property to my ViewModel to hold the current selected value(s) as these are already stored in the SelectListItems.
I was using a customised SelectTagHelper (which removed the multiple attribute) which I thought might be the problem but I've tested with the standard SelectTagHelper and I can't find a way to capture the selected item(s) from the SelectListItems.
Is there a way to make the default SelectTagHelper work like this or do I need to extend the custom SelectTagHelper?
I've tried with a SelectList and an IEnumerable of SelectListItems and neither work the way I'd have thought would be possible.
When debugging, the items are returned correctly (with one having a selected value of true in the example below). I'm assuming there's something in the standard SelectTagHelper that prevents the selected item being parsed from the Items collection.
Code as follows:
Model (with 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; }
}
Controller:
[HttpGet]
public IActionResult Edit(int Id)
{
var model = _repo.GetTaskEdit(Id);
if (model != null)
{
return View(model);
}
return NotFound();
}
Repository:
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>
Debugger:
Generated 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>
Upvotes: 0
Views: 221
Reputation: 1053
I decided to extend the custom TagHelper I was using to cover this as I wanted to provide a single TagHelper to cover both Create and Edit instances of the Select input.
This custom TagHelper removes the multiple attribute and creates an option list with the selected value taken from the SelectListItems passed via the model.
TagHelper:
[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>");
}
}
}
}
Usage:
<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>
Renders as:
<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>
Upvotes: 1
Reputation: 387915
As I have already written in the comment, the select tag helper will use the asp-for
element to bind to the view model. This means that the value of the model in the specified property is used to match a value of the available items of the tag helper to determine which items is currently selected. This process works regardless of what SelectListItem
has its Selected
property set to true
.
That means, if you do not use asp-for
, then you can totally have this work as you like:
// 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>
If you execute this, this will be the rendered output:
<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>
It’s only if you add the asp-for
that this will stop working.
<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>
This is because now, the form is rendering a form control for a property that is a list of something. So the tag helper assumes that it has to display a select allowing multiple selections. In addition, the logic will now try to match the SelectListItem.Value
with the value of Model.ProjectId
and only use that to determine if something is selected.
As noted in the comments, this is not how you would usually use the select tag helper here. Instead, you would have a separate property for the selected value and a separate property for the available items:
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>
Now, this is the HTML you will get:
<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>
Note that item 3 is now implicitly selected even though its SelectListItem
did not have the Selected
property set. This is because the current value of ProjectId
in the model happens to equal the Value
of the SelectListItem
. And that’s exactly the logic that this uses.
This approach has a huge benefit over using the Selected
property in the SelectListItem
: Now, it’s very clear what data is being sent when you submit the form. Since the value of the selected option
is what is being sent, and the name
of the select
tag is the key that value is being used for, the submitting the form would effectively send ProjectId=value-3
now.
And when that model is then bound as part of the POST action in the controller, that value can correctly be deserialized into the ProjectId
property of your model:
[HttpPost]
public IActionResult Edit(TaskEditViewModel model)
{
var selectedProject = model.ProjectId; // "value-3"
// …
}
Upvotes: 1