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!

Welcome

Jared specializes in Microsoft SharePoint and other Microsoft stack technologies associated with Microsoft’s Office 365 offerings.

Jared is a native of Illinois but now resides in the Mid-Atlantic region. In addition to his expertise in Office 365 technology, Jared is also an avid photographer, musician, and Thoroughbred racehorse breeder, pedigree analyst, and researcher.

PHOTOGRAPHY

Jared also offers photography capabilities that go beyond the those of your run-of-the-mill natural light photographers. Jared has studied the methods and techniques of some of the top head shot, portrait, and outdoors photographers in America to offer clients his own unique style that makes full use of modern photography / lighting capabilities.

DATA SERVICES

Jared Gollnitz is an industry-recognized leader in data services for the commercial Thoroughbred public auctions. Jared utilizes the latest database development offerings from Microsoft, utilizing ASP.net, JavaScript, OData, SQL Server, and Windows Azure to create dynamic, effective, reliable, and secure displays of horses being offered for sale. We can easily add on dynamic data tables to websites created using traditional html, or we can add them to new website templates such as Blogger.com (Google blogs), WordPress, Joomla!, Drupal, and any others that allow the rendering of custom JavaScript.

Some examples of my work include the Texas Thoroughbred Association and Minnesota Thoroughbred Association interactive catalogs:

Texas 2019 2 Year Old Sale

Jared has also developed a new prototype of the catalog service, displayed here using a Blogger.com (BlogSpot) site: http://breedforspeed.blogspot.com/2018/10/equine-sales-company-of-louisiana-2018_5.html 

The demonstrated prototypes store their data on SQL Azure… a cost-effective, powerful, industry-standard and enterprise-grade database platform.

THOROUGHBRED PEDIGREE RESEARCH

Jared also spends many hours each week studying the pedigrees of top Thoroughbred racehorses of both yesterday and today to come up with the most predictably successful breeding scenarios for both mares and stallions all across America and even some International bloodstock.

If you would like Jared to give you a quote for data services, storage and presentation of your data, photography, or Thoroughbred pedigree planning and research please schedule a free consultation today!