Why is ListBoxFor not selecting items, but ListBox

2019-01-22 02:01发布

问题:

I have the following code in my view:

<%= Html.ListBoxFor(c => c.Project.Categories,
        new MultiSelectList(Model.Categories, "Id", "Name", new List<int> { 1, 2 }))%>

<%= Html.ListBox("MultiSelectList", 
        new MultiSelectList(Model.Categories, "Id", "Name", new List<int> { 1, 2 }))%>

The only difference is that the first helper is strongly typed (ListBoxFor), and it fails to show the selected items (1,2), even though the items appear in the list, etc. The simpler ListBox is working as expected.

I'm obviously missing something here. I can use the second approach, but this is really bugging me and I'd like to figure it out.

For reference, my model is:

public class ProjectEditModel
{
    public Project Project { get; set; }
    public IEnumerable<Project> Projects { get; set; }
    public IEnumerable<Client> Clients { get; set; }
    public IEnumerable<Category> Categories { get; set; }
    public IEnumerable<Tag> Tags { get; set; }
    public ProjectSlide SelectedSlide { get; set; }
}

Update

I just changed the ListBox name to Project.Categories (matching my model) and it now FAILS to select the item.

<%= Html.ListBox("Project.Categories",
        new MultiSelectList(Model.Categories, "Id", "Name", new List<int> { 1, 2 }))%>

I'm obviously not understanding the magic that is happening here.

Update 2

Ok, this is purely naming, for example, this works...

<%= Html.ListBox("Project_Tags",
new MultiSelectList(Model.Tags, "Id", "Name", Model.Project.Tags.Select(t => t.Id)))%>

...because the field name is Project_Tags, not Project.Tags, in fact, anything other than Tags or Project.Tags will work. I don't get why this would cause a problem (other than that it matches the entity name), and I'm not good enough at this to be able to dig in and find out.

回答1:

I've stumbled across this problem myself, finally I realized that the problem was a naming convention.

You cannot name the ViewBag or ViewData poperty containing the SelectList or MultiSelectList to the same name your property model containing the selected items. At least not if you're using the ListBoxFor or DropDownListFor helper.

Here's an example:

    public class Person
    {
          public List<int> Cars { get; set; }
    }

    [HttpGet]
    public ActionResult Create()
    {
          //wont work
          ViewBag.Cars = new SelectList(carsList, "CarId", "Name"); 

          //will work due to different name than the property.
          ViewBag.CarsList = new SelectList(carsList, "CarId", "Name"); 

          return View();
    }

    //View
    @Html.ListBoxFor(model => model.Cars, ViewBag.CarsList as SelectList)

I'm sure theres plenty of other ways doing this, but it solved my problem, hope it will help someone!



回答2:

I have also been stuck with this exact same issue and encountered the same problem with ListBox and ListBoxFor.

No matter what I do, I cannot get selections to occur on the ListBoxFor. If I change to the ListBox and name it something OTHER than the property name of the data I am binding to, selections occur.

But then because I'm not using ListBoxFor and the data is sitting inside a model class (Model.Departments) for example, I don't get model binding on the way back to my controller and hence the property is null.

EDIT I found a solution posted by someone else here; Challenges with selecting values in ListBoxFor



回答3:

The correct answer is that it doesn't work very well. As such I read the MVC code. What you need to do is implement IConvertible and also create a TypeConverter.

So, in my instance I had a Country class, such that people could choose from a list of countries. No joy in selecting it. I was expecting an object equals comparison on the selectedItems against the listitems but no, that's not how it works. Despite the fact that MultiListItem works and correctly gets the selected items, the moment it is bound to your model it's all based on checking that the string represnetation of your object instance matches the string "value" (or name if that is missing) in the list of items in the SelectItemList.

So, implement IConvertible, return the string value from ToString which would match the value in the SelectItemList. e.g in my case CountryCode was serialized into the SelectItem Value property , so in ToString IConvertible I returned CountryCode. Now it all selects correctly.

I will point out the TypeConverter is used on the way in. This time its the inverse. That Countrycode comes back in and "EN" needs converting into Country class instance. That's where the TypeConverter came in. It's also about the time I realised how difficult this approach is to use.

p.s so on your Category class you need to implement IConvertible. If its from the entity framework as my company is then you'll need to use the partial class to implement IConvertible and implement ToString and decorate it with a TypeConverter you wrote too.



回答4:

Also, you can try to clear ModelState for c.Project.Categories in the controller:

[HttpPost]
public ActionResult Index(ModelType model)
{
    ModelState.Remove("Project.Categories");
    return View("Index", model);
}

And use the next construction:

<%= Html.ListBoxFor(c => c.Project.Categories,
        new MultiSelectList(Model.Categories, "Id", "Name"))%>

Where c.Project.Categories is IEnumerable<int>.

Sorry for my english. Good luck!



回答5:

Although this isn't an answer to your main question, it is worth noting that when MVC generates names it will turn something like Project.Tags into Project_Tags, replacing periods with underscores.

The reason that it does this is because a period in an element ID would look like an element named Project with a class of Tags to CSS. Clearly a bad thing, hence the translation to underscores to keep behaviour predictable.

In your first example,

<%= Html.ListBoxFor(c => c.Project.Categories,
    new MultiSelectList(Model.Categories, "Id", "Name", new List<int> { 1, 2 }))%>

the listbox is attempting to bind to Model.Project.Categories for your strongly typed Model which has been provided to the page (using the lambda notation). I'm not sure what the second parameter in the ListBoxFor is doing though.

What is the Model that is being passed to the page?



回答6:

Try this

<%= Html.ListBoxFor(c => c.Project.Categories,
    new MultiSelectList(
        Model.Categories
        ,"Id"
        ,"Name"
        ,Model.Project.Tags.Select(
        x => new SelectListItem()
            {
            Selected = true,
            Text = x.TEXT,
            Value = x.ID.ToString()
            }).ToList())

       )
)%>


回答7:

Html.ListboxFor and Html.Listbox work great when you're NOT binding the list box to its data source. I assume the intended use is this:

// Model
public class ListBoxEditModel
{
    public IEnumerable<Category> Categories { get; set; }
    public IEnumerable<Category> SelectedCategories { get; set; }
}    

In the view:

@Html.ListBoxFor(m => m.SelectedCategories,
                 new MultiSelectList(Model.Categories, "Id", "Name"))
// or, equivalently
@Html.ListBox("SelectedCategories" ,
                 new MultiSelectList(Model.Categories, "Id", "Name"))

Note that in this case you don't have to explicitly say which values are selected in the MultiSelectList - the model you're binding to takes precedence, even if you do!