1
votes

I know there are many custom implementation of CheckBoxListFor helper method to fill up the missing feature in the MVC framework. But I am not ready to use them just yet. I am interested in creating a checkbox list using MVC 4 or 5 provided features only. So, I created this model class:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace MvcApplication1.Models
{
    public class FruitViewModel
    {
        public int[] SelectedFruits { get; set; }
        public IList Fruits
        {
            get
            {
                return new List{
                new Fruit{Id=1, Name="Apple", Selected = false}, 
                new Fruit{Id=2, Name="Banana", Selected = false}, 
                new Fruit{Id=3, Name="Cherry", Selected = false}, 
                new Fruit{Id=4, Name="Durian", Selected = false}, 
                new Fruit{Id=5, Name="Elderweiss Grape", Selected = false}
            };
            }
        }
    }

    public class Fruit
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public bool Selected { get; set; }
    }
}

And here is the controller class:

using MvcApplication1.Models;
using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
    public class FruitController : Controller
    {
        public ActionResult Index()
        {
            FruitViewModel model = new FruitViewModel();
            return View(model);
        }

        [HttpPost]
        public ActionResult Index(FruitViewModel model)
        {
            if (ModelState.IsValid)
            {   
            }

            return View(model);
        }
    }
}

And here is the razor view:

@model MvcApplication1.Models.FruitViewModel

@{
    ViewBag.Title = "Index";
}

<h2>Select your favorite fruits</h2>

@using (Html.BeginForm("Index", "Fruit"))
{
    <p>Using Html helper method:</p>
    for (int i = 0; i < Model.Fruits.Count; i++)
    {

        @Html.CheckBoxFor(m => m.Fruits[i].Selected) @Model.Fruits[i].Name<br />
    }

    <p>Plain html without html helper</p>

      for (int i = 0; i < Model.Fruits.Count; i++)
    {
          <input type="checkbox" name="SelectedFruits" value="@Model.Fruits[i].Id" checked="@Model.Fruits[i].Selected" />
 @Model.Fruits[i].Name<br />
    }

    <input type="submit" name="submit" value="Submit" />
}

Here are the problems I am having:

  1. The plain HTML version will populate the SelectedFruits collection with my selected fruits' Ids properly, as you can see from the screen shot. But when the page refreshes after post back, the selected checkboxes status is reset to not checked.

  2. The version using the Html helper CheckBoxFor will not populate my SelectedFruits collection with my selected fruits' IDs, although it does seem to maintain the check box status as checked after post back completes.

So, in either case, there is a big problem.

What is the correct way to set up a checkbox list such that I can get the SelectedFruits collection populated correctly and the status of the checkboxes maintained after form post completes (important when I add other stuff on the page and if validation fails).enter image description here

2
Thanks for editing. Please tell me how to correctly format a razor view in an SO post. I spent 30 minutes doing it just now, but still could not get it right, and had to give up. Thanks a lot!Stack0verflow

2 Answers

4
votes

I would suggest that you can use the following solution,

Controller Change [Only the Get Method to set the data for the view to display]

public ActionResult Index()
{
    FruitViewModel model = new FruitViewModel();
    model.Fruits = new List<Fruit>
        {
        new Fruit{Id=1, Name="Apple", Selected = false}, 
        new Fruit{Id=2, Name="Banana", Selected = false}, 
        new Fruit{Id=3, Name="Cherry", Selected = false}, 
        new Fruit{Id=4, Name="Durian", Selected = false}, 
        new Fruit{Id=5, Name="Elderweiss Grape", Selected = false}
    };
    return View(model);
}

FruitModel Change, we are setting the property to be populated dynamically based on the fruits that the user has selected.

public int[] SelectedFruits
{
    get
    {
        return Fruits != null && Fruits.Count > 0
        ? Fruits.Where(f => f.Selected == true).Select(f => f.Id).ToArray()
        : new int[0];
    }
    set { }
}

View Page Change [Index.cshtml]

for (int i = 0; i < Model.Fruits.Count; i++)
{
    @Html.CheckBoxFor(m => m.Fruits[i].Selected) @Model.Fruits[i].Name<br />
    @Html.HiddenFor(m => m.Fruits[i].Id)
    @Html.HiddenFor(m => m.Fruits[i].Name)
}

The problem that I found in your code was that you have used the checkbox name property as "SelectedFruits" in the html for the checkbox that you have manually rendered. Whereas the markup rendered by the Html Helper is having the name of "Fruits[0].Selected" etc...

Hence, the selected fruits was not properly modelbound.

Kindly verify the currently generated markup and post your feedback in case of any queries.

1
votes

Your problem is that the Fruits property in your viewmodel always creates a new array where the Selected property for each fruit is always false. You can fix it by just initializing the array once in your viewmodel instead:

public IList<Fruit> Fruits
{
    get
    {
        if (_fruits == null)
            _fruits = new List<Fruit> {
                new Fruit{Id=1, Name="Apple", Selected = false}, 
                new Fruit{Id=2, Name="Banana", Selected = false}, 
                new Fruit{Id=3, Name="Cherry", Selected = false}, 
                new Fruit{Id=4, Name="Durian", Selected = false}, 
                new Fruit{Id=5, Name="Elderweiss Grape", Selected = false}
            }
        return _fruits;
    };
}
private List<Fruit> _fruits;

Once that's fixed, you'll have to update your controller to fix the Selected properties:

[HttpPost]
public ActionResult Index(FruitViewModel model)
{
    foreach (int fruitId in model.SelectedFruits)
        model.Fruits[fruitId].Selected = true;

    return View(model);
}