65.9K
CodeProject 正在变化。 阅读更多。
Home

在 .NET MVC 中实现 MultiSelectList 的分步指南

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (17投票s)

2015 年 12 月 13 日

CPOL

12分钟阅读

viewsIcon

125371

这是一个简单的分步指南,介绍如何在 .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 视图中实现一个多选列表,允许用户为他们创建的新玩家选择/分配多个团队。

文章将遵循特定的结构

  1. 模型 - 本节将介绍我们使用的模型,重点关注实现中的两个重要属性:Team 和 Player,以及支撑本文目标的数据库关系。
  2. 实现视图模型 - 我们将为 Player Create 视图实现一个视图模型。它将包含视图满足我们领域模型需求所需的一切,从而提供灵活性并与领域模型分离。
  3. 实现控制器要求 - 活动的核心将是实现控制器中所需的组件,以将数据解析成 .NET 的 HTML 辅助方法可用的内容。
  4. 实现视图 - 本次练习的最后一部分是实际实现我们要显示给用户的 ListBox
  5. 从视图模型接收数据 - 在这里,我们将查看用户提交表单后,数据如何回到 [HttpPost] 控制器操作。

1. 模型

我们将从基本的 MVC 设置开始。我有一些简单的模型 - PlayerTeam。我使用 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 的几个重要细节

  • 我们在模型本身中实例化了 TeamsPlayers 的集合(请参阅 this.Players = new List<Player>(); 行以及 Team 模型中的等效行)。
  • 重写 OnModelCreating 类允许我们定义如何创建 TeamsPlayers 之间的关系。因为我们想要一个多对多关系,我们定义的内容将在数据库中创建一个名为 "Team_Player" 的连接表,该表将包含两个主键列,名为 "TeamId"(它将使用我们 Team 模型的 TeamId 属性)和 "PlayerId"(它将使用我们 Player 模型的 PlayerId 属性)。这是一个独立的主题;有关更多详细信息,请参阅本文本节开头处的链接。

从这一点开始,我们将跳过一些步骤,例如实际创建控制器和视图,并假定它们已经就位但需要修改。具体来说,我们将假定视图或控制器中不存在我们要实现的多选列表的基础结构。

2. 实现视图模型

对于视图模型,我们需要满足几个要求,以便在控制器和视图之间传递数据,然后在控制器中解析数据以满足我们 Player 模型的要求。

  • 我们需要一个 .NET 的 MultiSelectList 实例,我们将用 Team 的信息填充它。
  • 我们需要一个 .NET 的 List<> 实例,我们将使用它来存放用户选择的 Team 的 Id(使其成为 TeamIdList<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 中突出显示来看到已分配给 PlayerTeam

选项 1

首先,我们需要创建一个 appDbContext 实例,然后使用 LINQ 将团队存储在一个变量中(具体来说,是一个 List)。

appDbContext db = new appDbContext();

var teams = db.Teams.ToList();

然后,我们需要实例化一个 SelectListItems 列表 - 即填充 SelectListMultiSelectList 的对象。

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");

然后,我们只需要将其分配给 CreatePlayerViewModelTeams 属性,我们需要先实例化它,然后再将其返回到我们的视图。

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)只允许您在实例化时告诉它哪些项被选中 - 它不会学习 SelectListItemsSelected 属性。

我们将使用一个示例目标来演示这一点。我们想要"选择" Player 已经分配到的若干团队。

我们首先需要做的是决定我们将使用 MultiSelectList 对象的哪个构造函数。再次参考 Microsoft 的文档,我们将使用 MultiSelectList(IEnumerable, string, string, IEnumerable),它与之前相同,只是增加了一个 IEnumerable 参数。

这个 IEnumerable 需要存储我们想让列表选择。它们需要与第一个 IEnumerable 参数中的相对应 - 即 teams 列表中的 TeamId 属性。

为此,我们需要首先创建一个单独的团队列表,我们将其命名为 playerTeams,使用 LINQ 过滤 teams 列表以获取我们想要的。我们将假设我们通过某个参数接收了我们想要过滤的 PlayerId,称之为 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 中选择的团队 Ids。我们将在下面进行介绍。

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。首先,我们将用 PlayerIdName 属性来实例化 Player,因为我们的 Player 模型不要求 Teams 属性包含数据。

if (ModelState.IsValid)
{
    Player player = new Player
    {
        PlayerId = Guid.NewGuid(),
        Name = model.Name
    };
    
    ...
}

现在,我们需要找到属于我们从模型收到的 TeamIdsTeams,并将它们分配给刚刚创建的玩家。假设我们在控制器中的某个位置已经实例化了一个 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 添加到了我们新创建的 Playerplayer 中。

请注意,如果您处于编辑方法中,其中 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,然后将这些项添加到 MultiSelectListSelectList。然而,这会失败,因为 MultiSelectList 的 "SelectedValues" 属性只能在其实例化时设置。

我花了很多时间试图弄清楚这一点,这促使我写了这篇文章,以帮助其他 .NET 新手节省时间和减少挫败感!

历史

  • 2015 年 12 月 14 日:首次发布
  • 2015 年 12 月 15 日:小幅编辑
  • 2015 年 12 月 16 日:编辑并添加示例解决方案
© . All rights reserved.