Why does the DropDownListFor lose the multiple selection after Submit but the ListBoxFor doesn't?

I have read many articles about using MultiSelectList and have yet to understand what is going wrong with my DropDownListFor. I have a ListBoxFor with the same View, ViewModel and data that works fine. I want to use the DropDownListFor because of its optionLabel parameter that ListBoxFor doesn't have.

When the View is first loaded, both the DropDownListFor and the ListBoxFor show the multiple selected items.

Initial View

When the Submit button is clicked, the selected items collection is posted back to the Controller action okay and the view is refreshed with the ListBoxFor still showing both selected items but the DropDownListFor is only showing one selected item.

Refreshed View The controller action is constructing the MultiSelectList like this:

vm.TasksFilterGroup.Assignees = new MultiSelectList(employees, "Id", "FullName", new string[] { "51b6f06a-e04d-4f98-88ef-cd0cfa8a2757", "51b6f06a-e04d-4f98-88ef-cd0cfa8a2769" });

The View code looks like this:

<div class="form-group">
  <label>ListBoxFor</label>
  @Html.ListBoxFor(m => m.TasksFilterGroup.SelectedAssignees, Model.TasksFilterGroup.Assignees, new { @class = "form-control", multiple = "multiple" })
</div>
<div class="form-group">
  <label>DropDownListFor</label>
  @Html.DropDownListFor(m => m.TasksFilterGroup.SelectedAssignees, Model.TasksFilterGroup.Assignees, new { @class = "form-control", multiple = "multiple" })
</div>

Why does the DropDownListFor lose the multiple selection after Submit but the ListBoxFor doesn't?


As the names of the methods imply, DropDownListFor() is for creating a <select> (to select 1 option) and ListBoxFor() is for creating a <select multiple> (to select multiple options). While both methods share a lot of common code, they do produce different results.

Adding the multiple="multiple" attribute changes the display, but it does not change the functionality of the code executed by these methods.

If you inspect the source code, you will note that all the overloads of DropDownListFor() ultimately call the private static MvcHtmlString DropDownListHelper() method, and similarly ListBoxFor() ultimately calls the private static MvcHtmlString ListBoxHelper() method.

Both these methods call the private static MvcHtmlString SelectInternal() method, but the difference is that DropDownListHelper() passes allowMultiple = false while the ListBoxHelper() passes allowMultiple = true.

Within the SelectInternal() method, the key line of code is

object defaultValue = (allowMultiple) ? htmlHelper.GetModelStateValue(fullName, typeof(string[])) : htmlHelper.GetModelStateValue(fullName, typeof(string));

The value of defaultValue is then used when building html for the <option> elements and is used to set the selected attribute(s).

In the case of ListBoxFor(), the value of defaultValue will be the array defined by your SelectedAssignees property. In the case of DropDownListFor() it returns null because the value of your property cannot be cast to string (its an array).

Because defaultValue is null, none of the <option> elements have the selected attribute set and you lose model binding.

As a side note, if you were to set the values of SelectedAssignees in the GET method before you pass the model to the view, you will see that none of them are selected when using DropDownListFor() for the same reasons described above.

Note also that the code for generating the SelectList should just be

vm.TasksFilterGroup.Assignees = new SelectList(employees, "Id", "FullName" });

There is no point setting the 3rd parameter when using either the DropDownListFor() or ListBoxFor() methods because its the value of the property your binding to (SelectedAssignees) that determines which options are selected (the 3rd parameter is ignored by the methods). If you want the options matching those Guid values to be selected, then in the GET method, use

vm.TasksFilterGroup.SelectedAssignees= new string[]{ "51b6f06a-e04d-4f98-88ef-cd0cfa8a2757", "51b6f06a-e04d-4f98-88ef-cd0cfa8a2769" };