Introduction
This article is for beginners to .NET MVC who, like me, have been frustrated by seemingly simple implementations of basic HTML structures that turn out to be not so simple upon their implementation in .NET - at least until you understand the concepts that are underpinning these constructs.
In this, we cover the implementation of a MultiSelectList
, both with and without selected values, right from the implementation of the Model through to receiving the data back from the view. This also applies to SelectLists
, meaning this article is relevant if you are looking to employ any of the following on a user form:
- Dropdown Lists
- List Boxes (without pre-selected values)
- List Boxes (with pre-selected values) - Note that there is a lot of misinformation on Stack Overflow and other places when implementing a list with pre-selected values so be sure to review this section.
- Checkboxes or Radio Buttons (with or without pre-selected values)
If you are an experienced coder who sees any flaws or way to improve the methods in this article, please do comment or message me.
Accompanying Source Code
I have created a sample solution in Visual Studio 2015 Community which illustrates each of the concepts listed in this article in more detail which you can access via this DropBox link. If you have any issues at all accessing it, please let me know. You may need to let Nuget update the missing packages for the project to run.
Download Accompanying Solution/Source Code
Background
This article assumes some background knowledge of .NET and MVC, but not much. You should be able to spin up a new .NET MVC project (I'm using Visual Studio Community 2015), and be able to create some basic models and scaffold out some controllers and views from there. If you need help with any of these things, there are many good tutorials out there, but the one I recommend is this one - free from Plurasight (and Microsoft).
If you have any trouble understanding any of the concepts in this article, go ahead and post a comment and I will help you out as much as possible.
Implementation
This section will be broken out into the logical steps required to implement the multi-select list that we want. Our goal is to implement a multi-select list on the Player Create view, which will allow users to select/assign multiple teams to a new player they are creating.
The article will follow a specific structure:
- The Models - This section will take a look at the models that we are using with focus on the two important properties to the implementation: Team and Player and the database relationship that underpins the goal of this article.
- Implementing the View Model - We will be implementing a view model for the Player Create view to use. It will contain everything the view requires to fulfill the needs of our domain model, and therefore provides the benefits of flexibility along with separation from the domain models.
- Implementing the Controller Requirements - The guts of the activity will be implementing components of the controller required to parse the data into something that is usable by .NET's HTML helper methods.
- Implementing the View - The last part of the exercise is actually implementing the
ListBox
that we want to display to the users.
- Receiving the Data from the ViewModel - Here, we will look at what to do with the data once the user submits the form back to your
[HttpPost]
controller action.
1. The Models
We will start with our basic MVC setup. I have a couple of simple models - a Player
and a Team
. I have created a many-to-many relationship using Entity Framework and Code First (if you don't know how to do this, I suggest giving this tutorial a look but, again, just comment and I will help you out as best I can), so that a Player
can be on many Teams
, and Teams
can have many Players
assigned.
Here are the models and the database context:
public class Team
{
public Team()
{
this.Players = new List<Player>();
}
[Key]
public Guid TeamId { get; set; }
[Required]
[Display(Name = "Team Name")]
[StringLength(128, ErrorMessage = "Team Name can only be 128 characters in length.")]
public string Name { get; set; }
public virtual ICollection<Player> Players { get; set; }
}
public class Player
{
public Player()
{
this.Teams= new List<Team>();
}
[Key]
public Guid PlayerId { get; set; }
[Required]
[Display(Name = "Player Name")]
[StringLength(128, ErrorMessage = "Player's name can only be 128 characters in length.")]
public string Name { get; set; }
public virtual ICollection<Team> Teams { get; set; }
}
public class AppDbContext : DbContext
{
public AppDbContext()
: base("AppDbContext")
{
}
public DbSet<Team> Teams { get; set; }
public DbSet<Player> Players { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Team>()
.HasMany(i => i.Players)
.WithMany(i => i.Teams)
.Map(i =>
{
i.MapLeftKey("TeamId");
i.MapRightKey("PlayerId");
i.ToTable("Team_Player");
});
}
}
You will notice a couple of important details about these models and the DbContext
that we have initialized:
- We instantiate the collection of
Teams
and Players
in the model itself (see the this.Players = new List<Player>()
; line and equivalent in the Team
model)
- The override of the
OnModelCreating
class lets us define how we want the relationship between Teams
and Players
created. Because we want a many-to-many relationship, what we have defined will create us a join
table in the database called "Team_Player
" which will contain two primary keys in columns called "TeamId
" (it will use the TeamId
property of our Team
model) and "PlayerId
" (it will use the PlayerId
property of our Player
model. This is a separate topic in itself; refer to the link at the beginning of this section of this article for more details on this.
From this point on, we will skip some steps such as the actual creation of the controller and views, and assume that these are already in place but require modifications. Specifically, we will assume that no infrastructure in the view or the controller exists for the multi-select list that we want to implement.
2. Implementing the View Model
For the view model, there are a couple of requirements that we need to meet in order to pass data in between the controller and the view, and then parse in the controller to fulfill the requirements of our Player
model.
- We need an instance of .NET's
MultiSelectList
which we will populate with Team
s' information
- We need an instance of .NET's
List<>
which will use to house the Ids of the Team
s the user selects (making it a List<Guid>
of TeamId
s)
Therefore, our view model will look like the below:
public class CreatePlayerViewModel
{
[Required]
[Display(Name = "Player Name")]
[StringLength(128, ErrorMessage = "Players name can only be 128 characters in length.")]
public string Name { get; set; }
public List<string> TeamIds { get; set; }
[Display(Name = "Teams")]
public MultiSelectList Teams { get; set; }
}
You will notice that the property in our view model that houses the TeamIds
in a List
of string
s and not Guid
s. This is to make it easier to pass the Ids back and forth between the view and controller.
3. Implementing the Controller Requirements
There are a couple of different ways we can achieve what we need to in the controller. We are going to review two options, with the first trying to explain the concept and the second being the much more condensed version.
Finally, we will demonstrate how to add selected items to the list, such that may be used in an Edit
method, where you want the user to see which Team
s are already assigned to the Player
by highlighting them in the ListBox
.
Option 1
First, we need to create an instance of our appDbContext
, then use LINQ to store the teams in a variable (specifically, a List
).
appDbContext db = new appDbContext();
var teams = db.Teams.ToList();
Then, we need to instantiate a list of SelectListItems
- i.e., the objects that populate a SelectList
or MultiSelectList
.
List<SelectListItem> items = new List<SelectListItem>();
Then, we will loop through each of the team objects in our teams
List, and will create a new SelectListItem
for each one, subsequently adding the SelectListItem
to our items
list.
foreach (var team in teams)
{
var item = new SelectListItem
{
Value = team.TeamId.ToString(),
Text = team.Name
};
items.Add(item);
}
Then, we can instantiate a new MultiSelectList
(or SelectList
) and add our items list to it. To do this, we are going to use one of the numerous constructors for the MultiSelectList
that you can review here. The one we are going to use is MultiSelectList(IEnumerable, string, string)
which, for us, translates to MultiSelectList
(ouritems
List, the "Value"
field which contains our Id
, and our "Text"
value which contains the text that we want to show the user in the actual list. Note that I am applying the OrderBy()
method to our items
list, which is going to order the list by the "Text
" field in the SelectListItems
list, which for us is the Name
property of the Team
instance.
MultiSelectList teamsList = new MultiSelectList(items.OrderBy(i => i.Text), "Value", "Text");
Then, we just need to assign this to the Teams
property on our CreatePlayerViewModel
, which we need to first instantiate, before passing it back to our view.
CreatePlayerViewModel model = new CreatePlayerViewModel { Teams = teamsList };
return View(model);
Option 2
The second of our two options is much simpler as we get to skip many of the above steps, jumping straight to the instantiating of the MultiSelectList
. Note that we are using the same constructor as Option 1.
MultiSelectList teamsList = new MultiSelectList(db.Teams.ToList().OrderBy(i => i.Name), "TeamId", "Name");
Then, we just need to instantiate the view model, set the Teams
property and return it in our view.
CreatePlayerViewModel model = new CreatePlayerViewModel { Teams = teamsList };
Adding Selected Items to the List
Adding selected items is a little tricky, as you may be tempted to do something like the first Option
outlined above, do some filtering on the teams list and add the "Selected
" property to the SelectListItem
generation, like the below:
foreach (var team in teams)
{
var item = new SelectListItem
{
Value = team.TeamId.ToString();
Text = team.Name;
Selected = true;
};
items.Add(item);
}
This will not work, because the MultiSelectList
(or SelectList
) will only allow you to tell it which items are selected on its instantiation - it will not learn from the Selected
property of the SelectListItems
.
We will use an example goal to demonstrate this. We want to "select
" a series of teams that the Player
is already assigned to.
The first thing we need to do is decide which constructor of the MultiSelectList
object we are going to use. Again, referring to Microsoft's documentation here, we will use MultiSelectList(IEnumerable, string, string, IEnumerable)
, which is the same as before with the addition of the last IEnumerable
.
This IEnumerable
needs to store the Values
that we want to tell the list to select
. They need to correspond to the Values
in the first IEnumerable param
- i.e., the TeamId
property within our teams
list of Teams.
To do this, we need to first create a separate list of teams that we will call playerTeams
, using LINQ to filter the teams
list to what we want. We will assume that we have received via some parameter the Id
of the Player
that we want to filter on, call it playerId
.
var playerTeams = teams.Where(t => t.Players.Contains(player)).ToList();
We need the Id
s of each of these playerTeams
in some enumerable object, for which we will use an array of string
s (note that we will need to convert the Guid Id
into a string
).
string[] playerTeamsIds = new string[playerTeams.Count];
int length = playerTeams.Count;
for (int i = 0; i < length; i++)
{
playerTeamsIds[i] = playerTeams[i].TeamId.ToString();
}
Now, we have the IEnumerable
containing the values we need that our MultiSelectList
constructor requires. We can go ahead and instantiate the MultiSelectList
now and return it in our view (note that we are assuming that we're in some kind of Edit
method, which is the only scenario that really makes sense to want to pre-select things in the MultiSelect
list).
MultiSelectList teamsList = new MultiSelectList(db.Teams.ToList().OrderBy(i => i.Name),
"TeamId", "Name", playerTeamsIds);
EditPlayerViewModel model = new EditPlayerViewModel { Teams = teamsList };
4. Implementing the View
For the view, we will be using one of .NET MVC's Html helper methods = the ListBoxFor
method - within our CreatePlayer
form. This will look like the following:
@Model MultiSelectListSample.Models.CreateUserViewModel
@using (Html.BeginForm("Create", "Players",
FormMethod.Post, new { @class = "form-horizontal" }))
{
@Html.AntiForgeryToken()
<div class="form-group">
@Html.LabelFor(i => i.Teams, new { @class = "control-label col-md-3" })
<div class="col-md-9">
@Html.ListBoxFor(i => Model.TeamIds, Model.Teams,
new { @class = "form-control" })
@Html.ValidationMessageFor(i => i.Teams,
"", new { @class = "text-warning" })
</div>
Looking at the ListBoxFor
helper above, we first see that the ListBox
is for the TeamIds
property of the view property (this is the <Model>
parameter of the method). What this means is that when the user selects some teams and then submits the form, in your Create [HttpPost]
method, you will receive the Id
s of the teams that the user has selected in that List
. We will look at this below.
The next parameter the ListBoxFor
method wants is the MultiSelectList
(or SelectList
), which we find in Model.Teams
(remember, this is the teamsList
MultiSelectList
that we created).
The last property is the htmlattributes
where we can apply the class that we want, or any other attributes such as Id
, etc.
5. Receiving the Data from the View Model
Once the user submits the form, you will receive the model back to your [HttpPost] Create
method, which would be structured to accept a CreateViewModel
parameter:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "Name, TeamIds")] CreatePlayerViewModel model)
{
...
}
Note that we are only interested in the TeamIds
property, not the Teams
property - this is only used to instantiate the MultiSelectList
and contains none of the data we want here.
Now that we have the TeamIds
back from the view, we can create our new Player
. First, we will instantiate the Player
with just the PlayerId
and the Name
property, seeing as how our Player
model doesn't require the Teams
property contain data.
if (ModelState.IsValid)
{
Player player = new Player
{
PlayerId = Guid.NewGuid(),
Name = model.Name
};
...
}
Now, we need to find the Teams
that belong to the TeamIds
that we have received back from the model, and assign them to the player that we have just created. Assuming we have instantiated an instance of our appDbContext
called db
somewhere above in our controller:
if (model.TeamIds != null)
{
foreach (var id in model.TeamIds)
{
var teamId = Guid.Parse(id);
var team = db.Teams.Find(teamId);
try
{
player.Teams.Add(team);
}
catch (Exception ex)
{
return View("Error", new HandleErrorInfo(ex, "Players", "Index"));
}
}
}
try
{
db.Players.Add(player);
db.SaveChanges();
}
catch (Exception ex)
{
return View("Error", new HandleErrorInfo(ex, "Players", "Index"));
}
Now, we have added each of the Teams
that the user selected to our new Player
, player
.
Note that if you're in an edit method, where the Player
already has some Teams
assigned, these Teams
will also come back from View
. After adding any Teams
to the Player
, you will want to remove these.
To do this, we just need to create a list of Teams which contains all the Teams in the db, less the teams the user wants to assign to the Player. We will employ the Except() method to achieve this difference. We can then loop through this list and call the Remove() method on each one.
if (model.TeamIds.Count > 0)
{
List<Team> viewModelTeams = new List<Team>();
foreach (var id in model.TeamIds)
{
var team = db.Teams.Find(Guid.Parse(id));
if (team != null)
{
try
{
player.Teams.Add(team);
viewModelTeams.Add(team);
}
catch (Exception ex)
{
return View("Error", new HandleErrorInfo(ex, "Players", "Index"));
}
}
}
var allTeams = db.Teams.ToList();
var teamsToRemove = allTeams.Except(viewModelTeams);
foreach (var team in teamsToRemove)
{
try
{
player.Teams.Remove(team);
}
catch (Exception ex)
{
return View("Error", new HandleErrorInfo(ex, "Players", "Index"));
}
}
}
try
{
db.Entry(player).State = EntityState.Modified;
db.SaveChanges();
}
catch (Exception ex)
{
return View("Error", new HandleErrorInfo(ex, "Players", "Indes"));
}
Note that you can read how Except()
works here.
Conclusion
This concludes the article on how to implement MultiSelectLists
(also applies to SelectLists
). It is the author's hope that in reading and following along with the code in the sample solution provided, the reader is able to save some time in implementing something that isn't exactly straightforward to implement and, to the beginner, can be a frustrating and time-consuming experience to learn.
As always, if you are an experienced coder and you see any flaws or areas of improvement, please do place a comment below.
Points of Interest
The most challenging part of this for me was creating a MultiSelectList
with pre-selected values. There was a lot of information on Stack Overflow where answers were suggesting that users loop over a list of SelectListItems
and mark their "Selected
" property as true
, then adding these items to a MultiSelectList
or SelectList
. This fails, however, as the "SelectedValues
" property of a MultiSelectList
can only be set on the MultiSelectList
's instantiation.
I burned a lot of time trying to figure it out, which is what inspired me to write this article to help other newbies to .NET save some time and frustration!
History
- 14th December, 2015: First copy
- 15th December, 2015: Minor edits
- 16th December, 2015: Edits and addition of sample solution