9

I have a list of items that will be associated to a user. It's a one-to-many relationship. I want the entire list of items passed into the view so that they can choose from ones that are not associated to them yet (and also see those that are already associated). I want to create checkboxes from these. I then want to send the selected ones back into the controller to be associated. How can I pass in the list of all of them, including those that aren't yet associated, and reliably pass them back in to be associated?

Here's what I tried first, but it's clear this won't work as I'm basing the inputs off the items passed in via the AllItems collection, which has no connection to the Items on the user itself.

<div id="item-list">
    @foreach (var item in Model.AllItems)
    {
        <div class="ui field">
            <div class="ui toggle checkbox">
                <input type="checkbox" id="item-@item.ItemID" name="Items" value="@item.Active" />
                <label for="item-@item.ItemID">@item.ItemName</label>
            </div>
        </div>
    }
</div>
Fran
  • 6,440
  • 1
  • 23
  • 35
muttley91
  • 12,278
  • 33
  • 106
  • 160

2 Answers2

22

You cannot bind to a collection using a foreach loop. Nor should you be manually generating your html, which in this case would not work because unchecked checkboxes do not post back. Always use the strongly typed html helpers so you get correct 2-way model binding.

You have not indicated what you models are, but assuming you have a User and want to select Roles for that user, then create view models to represent what you want to display in the view

public class RoleVM
{
  public int ID { get; set; }
  public string Name { get; set; }
  public bool IsSelected { get; set; }
}
public class UserVM
{
  public UserVM()
  {
    Roles = new List<RoleVM>();
  }
  public int ID { get; set; }
  public string Name { get; set; }
  public List<RoleVM> Roles { get; set; }
}

In the GET method

public ActionResult Edit(int ID)
{
  UserVM model = new UserVM();
  // Get you User based on the ID and map properties to the view model
  // including populating the Roles and setting their IsSelect property
  // based on existing roles
  return View(model);
}

View

@model UserVM
@using(Html.BeginForm())
{
  @Html.HiddenFor(m => m.ID)
  @Html.DisplayFor(m => m.Name)
  for(int i = 0; i < Model.Roles.Count; i++)
  {
    @Html.HiddenFor(m => m.Roles[i].ID)
    @Html.CheckBoxFor(m => m.Roles[i].IsSelected)
    @Html.LabelFor(m => m.Roles[i].IsSelected, Model.Roles[i].Name)
  }
  <input type"submit" />
}

Then in the post method, your model will be bound and you can check which roles have been selected

[HttpPost]
public ActionResult Edit(UserVM model)
{
  // Loop through model.Roles and check the IsSelected property
}
  • This is actually something like what I ended up doing (with small differences). I may make some tweaks based on your suggestions here, though. Thanks! – muttley91 Apr 10 '15 at 15:03
0

It doesn't look like you're going to be deleting the checkboxes dynamically so that makes this problem a lot easier to solve. NOTE: The following solution won't work as expected if you allow clients or scripts to dynamically remove the checkboxes from the page because the indexes will no longer be sequential.

MVC model binding isn't foolproof so sometimes you have to help it along. The model binder knows it needs to bind to a property called Items because the input field's name is Items, but it doesn't know Items is a list. So assuming in your controller you have a list of items to model bind to called Items what you need to do is help MVC recognize that you're binding to a list. To do this specify the name of the list and an index.

<div id="item-list">
    @for (var i = 0; i < Model.AllItems.Count; i++)
    {
        <div class="ui field">
            <div class="ui toggle checkbox">
                <input type="checkbox" id="item-@Model.AllItems[i].ItemID" name="Items[@i]" value="@Model.AllItems[i].Active" />
                <label for="item-@Model.AllItems[i].ItemID">@Model.AllItems[i].ItemName</label>
            </div>
        </div>
    }
</div>

The key line here is this:

<input type="checkbox" id="item-@Model.AllItems[i].ItemID" name="Items[@i]" value="@Model.AllItems[i].Active" />

Notice the Items[@i]? That's telling the model binder to look for a property named Items and bind this value to the index at i for Items.

BoredBlazer
  • 354
  • 1
  • 11
  • This works, as it passes in the item indexes, but the actual value of each item is null. Is there a way to pass more info about the object in? – muttley91 Apr 09 '15 at 15:50
  • So I'm going to assume your Items list isn't a list of booleans then? The way you handle that is by giving the model binder even MORE info. So if the objects in your items list have a property called "Active" then your checkbox's name should be Items[@i].Active. Then for any additional fields you want to populate, like name for instance, you'd have another input with the name Items[@i].Name. – BoredBlazer Apr 09 '15 at 16:18
  • I guess I'm confused - are you saying I need multiple inputs for one checkbox? What if I just want to pass the item ID back to the controller? EDIT: also, in order for the checkbox to be checked, you probably meant to use the `checked` attribute. – muttley91 Apr 09 '15 at 17:24
  • What are you trying to bind to? If you're binding to a List then no you only need the one checkbox and you don't have to worry about giving the model binder more info than Items[@i]. If you're binding to a List then yes you're going to need multiple input fields to fill out each property for each object in the list. Primitive types (bool, int, char, etc) do not require extra dot notation. Object types do. – BoredBlazer Apr 09 '15 at 18:00
  • Well, because I'm dealing with checkboxes, I'm either passing "Yes, Item1" or nothing at all (for Item1). So I'll probably just pass the ID in as the value, so it'll say "Yes, 1" and "Yes, 2" and get the Items after the fact. That way, I can avoid having to put other fields. Though I suppose I could just use hidden fields... – muttley91 Apr 09 '15 at 18:15
  • Yeah, typically you'd have a model similar to the way you passed it in. You passed it in as a model with two fields so you could also bind it to a model with two fields using hidden inputs. That reduces the code complexity and puts the responsibility for binding on the model binder without you having to do it. – BoredBlazer Apr 09 '15 at 18:47
  • 1
    This does not work unless every checkbox is checked. The `DefaultModelBinder` requires collection indexers to start at zero and be consecutive. Since unchecked checkboxes do not post back, it the first checkbox was not checked, then model binding would fail and `Items` would be empty. –  Apr 10 '15 at 05:37