0

See below for updated summary...

I understand that using the 'For' Html Helpers is preferred but I'm having a problem with DropDownListFor when using it as a multi-select.

This example (DropDownList) works perfectly:

@Html.DropDownList(
    "ProtocolDisciplines", 
    new MultiSelectList(Model.Disciplines, "DisciplineId", "Discipline", Model.ProtocolDisciplines.Select(pd => pd.DisciplineId)), 
    new { @class = "form-control", multiple = "multiple", size = "8" }
)

This example (DropDownListFor) works perfectly EXCEPT the default value(s) does not get set:

@Html.DropDownListFor(
    model => model.ProtocolDisciplines, 
    new MultiSelectList(Model.Disciplines, "DisciplineId", "Discipline", Model.ProtocolDisciplines.Select(pd => pd.DisciplineId)), 
    new { @class = "form-control", multiple = "multiple", size = "8" }
)

UPDATES Based on what I'm learning I've updated from the original post. Here is the code that is still not working. To be clear, it is doing everything perfectly EXCEPT it is not selecting the default value when rendered. In the example I'm working with there is only a single default value.

@Html.ListBoxFor(
    model => model.ProtocolDisciplines, 
    new MultiSelectList(Model.Disciplines, "DisciplineId", "Discipline", Model.ProtocolDisciplines), 
    new { @class = "form-control", size = "8" }
)

I've have made certain that Disciplines (the list of all 16 Disciplines in the db) and ProtocolDisciplines (the list of Disciplines that belong to the Protocol) are the same type (DisciplineViewModel). Further, that class (see below) contains only 2 properties (DisciplineId and Discipline).

I have a breakpoint where the model is returns to the view and I have verified that both Disciplines and ProtocolDisciplines have the values expected so I am currently focusing on the view and the ListBoxFor helper. As a note, I have also tried the exact same code with a DropDownListFor helper with identical behavior).

I suspect the problem is in the creation of the MultiSelectList. As you can see, I'm using the overload (IEnumerable ListItems, string DataValue, string DataText, IEnumerable SelectedValues). It would seem that the SelectedValues are simply not getting a match on anything in the ListValues but I can't figure out why. The types used in the two are the same, the DataValue and DataTypes names match the members of the types (just to be safe). I know the ListItems is correct because the list renders them correctly.

I'm at a loss.

Reference:

public partial class DisciplineViewModel
{
    public Guid DisciplineId { get; set; }

    public string Discipline { get; set; }
}

Here is the model:

public partial class ProtocolViewModelEdit
{
    [Key]
    public Guid ProtocolId { get; set; }

    [Display(Name = "Name")]
    public string Protocol { get; set; }

    public string ProtocolType { get; set; }

    [Display(Name = "Type")]
    public Guid ProtocolTypeId { get; set; }

    [Display(Name = "Status")]
    public Guid ProtocolStatusId { get; set; }

    public virtual ICollection<ProtocolTypeViewModel> ProtocolTypes { get; set; }

    public virtual ICollection<ProtocolStatusViewModel> ProtocolStatuses { get; set; }

    public virtual ICollection<DisciplineViewModel> ProtocolDisciplines { get; set; }

    public virtual ICollection<ProtocolXProgramViewModel> ProtocolPrograms { get; set; }

    public virtual ICollection<DisciplineViewModel> Disciplines { get; set; }

    public virtual ICollection<ProgramViewModel> Programs { get; set; }
}
Steven Frank
  • 551
  • 3
  • 16
  • Use `ListBoxFor` for multiselects. – Chris Pratt Feb 15 '17 at 16:28
  • Thanks for the response, but that yields the same issue. Everything looks great except the default value(s) is not selected. – Steven Frank Feb 15 '17 at 17:29
  • I just found this post from 4 years ago but it has recent responses, is it possible this is still a bug?? [link]https://social.msdn.microsoft.com/Forums/vstudio/en-US/05ee3b35-f3d3-48b4-83f5-ca3d9073624e/mvc-htmlhelper-listboxfor-and-listbox-multiselectlist-bug?forum=netfxbcl – Steven Frank Feb 15 '17 at 17:46
  • Actually, I'm really surprised by this forum post, as none of the issues discussed are bugs. It's just how things are supposed to work, and if you do things properly, you don't issues. I'll add an answer with additional information. – Chris Pratt Feb 15 '17 at 18:06
  • I have just rolled back your edit. You cannot completely change the question and invalid the comments and answers by other (especially when the answer is correct). If you want to add additional information, append it to your original question. –  Feb 15 '17 at 21:52
  • First refer [this answer](http://stackoverflow.com/questions/40725358/why-does-the-dropdownlistfor-lose-the-multiple-selection-after-submit-but-the-li/40732481#40732481) to explain why you must use `ListBoxFor()` not `DropDownListFor()` –  Feb 15 '17 at 21:53
  • You have not shown your model, but `ProtocolDisciplines` needs to be `IEnumerable` and if you set its value in the GET method before you pass the model to the view, and those values match exactly the values of the options, then they will be selected. –  Feb 15 '17 at 21:55
  • @StephenMuecke I've changed completely to a ListBoxFor - same behavior. – Steven Frank Feb 15 '17 at 22:03
  • @StephenMuecke Which answer is correct? Nothing posted here solves the problem. – Steven Frank Feb 15 '17 at 22:05
  • Show the new code you have tried. And your model and how your set the values in the controller so we can see what error you have made (append it to your question) –  Feb 15 '17 at 22:05
  • @StephenMuecke Nowhere in the docs does it say the IEnumerable for the SelectedValues has to be an int. That makes no sense to me at all. – Steven Frank Feb 15 '17 at 22:06
  • It needs to be `IEnumerable` - the purpose of a ` –  Feb 15 '17 at 22:08
  • `ProtocolDisciplines` is a collection of complex objects (a ` –  Feb 15 '17 at 22:24
  • @StephenMuecke Thank you! That was what I needed to hear. – Steven Frank Feb 15 '17 at 23:14

1 Answers1

1

You referred to a post on the MSDN forums wherein the OP describes the following:

1) The selectedValues parameter must be populated with a collection of key values only. It cannot be a collection of the selected objects, as the HtmlHelper does not apply the dataValueField to this collection for you.

2) If using the ListBox you cannot set the name parameter the same as a Model property. Also you cannot name the ViewBag property that will contain the collection of items the same as the Model property.

3) If using the ListBoxFor it gets even more wonky. You should name the ViewBag property that will contain the collection of items the same as the Model property name. Now when you use the ListBoxFor within the View you must use a ViewBag property that does not exist (this is important!). The HtmlHelper.ListBoxFor will look automatically for a ViewBag property with the assigned Model property name.

None of these are actual issues. A SelectList ultimately has to be translated to/from an HTML select element, which can only work with simple types (string, int, etc.). Actually, everything is a string, and it's only the work of the model binder that translates the posted values into more specific types like int. As a result, it's obvious why you cannot bind a list of objects.

The other two mentioned issues are a result of ModelState. The values of bound form fields are determined by what's in ModelState, which is composed of values from Request, ViewData/ViewBag, and finally Model, as a last resort. If you set a SelectList in ViewBag with the same name as a property on Model, then the value for that key in ModelState will be that SelectList rather than the actual selected values, and your select will therefore have no selected items, because none of the option values will of course match that SelectList instance. Again, this is just standard behavior, and it's only a "bug" if you're not aware of how things work, and don't realize the implications of what you're doing.

Your issue here is exactly the first problem. You're passing a list of objects as the selected values, and there's simply no way to bind that properly to an HTML select element. However, things are far easier if you don't even bother to create your own MultiSelectList anyways. All the helper needs is IEnumerable<SelectListItem>. Razor will take care of creating a SelectList/MultiSelectList and setting the appropriate selected values. Just do:

@Html.ListBoxFor(
    m => m.ProtocolDisciplines,
    Model.Disciplines.Select(d => new SelectListItem { Value = d.DisciplineId.ToString(), Text = d.Discipline }),
    new { @class = "form-control", size = 8 }
)

UPDATE

To answer you question about how Razor "knows", like I said in my answer, the info comes from ModelState. However, as pointed out by Stephen in the comments below, the property you're binding this to is a collection of objects. That's never going to work. Again, the posted values from an HTML select element will always be simple types, not objects. As a result, you need a property that the model binder can bind the posted data to, and then you need to use that information to lookup the actual objects you need, before finally setting something like your ProtocolDisciplines property. In other words:

public List<int> SelectedProtocolDisciplines { get; set; }

public IEnumerable<SelectListItem> DisciplineOptions { get; set; }

Since you're using a view model, it's better if you include the select list items on that view model, so I added a property for that. In your actions (GET and POST), you'll need to set this property:

model.DisciplineOptions = model.Disciplines..Select(d => new SelectListItem { 
    Value = d.DisciplineId.ToString(),
    Text = d.Discipline
});

Since you'll need to call that in both the GET and POST actions, you might want to factor it out into a private method on your controller that both can call. Then, in your view:

@Html.ListBoxFor(m => m.SelectedProtocolDisciplines, Model.DisciplineOptions, new { @class = "form-control" })

Finally, in your POST action:

var protocolDisciplines = db.Disciplines.Where(m => model.SelectedProtocolDisciplines.Contains(m.DisciplineId));

Then, if this is a "create" method, you can simply set the appropriate property on your entity with that. If you're editing an existing entity, you'll need to do a little bit more work:

// Remove deselected disciplines
entity.ProtocolDisciplines
    .Where(m => !model.SelectedProtocolDisciplines.Contains(m.DisciplineId))
    .ToList()
    .ForEach(m => entity.ProtocolDisciplines.Remove(m));

// Add new selected disciplines
var addedDisciplineIds = model.SelectedProtocolDisciplines.Except(entity.ProtocolDisciplines.Select(m => m.DisciplineId));
db.Disciplines
    .Where(m => addedDisciplineIds.Contains(m.DisciplineId))
    .ToList()
    .ForEach(m => entity.ProtocolDisciplines.Add(m));

This extra footwork is necessary to maintain the existing, unchanged M2M relationships.

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • Not related to your answer but I suggest to using [The Select Tag Helper](https://learn.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms#the-select-tag-helper) instead of `@Html.ListBoxFor` – Tân Feb 15 '17 at 18:26
  • I've implemented your code and just like everything else I've tried, the control renders exactly how I would expect with the exception of the selected value(s). They are not selected. I've looked at the debugger just prior to returning the model to the view and everything is as I would expect; I see the 16 total Disciplines and I see the single Discipline that should be the default (selected) value. You stated that Razor will take care of setting the appropriate selected values but I don't see how it knows (based on your code). – Steven Frank Feb 15 '17 at 20:53
  • Chris, You might want to update this now that OP has added extra info basd on discussion above (in particular the `ProtocolDisciplines` property is a collection of complex objects) –  Feb 15 '17 at 22:27