Multi-Select List Box in ASP.NET / MVC using EntityFramework – Code First

For this project, I found a few resources. Each resource seemed to have only a small portion of the overall information, so I am going to try and compile everything needed into one post. NOTE: this example uses strongly typed classes only, no ViewModels, just straight MVC pattern. This example uses Code-First to new database.

Let’s assume the following scenario; you’ve created a class that will be used for a form submission for basic CRUD operations. Within this form, you will have a multi-select dropdown list that references a related class for the dropdown items. Sounds simple, right? Well, given the lack of documentation out there on this topic, the answer ended up being fairly simple but took a lot of digging and trial and error to finally solve. You can substitute my particular classes for the Courses / Instructors, Books / Authors, Movies / Actors scenarios, whatever your particular scenario might be.

1) First, fire up a project in Visual Studio for ASP.NET MVC web application.

2) Make sure the latest build of EntityFramework is added to the project.

3) Create your primary class that will be used for the parent form that will contain the dropdown list. In this case, class Case.cs (in the Models folder of your project) will be used for the primary form. Class CaseTypes will be used for the dropdown list selections. We reference these in the Cases class with:

public virtual ICollection<CaseType> CaseTypes { get; set; }

as seen below:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace CaseManagement.Models
{
    public class Cases
    {
        [Key]
        public int Id { get; set; }
        [Display(Name = "Patient or Provider Name")]
        public string SubjectName { get; set; }
        public string EventIdentifier { get; set; }

        public string Case_Identifier { get; set; }
        public virtual ICollection<CaseType> CaseTypes { get; set; }
        //There is more to this in my actual project, but let's just keep it simple.
    }
}

4) Next, we create the CaseType.cs class (also in the Models folder of the project):

Note the reference back to the Cases class:

public virtual ICollection<Case> Cases { get; set; }

Entity Framework creates a many-to-many relationship (by creating a link table) out of these references pointing to each other from the other’s class.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;

namespace CaseManagement.Models
{
    public class CaseType
    {
        [Key]
        public int Id { get; set; }
        [Display(Name ="Case Type")]
        public string CaseTypeName { get; set; }
        public string CreatedBy { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:MM-dd-yyyy}", ApplyFormatInEditMode = true)]
        public DateTime? CreatedOn { get; set; }
        public string ModifiedBy { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:MM-dd-yyyy}", ApplyFormatInEditMode = true)]
        public DateTime? ModifiedOn { get; set; }
        public virtual ICollection<Case> Cases { get; set; }
    }
}

5) For now, at this step, let’s assume that you will use EntityFramework to scaffold your views and your controllers for the classes you created above. For a more detailed tutorial, go here.

Now, we have to populate the lookup list in the CasesController:

Please note the Create and Edit controller actions for use of the ViewBag.CaseTypesVB in the GET action, and that the incoming argument for the dropdown in the HttpPost action is of type int[] (array),

and also the Load() function in the HttpPost action for the Edit:

if (ModelState.IsValid)
                {
                    db.Entry(caseIdentifier).State = EntityState.Modified;
                    db.Entry(caseIdentifier).Collection(p => p.CaseTypes).Load();

                    var newCaseTypes = db.CaseTypes.Where(x => CaseTypesVB.Contains(x.Id)).ToList();
                    caseIdentifier.CaseTypes = newCaseTypes;

                }

Full Cases controller code:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Mvc;
using CaseManagement.Data;
using CaseManagement.Models;
using System.Data.Entity.Migrations;

namespace CaseManagement.Controllers
{
    public class CasesController : Controller
    {
        private DBContext db = new DBContext();

        // GET: Cases
        public ActionResult Index()
        {
            return View(db.Cases.ToList());
        }

        // GET: Cases/Details/5
        public ActionResult Details(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            Case caseIdentifier = db.Cases.Find(id);
            if (caseIdentifier == null)
            {
                return HttpNotFound();
            }
            return View(caseIdentifier);
        }

        // GET: Cases/Create
        public ActionResult Create()
        {
               ///////////////HERE!!!!!!!!!!!!!!!
               ViewBag.CaseTypesVB = new MultiSelectList(db.CaseTypes, "Id", "CaseTypeName");
                return View();
            
        }
        // POST: Cases/Create
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
        // more details see https://go.microsoft.com/fwlink/?LinkId=317598.
        
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(Case caseIdentifier, int[] CaseTypesVB)
        {
            
         
                if (ModelState.IsValid)
                {
                    if (CaseTypesVB != null)
                    {
                        var ct = new List<CaseType>();
                        foreach (var casetp in CaseTypesVB)
                        {

                            var ctitem = db.CaseTypes.Find(casetp);
                           

                            ct.Add(ctitem);
                        }
                        caseIdentifier.CaseTypes = ct;
                    }
                    db.Cases.Add(caseIdentifier);
                    db.SaveChanges();
                    return RedirectToAction("Index");
                }

                return View(caseIdentifier);
            
        }

        // GET: Cases/Edit/5
        public ActionResult Edit(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            Case caseIdentifier = db.Cases.Find(id);
            
            List<int> selectedCaseTypes = new List<int>();
            foreach (var caseTypes in caseIdentifier.CaseTypes)
            {
                selectedCaseTypes.Add(caseTypes.Id);
            }
            int[] selectedCTs = selectedCaseTypes.ToArray();
            if (caseIdentifier == null)
            {
                return HttpNotFound();
            }
            ViewBag.CaseTypesVB = new MultiSelectList(db.CaseTypes, "Id", "CaseTypeName", selectedCTs);
            return View(caseIdentifier);
        }

        // POST: Cases/Edit/5
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
        // more details see https://go.microsoft.com/fwlink/?LinkId=317598.
        
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Edit(Case caseIdentifier, int[] CaseTypesVB)
        {
           

            
                if (ModelState.IsValid)
                {
                    db.Entry(caseIdentifier).State = EntityState.Modified;
                    db.Entry(caseIdentifier).Collection(p => p.CaseTypes).Load();

                    var newCaseTypes = db.CaseTypes.Where(x => CaseTypesVB.Contains(x.Id)).ToList();
                    caseIdentifier.CaseTypes = newCaseTypes;

                }
                db.SaveChanges();
                return RedirectToAction("Index");
                      
        }

        // GET: Cases/Delete/5
        public ActionResult Delete(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            Case caseIdentifier = db.Cases.Find(id);
            if (caseIdentifier == null)
            {
                return HttpNotFound();
            }
            return View(caseIdentifier);
        }

        // POST: Cases/Delete/5
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public ActionResult DeleteConfirmed(int id)
        {
            Case caseIdentifier = db.Cases.Find(id);
            db.Cases.Remove(caseIdentifier);
            db.SaveChanges();
            return RedirectToAction("Index");
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                db.Dispose();
            }
            base.Dispose(disposing);
        }
    }
}

Finally, the Create and Edit Views:

Note the most important lines, especially @Html.ListBox(“CaseTypesVB”):

<div class="form-group">
        @Html.LabelFor(model => model.CaseTypes, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.ListBox("CaseTypesVB")
            @Html.ValidationMessageFor(model => model.CaseTypes, "", new { @class = "text-danger" })
        </div>
    </div>

Create:

@model CaseManagement.Models.Case
@{
    ViewBag.Title = "Create";
}

<h2>Create</h2>


@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()
    
<div class="form-horizontal">
    <h4>CaseIdentifier</h4>
    <hr />
    @Html.ValidationSummary(true, "", new { @class = "text-danger" })
    <div class="form-group">
        @Html.LabelFor(model => model.SubjectName, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.SubjectName, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.SubjectName, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.EventIdentifier, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.EventIdentifier, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.EventIdentifier, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.CaseTypes, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.ListBox("CaseTypesVB")
            @Html.ValidationMessageFor(model => model.CaseTypes, "", new { @class = "text-danger" })
        </div>
    </div>
///// additional form fields removed from this example for brevity 
    }

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Edit:

@model CaseManagement.Models.Case

@{
    ViewBag.Title = "Edit";
}

<h2>Edit</h2>


@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>CaseIdentifier</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        @Html.HiddenFor(model => model.Id)

        <div class="form-group">
            @Html.LabelFor(model => model.SubjectName, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.SubjectName, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.SubjectName, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.EventIdentifier, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.EventIdentifier, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.EventIdentifier, "", new { @class = "text-danger" })
            </div>
        </div>
        <div class="CaseType">
            <div class="form-group">
                @Html.LabelFor(model => model.CaseTypes, htmlAttributes: new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.ListBox("CaseTypesVB")
                    @Html.ValidationMessageFor(model => model.CaseTypes, "", new { @class = "text-danger" })
                </div>
            </div>
        </div>
///////////additional fields removed from this example for brevity
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>


@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
    
}

In your Details view (only the relevant code shown, not the whole view):

<dt>
            @Html.DisplayFor(model => model.CaseTypes)
        </dt>
        <dd>
            @foreach (var ct in Model.CaseTypes)
            {
               <div>@ct.CaseTypeName </div>
                
            }
        </dd>

You basically do the same type of foreach in your Index view as well… you get the idea.

I hope this helps everyone who may want to use a multi-select dropdown list using a related lookup class!