ASP.NET MVC multiselect DropDownFor/listBoxFor [duplicate] - asp.net-mvc

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.
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.
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" };

Related

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.
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.
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" };

MVC View - Inconsistent model values

I've encountered what seems to be a strange inconsistency in the values of properties in my model.
I have the following controller action...
<Route("news/edit")>
<HttpGet>
Function EditIndex(Optional filter As Models.NewsFilter = Nothing) As ActionResult
filter.Start = 0
filter.Count = 100
Return View("EditIndex", filter)
End Function
Most of the properties in the model come from the querystring, but my controller explicitly setting "Start" and "Count", and then passing the model to the view.
My view is declared like so...
#inherits System.Web.Mvc.WebViewPage(Of Models.NewsFilter)
The odd thing is that if I simply display the models properties within the view like...
Start=#Model.Start
Count=#Model.Count
They display the values as defined in my controller. However when I use the following...
#Html.EditorFor(Function(m) m.Start, New With {.class = "form-control"})
#Html.EditorFor(Function(m) m.Count, New With {.class = "form-control"})
The values displayed are the original values from the QueryString. It's ignoring the values that I've set in my controller.
I don't understand why this happens. Is it trying to be clever and automatically checking the QueryString parameters because it's within a form?

Where does Html.DropDownList return the selected value to?

I am trying to use a dropdownlist in my mvc application, but am very confused by how it operates. It appears that you pass the list that appears in the dropdown, and a temporary description, but never tell it where you would like the selected value stored. Am I missing something or how do I retrieve a value from my drop-down list?
My drop-down list:
#Html.DropDownList("EmployeeNames", null, "Select an Employee", htmlAttributes: new { #class = "form-control" })
My current drop-down list appears correctly but I am unable to retain the selected value.
You tell where the value to be stored, the property EmployeeNames from the current Model.
You can take a look at MVC Model Binding. The first parameter of DropDownList is the name. The MVC bind the value by the form elements name.
The following
#Html.DropDownList("EmployeeNames", null, "Select an Employee", htmlAttributes: new { #class = "form-control" })
In HTML will look similar to this
<select id="EmployeeNames" name="EmployeeNames">
<!-- options -->
</select>
You notice name="EmployeeNames", that will help to bind to the property EmployeeNames from the curent Model.
Actually, it's pretty straightforward.. just grab the variable on the method call which handle the particular form action in your controller, if the name of your DropDownList is EmployeeNames , then your method should look like this
public ActionResult handleForm(string employeeNames)
{
return View();
}

Persisting Selected Value in MVC DropDownLists

I'm new to MVC, but I've been all over this, read all the documentation and all the questions and all the blog posts I can find, and all I'm doing is getting completely wrapped around the axle.
I'm trying to make a "create" Action and View. My data entry is relatively straight forward, and common: I have a drop down list and a text box. In my case, I'm creating a user contact channel, and the drop down box chooses between email and textmsg, and the text box then enters the relevant contact information, either a well formed email address, or a mobile phone number.
Here's a (slightly simplified form of) my View page:
<tr>
<td><%= Html.DropDownList("ChannelDescription", Model.ChannelDescription, "Select a Channel", new { id = "ChannelDDL", onchange="ChannelDDLChanged()" })%>
<br />
<%= Html.ValidationMessage("ChannelDescription", "Please Select a Channel") %>
</td>
<td>
<%= Html.TextBox("SubscriberNotificationAddr") %> <br />
<%= Html.ValidationMessage("SubscriberNotificationAddr", "Please enter a contact address or number") %>
</td>
</tr>
I'm using a strongly typed ViewData model, rather than using the ViewDataDictionary. The ChannelDescription element is a SelectList, which is initialized with the list of choices and no selection.
The initial display of the form, the data entry into the form, and the extraction of the data from the form by the controller goes fine.
My problem is if the data contains an error, such as a mal-formed email address or cell phone number, and I have to return to the view, I have not been successful in getting the drop down list selection redisplayed. The ChannelDescription element is recreated in the controller with the user's choice as the selected item. I have set breakpoints on that line of the View, and verified that the selected element of the list of items has the Selected property set to true, but it still displays the default "Select a Channel".
This seems like it would be a very common situation, and shouldn't be this hard. What am I doing wrong?
FYI, this is with MVC 1.0 (Release), Windows 7, and VS 2008, running under Firefox 3.5.2.
After viewing the answer above, I wanted to check it out, because all the examples I had seen had, indeed, used ViewDataDictionary, rather than a strongly typed ViewDataModel.
So I did some experiments. I constructed a very simple view that used a plain ViewDataDictionary, and passed values in by named keys. It persisted the selected item just fine. Then I cut and pasted that View (and controller) to another one, changing only what was necessary to switch to a strongly typed ViewData Model. Lo, and behold, it also persisted the selected item.
So what else was different between my simple test and my application? In my test, I had used simply "Html.DropDownList("name", "optionLabel")". However, in my application, I had needed to add HTML attributes, and the only overloads available that included HtmlAttributes also include the select List.
It turns out that the DropDownList overload with a select list parameter is broke! Looking at the downloaded MVC source code, when DropDownList is called with just a name, or a name and an optionLabel, it ends up retrieving the target select list from the ViewData, and then invoking the private SelectInternal method by the following call:
return SelectInternal(htmlHelper, optionLabel, name, selectList, true /* usedViewData */, false /* allowMultiple */, (IDictionary<string, object>)null /* htmlAttributes */);
However, if it's called with a selectList parameter, it ends up with the following:
return SelectInternal(htmlHelper, optionLabel, name, selectList, false /* usedViewData */, false /* allowMultiple */, htmlAttributes);
The difference is that in the first one (which will work correctly) the "usedViewData" parameter is true, while in the second one, it is false. Which is actually okay, but exposes an internal defect in the SelectInternal routine.
If usedViewData is false, it gets a object variable "defaultValue" from the ViewData model.
However, defaultValue is used as though it is either a string or an array of strings, when, in fact what is returned from the ViewData is a SelectList. (IEnumerable<SelectListItem>).
If usedViewData is true, then defaultValue will be either null or a string.
Then if defaultValue is not null, it ends up going into a block of code which contains this:
foreach (SelectListItem item in selectList) {
item.Selected = (item.Value != null) ? selectedValues.Contains(item.Value) : selectedValues.Contains(item.Text);
newSelectList.Add(item);
selectList is the original selectList that was passed in, so the item is a SelectListItem (string Text, string Value, and bool Selected). But selectedValues was derived from the defaultValue, and becomes a List of SelectLists, not a List of strings. So for each of the items, it's setting the Selected flag based on whether the selectedValues list "Contains" the item.Value. Well, a List of SelectLists is never going to "Contain" a string, so the item.Selected never gets set. (Correction: actually, after more tracing with the debugger, I found that selectedValues is derived from the defaultValue by a "ToString()" call. So it actually is a list of strings, but instead of containing the values that we want, it contains "System.Web.Mvc.SelectList" - the result of applying "ToString()" to complex object like a SelectList. The result is still the same - we're not going to find the value we're looking for in that list.)
It then substitutes the newly constructed "newSelectList" for the original "selectList", and proceeds to build the HTML from it.
As cagdas (I apologize for butchering your name, but I don't know how to make those characters on my US Keyboard) said above, I think I'll have to build my own method to use in place of the DropDownList HtmlHelper. I guess since this release 1 and Release2 is in Beta 2, we can't really expect any bug fixes unless we do it ourselves right?
BTW, if you've followed me this far, this code is in src\SystemWebMvc\Mvc\Html\SelectExtensions.cs, at around line 116-136
I had some discussions with Brad Wilson, from the MVC team, and he explained to me that I was misunderstanding how the DropDownList helper method should be used (a misunderstanding that I think might be fairly common, from what I've read).
Basically, EITHER give it the SelectList in the named parameter of the ViewModel, and let it build the drop down list from that with the appropriate items selected, OR give it the SelectList as a separate parameter and let the named parameter of the ViewModel be just the value strings for the selected item(s). If you give it a SelectList parameter, then it expects the named value to be a string or list of strings, NOT a SelectList.
So, now your ViewModel ends up having two elements for one conceptual item in the view (the dropdown list). Thus, you might have a model that has
string SelectedValue {get; set;}
SelectList DropDownElements { get; set;}
Then you can pre-populate the DropDownElements with the choices, but in your model view binding, you just need to deal with SelectedValue element. It seems to work pretty well for me when I do it that way.
Yes, I too had so many problems getting DropDownList to respect the selected item I've given to it.
Please check my answer in this question. As far as I can remember, that was the only way I could get it to work. By passing the list via ViewData.
FYI, I stopped using that HtmlHelper method. I'm now simply outputting the <select> and <option> tags myself with a loop and setting the selected property of the option tag by checking it myself.

HTML.DropDownList does not respect pre-selected value

I want to use Html.DropDownList(string name, IEnumerable SelectList, Object htmlAttributes) to render a select list with a preselected value for me.
My select list is in the model object of my view, so I have been writting the following code:
<%= Html.DropDownList("aName", mySelectList, new { }) %>
Which will render the select list without the pre-selected value.
A workaround I have found is passing the SelectList as ViewData and doing the following:
In the controller:
ViewData["TimeZones"] = mySelectList;
In the view:
<%= Html.DropDownList("TimeZones", null, new { }) %>
This way the select list will be rendered with the preselected value, however, I don't want to be forced to pass my select list as view data. What am I doing wrong? Thank you in advance for your help.
You can simply do this (it works):
<%: Html.DropDownList("DropdownName", new SelectList(ListItems, "Value", "Text", selectedItem), htmlAttributes)%>
Let me know in case this does not work for you.
Autobinding the selected item
The other approach is to automatically bind the selected item, and passing the list of items as parameter to the DropDownList helper method.
In the controller you do exactly the same thing as before, just don’t set to true the Selected property of any item. Then you also have to put into the view model the value of the item you want to select.
var model = new IndexViewModel()
{
ListItems = items,
SelectedItem = 2
};
And finally in the view, you use the overload which accepts also the selectList parameter:
<%= Html.DropDownList("SelectedItem", Model.ListItems) %>
The only difference in the HTML rendered is that, with first approach, the name of the HTML select element is ListItems, with the second it is SelectedItem.
These approaches are fine if you are creating your own list, but they are not the optimal solution if you are receiving the list of options for an external method, for example from a DB repository.
Take a look at this link
I know this is an old post, but I ran into a similar issue in MVC5. The solution is simple, but I struggled with it for a while, so I thought I'd share.
VS auto-generates DropDownLists with this in the view:
#Html.DropDownList("EpisodeTypeId", null, htmlAttributes: new { #class = "form-control" })
In the controller, VS auto-generates the ViewBag code differently for a Create vs. and Edit.
Here's the code from a create:
ViewBag.EpisodeTypeId = new SelectList(db.EpisodeTypes, "Id", "Name");
And from an edit:
ViewBag.EpisodeTypeId = new SelectList(db.EpisodeTypes, "Id", "Name", episode.EpisodeTypeId);
That fourth argument is important for Edits, as you might expect. If you leave it out, the database value for that record will not be pre-selected in the DropDown.
The VS auto-generated code will be correct, but if you're adding in fields manually later, this is easy to miss.

Resources