在 .NET MVC 中实现 MultiSelectList 的分步指南
这是一个简单的分步指南,介绍如何在 .NET MVC 中实现多选列表,面向 .NET 新手。
引言
本文档面向 .NET MVC 初学者,就像我一样,过去曾因看似简单的基本 HTML 结构实现起来却不那么简单而感到沮丧,至少在理解了这些结构背后的概念之前是这样。
在本文中,我们将介绍 MultiSelectList
的实现,包括有选定值和无选定值的情况,从 Model 的实现一直到从视图接收数据。这同样适用于 SelectList
,这意味着如果您打算在用户表单中使用以下任何一种,本文都将相关:
- 下拉列表
- 列表框(无预选值)
- 列表框(有预选值)- 请注意,在 Stack Overflow 和其他地方关于带有预选值的列表实现存在许多错误信息,请务必仔细阅读本节。
- 复选框或单选按钮(有或无预选值)
如果您是经验丰富的编码人员,发现本文中的方法有任何缺陷或改进之处,请发表评论或给我留言。
附带源代码
我创建了一个 Visual Studio 2015 Community 的示例解决方案,其中更详细地说明了本文中列出的每个概念,您可以通过 此 DropBox 链接 访问。如果您在访问时遇到任何问题,请告知我。您可能需要让 Nuget 更新缺失的包才能运行项目。
背景
本文假定您具备一定的 .NET 和 MVC 背景知识,但不多。您应该能够启动一个新的 .NET MVC 项目(我使用的是 Visual Studio Community 2015),并能够从中创建一些基本模型并生成一些控制器和视图。如果您需要有关这些方面的帮助,有许多优秀的教程,但我推荐 这个 - 免费来自 Plurasight(和 Microsoft)。
如果您在理解本文中的任何概念时遇到困难,请随时发表评论,我将尽力提供帮助。
实现
本节将分解为实现我们想要的多选列表所需的逻辑步骤。我们的目标是在 Player Create 视图中实现一个多选列表,允许用户为他们创建的新玩家选择/分配多个团队。
文章将遵循特定的结构
- 模型 - 本节将介绍我们使用的模型,重点关注实现中的两个重要属性:Team 和 Player,以及支撑本文目标的数据库关系。
- 实现视图模型 - 我们将为 Player Create 视图实现一个视图模型。它将包含视图满足我们领域模型需求所需的一切,从而提供灵活性并与领域模型分离。
- 实现控制器要求 - 活动的核心将是实现控制器中所需的组件,以将数据解析成 .NET 的 HTML 辅助方法可用的内容。
- 实现视图 - 本次练习的最后一部分是实际实现我们要显示给用户的
ListBox
。 - 从视图模型接收数据 - 在这里,我们将查看用户提交表单后,数据如何回到
[HttpPost]
控制器操作。
1. 模型
我们将从基本的 MVC 设置开始。我有一些简单的模型 - Player
和 Team
。我使用 Entity Framework 和 Code First 创建了一个多对多关系(如果您不知道如何做,我建议您查看 这个教程,但同样,只需评论,我会尽力帮助您),以便 Player
可以属于多个 Teams
,而 Teams
可以分配给多个 Players
。
以下是模型和数据库上下文
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");
});
}
}
您会注意到关于这些模型和我们初始化的 DbContext
的几个重要细节
- 我们在模型本身中实例化了
Teams
和Players
的集合(请参阅this.Players = new List<Player>()
; 行以及Team
模型中的等效行)。 - 重写
OnModelCreating
类允许我们定义如何创建Teams
和Players
之间的关系。因为我们想要一个多对多关系,我们定义的内容将在数据库中创建一个名为 "Team_Player
" 的连接表,该表将包含两个主键列,名为 "TeamId
"(它将使用我们Team
模型的TeamId
属性)和 "PlayerId
"(它将使用我们Player
模型的PlayerId
属性)。这是一个独立的主题;有关更多详细信息,请参阅本文本节开头处的链接。
从这一点开始,我们将跳过一些步骤,例如实际创建控制器和视图,并假定它们已经就位但需要修改。具体来说,我们将假定视图或控制器中不存在我们要实现的多选列表的基础结构。
2. 实现视图模型
对于视图模型,我们需要满足几个要求,以便在控制器和视图之间传递数据,然后在控制器中解析数据以满足我们 Player
模型的要求。
- 我们需要一个 .NET 的
MultiSelectList
实例,我们将用Team
的信息填充它。 - 我们需要一个 .NET 的
List<>
实例,我们将使用它来存放用户选择的Team
的 Id(使其成为TeamId
的List<Guid>
)。
因此,我们的视图模型将如下所示:
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; }
}
TeamIds
的属性是一个 string
列表,而不是 Guid
列表。这是为了更方便地在视图和控制器之间传递 Id。3. 实现控制器要求
我们在控制器中实现所需功能有几种不同的方法。我们将回顾两种选择,第一种试图解释概念,第二种则是更精简的版本。
最后,我们将演示如何向列表中添加选定项,以便在 Edit
方法中使用,在该方法中,您希望用户通过在 ListBox
中突出显示来看到已分配给 Player
的 Team
。
选项 1
首先,我们需要创建一个 appDbContext
实例,然后使用 LINQ 将团队存储在一个变量中(具体来说,是一个 List
)。
appDbContext db = new appDbContext();
var teams = db.Teams.ToList();
然后,我们需要实例化一个 SelectListItems
列表 - 即填充 SelectList
或 MultiSelectList
的对象。
List<SelectListItem> items = new List<SelectListItem>();
然后,我们将遍历 teams
列表中的每个团队对象,并为每个对象创建一个新的 SelectListItem
,然后将 SelectListItem
添加到我们的 items
列表中。
foreach (var team in teams)
{
var item = new SelectListItem
{
Value = team.TeamId.ToString(),
Text = team.Name
};
items.Add(item);
}
然后,我们可以实例化一个新的 MultiSelectList
(或 SelectList
)并将我们的 items 列表添加到其中。要做到这一点,我们将使用 MultiSelectList
的众多构造函数之一,您可以在 此处 查看。我们将使用的是 MultiSelectList(IEnumerable, string, string)
,对我们来说,它翻译为 MultiSelectList
(我们的 items
列表,包含我们 Id
的 "Value
" 字段,以及我们显示给用户实际列表中的文本的 "Text
" 值。请注意,我正在对我们的 items
列表应用 OrderBy()
方法,这将按 SelectListItems
列表中的 "Text
" 字段排序,对我们来说,这实际上是我们 Team
实例的 Name
属性。
MultiSelectList teamsList = new MultiSelectList(items.OrderBy(i => i.Text), "Value", "Text");
然后,我们只需要将其分配给 CreatePlayerViewModel
的 Teams
属性,我们需要先实例化它,然后再将其返回到我们的视图。
CreatePlayerViewModel model = new CreatePlayerViewModel { Teams = teamsList };
return View(model);
选项 2
我们第二个选项要简单得多,因为我们可以跳过上述许多步骤,直接实例化 MultiSelectList
。请注意,我们使用的是与选项 1 相同的构造函数。
MultiSelectList teamsList = new MultiSelectList(db.Teams.ToList().OrderBy(i => i.Name), "TeamId", "Name");
然后,我们只需要实例化视图模型,设置 Teams
属性,并将其返回到视图。
CreatePlayerViewModel model = new CreatePlayerViewModel { Teams = teamsList };
向列表中添加选定项
添加选定项有点棘手,因为您可能会被诱惑去做类似上面第一个选项的事情,对团队列表进行一些过滤,并在生成 SelectListItem
时添加 "Selected
" 属性,如下所示:
foreach (var team in teams)
{
var item = new SelectListItem
{
Value = team.TeamId.ToString();
Text = team.Name;
Selected = true;
};
items.Add(item);
}
这将不起作用,因为 MultiSelectList
(或 SelectList
)只允许您在实例化时告诉它哪些项被选中 - 它不会学习 SelectListItems
的 Selected
属性。
我们将使用一个示例目标来演示这一点。我们想要"选择" Player
已经分配到的若干团队。
我们首先需要做的是决定我们将使用 MultiSelectList
对象的哪个构造函数。再次参考 Microsoft 的文档,我们将使用 MultiSelectList(IEnumerable, string, string, IEnumerable)
,它与之前相同,只是增加了一个 IEnumerable
参数。
这个 IEnumerable
需要存储我们想让列表选择的值。它们需要与第一个 IEnumerable
参数中的值相对应 - 即 teams
列表中的 TeamId
属性。
为此,我们需要首先创建一个单独的团队列表,我们将其命名为 playerTeams
,使用 LINQ 过滤 teams
列表以获取我们想要的。我们将假设我们通过某个参数接收了我们想要过滤的 Player
的 Id
,称之为 playerId
。
var playerTeams = teams.Where(t => t.Players.Contains(player)).ToList();
我们需要这些 playerTeams
中每个团队的 Id
放入某个可枚举对象中,我们将使用一个 string
数组(请注意,我们需要将 Guid Id
转换为 string
)。
// First, initialize the array to number of teams in playerTeams
string[] playerTeamsIds = new string[playerTeams.Count];
// Then, set the value of platerTeams.Count so the for loop doesn't need to work it out every iteration
int length = playerTeams.Count;
// Now loop over each of the playerTeams and store the Id in the playerTeamsId array
for (int i = 0; i < length; i++)
{
// Note that we employ the ToString() method to convert the Guid to the string
playerTeamsIds[i] = playerTeams[i].TeamId.ToString();
}
现在,我们有了包含我们需要的 MultiSelectList
构造函数所需的 IEnumerable
。我们可以继续实例化 MultiSelectList
并将其返回到我们的视图(请注意,我们假设我们处于某种 Edit
方法中,这是唯一真正需要预选 MultiSelect
列表中的项的情况)。
MultiSelectList teamsList = new MultiSelectList(db.Teams.ToList().OrderBy(i => i.Name),
"TeamId", "Name", playerTeamsIds);
EditPlayerViewModel model = new EditPlayerViewModel { Teams = teamsList };
4. 实现视图
对于视图,我们将在我们的 CreatePlayer
表单中使用 .NET MVC 的一个 HTML 辅助方法 - ListBoxFor
方法 - 它看起来如下:
@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>
</div>
<div class="form-group">
<input type="submit" value="Submit" class="btn btn-primary" />
</div>
}
查看上面的 ListBoxFor
辅助方法,我们首先看到 ListBox
是用于视图属性的 TeamIds
属性(这是方法的 <Model>
参数)。这意味着当用户选择一些团队然后提交表单时,在您的 Create [HttpPost]
方法中,您将收到用户在该 List
中选择的团队 Id
s。我们将在下面进行介绍。
ListBoxFor
方法的下一个参数是 MultiSelectList
(或 SelectList
),我们在 Model.Teams
中找到它(记住,这是我们创建的 teamsList
MultiSelectList
)。
最后一个属性是 htmlattributes
,我们可以在其中应用我们想要的类,或任何其他属性,如 Id
等。
5. 从视图模型接收数据
一旦用户提交表单,您将收到模型回到您的 [HttpPost] Create
方法,该方法将被构造为接受 CreateViewModel
参数。
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "Name, TeamIds")] CreatePlayerViewModel model)
{
...
}
请注意,我们只关心 TeamIds
属性,而不是 Teams
属性 - 它仅用于实例化 MultiSelectList
并且不包含我们想要的任何数据。
现在我们已经从视图收到了 TeamIds
,我们可以创建一个 new Player
。首先,我们将用 PlayerId
和 Name
属性来实例化 Player
,因为我们的 Player
模型不要求 Teams
属性包含数据。
if (ModelState.IsValid)
{
Player player = new Player
{
PlayerId = Guid.NewGuid(),
Name = model.Name
};
...
}
现在,我们需要找到属于我们从模型收到的 TeamIds
的 Teams
,并将它们分配给刚刚创建的玩家。假设我们在控制器中的某个位置已经实例化了一个 appDbContext
实例,名为 db
:
if (model.TeamIds != null)
{
foreach (var id in model.TeamIds)
{
// Convert the id to a Guid from a string
var teamId = Guid.Parse(id);
// Retrieve team from database...
var team = db.Teams.Find(teamId);
// ... and add it to the player's Team collection
try
{
player.Teams.Add(team);
}
catch (Exception ex)
{
return View("Error", new HandleErrorInfo(ex, "Players", "Index"));
}
}
}
// Add new Player to db & save changes
try
{
db.Players.Add(player);
db.SaveChanges();
}
catch (Exception ex)
{
return View("Error", new HandleErrorInfo(ex, "Players", "Index"));
}
现在,我们已经将用户选择的每个 Team
添加到了我们新创建的 Player
,player
中。
请注意,如果您处于编辑方法中,其中 Player
已经分配了一些 Teams
,这些 Teams
也将从 View
返回。在将任何 Teams
添加到 Player
之后,您将需要删除它们。
为此,我们只需要创建一个包含数据库中所有团队的团队列表,减去用户想分配给 Player 的团队。我们将使用 Except() 方法来实现这种差异。然后,我们可以遍历这个列表并对每个团队调用 Remove() 方法。
// Check if any teams were selected by the user in the form
if (model.TeamIds.Count > 0)
{
// First, we will instantiate a list to store each of the teams in the EditPlayerViewModel for later comparison
List<Team> viewModelTeams = new List<Team>();
// Now, loop over each of the ids in the list of TeamIds
foreach (var id in model.TeamIds)
{
// Retrieve the team from the db
var team = db.Teams.Find(Guid.Parse(id));
if (team != null)
{
// We will add the team to our tracking list of viewmodelteams and player teams
try
{
player.Teams.Add(team);
viewModelTeams.Add(team);
}
catch (Exception ex)
{
return View("Error", new HandleErrorInfo(ex, "Players", "Index"));
}
}
}
// Now we will create a list of all teams in the db, which we will "Except" from the new player's list
var allTeams = db.Teams.ToList();
// Now exclude the viewModelTeams from the allTeams list to create a list of teams that we need to delete from the player
var teamsToRemove = allTeams.Except(viewModelTeams);
// Loop over each of the teams in our teamsToRemove List
foreach (var team in teamsToRemove)
{
try
{
// Remove that team from the player's Teams list
player.Teams.Remove(team);
}
catch (Exception ex)
{
// Catch any exceptions and error out
return View("Error", new HandleErrorInfo(ex, "Players", "Index"));
}
}
}
// Save the changes to the db
try
{
db.Entry(player).State = EntityState.Modified;
db.SaveChanges();
}
catch (Exception ex)
{
return View("Error", new HandleErrorInfo(ex, "Players", "Indes"));
}
请注意,您可以在此 处 阅读 Except()
的工作原理。
结论
本文关于如何实现 MultiSelectLists
(也适用于 SelectLists
)的介绍到此结束。作者希望通过阅读和跟随提供的示例解决方案中的代码,读者能够节省一些时间来实现一个并非易于实现的东西,并且对于初学者来说,学习它可能是一个令人沮丧且耗时的过程。
一如既往,如果您是经验丰富的编码人员,发现任何缺陷或改进之处,请在下方发表评论。
关注点
对我来说,这部分最具挑战性的是创建一个带有预选值的 MultiSelectList
。Stack Overflow 上有很多信息,答案建议用户遍历 SelectListItems
列表并将其 "Selected
" 属性标记为 true
,然后将这些项添加到 MultiSelectList
或 SelectList
。然而,这会失败,因为 MultiSelectList
的 "SelectedValues
" 属性只能在其实例化时设置。
我花了很多时间试图弄清楚这一点,这促使我写了这篇文章,以帮助其他 .NET 新手节省时间和减少挫败感!
历史
- 2015 年 12 月 14 日:首次发布
- 2015 年 12 月 15 日:小幅编辑
- 2015 年 12 月 16 日:编辑并添加示例解决方案