MVC 5、Entity Framework 6 和多对多关系:循序渐进的视图模型方法






4.91/5 (29投票s)
使用 MVC5 和 Entity Framework 实现多对多关系的真实世界示例。
2014 年 10 月 27 日,asp.net 网站上的每日文章
引言
有很多关于 ASP.NET MVC 和 Model Binder 的文章。我没找到任何关于多对多关系 Model Binder 的真实世界的文章。所以我决定写这篇帖子,分享我在这个主题上的经验。尽管我会轻松地一步步解释我的方法,但本文的读者应该具备 ASP.NET MVC 中的基本概念的先决知识,如 Controller(控制器)、View(视图)、Model(模型)、Model Binder(模型绑定器)和 Scaffolding(脚手架)。
背景
在这篇文章中,我将使用 ASP.NET MVC 5、Entity Framework 6.1.1 和 Visual Studio 2012。要在 VS2012 中添加 MVC 5 支持,请查看此链接:ASP.NET and Web Tools 2013.1 for Visual Studio 2012。您可以通过 NuGet 添加 Entity Framework 6.1.1。我的方法可能不是最好的,但它效果很好。
多对多实体模型生成
我的示例来自一个真实世界的应用程序,一个面向雇主和求职者的 Web 工作门户。在我的示例中,我将使用数据库优先的方法。使用 Entity Framework,我们也可以通过模型优先方法开始我们的项目,设计我们的实体的模型,然后使用 Entity Framework 工具生成数据库架构。我不会再详细介绍,因为这不是帖子的主题。让我们继续。我们有以下数据库图:
JobPost
包含由 Employer
发布的工作公告。此外,Employer
可以为 JobPost
添加许多 JobTag
。在数据库中,这两张表通过一个链接表或连接表 `JobPost_JobTag` 相关联,该表没有附加信息。这张表只包含用于将两张表链接成多对多关系的外部键。我们创建一个名为 ManyToManyMVC5 的新 ASP.NET MVC5 Web 项目。
现在我们必须从 NuGet 添加 Entity Framework 6.1.1。我们已准备好创建一个模型并将这些表和关系导入到我们的项目中。
通过右键单击 Models 文件夹并选择 Add > New > ADO.NET Entity Data Model 来向项目添加一个新模型。
- 为项目指定名称 "JobPortal"
- 选择“从数据库生成”选项
- 使用向导创建数据库连接或连接到现有数据库。将 Web.Config 文件(底部选项)的连接字符串命名为 "JobPortalEntities"。
- 选择要添加到模型的 3 个表:Employer、JobPortal、JobTag 和 JobPortal_JobTag。勾选“复数或单数化生成的对象名称”选项,并保留其他设置为默认值。
现在您将看到生成模型的实体图。
请注意,我们在 Employer 和 JobPost 之间有一个一对多关系,在 JobPost 和 JobTag 之间有一个多对多关系。请注意,链接表 JobPost_JobTag 在我们的模型中没有表示为实体。这是因为我们的链接表没有附加信息(即它没有标量属性)。如果我们有附加信息,我们就不应该有多对多关系,而应该有一个与 JobPost 和 JobTag 具有一对多关系的第四个实体。让我们继续。在 Models 文件夹中,我们有 JobPortal.edmx 文件,这是我们模型的物理表示。我们可以展开它,然后展开 JobPortal.tt。我们看到后者包含我们生成的实体的 .cs 文件。查看 JobPost.cs 和 JobTag.cs。
public partial class JobPost
{
public JobPost()
{
this.JobTags = new HashSet<JobTag>();
}
public int Id { get; set; }
public string Title { get; set; }
public int EmployerID { get; set; }
public virtual Employer Employer { get; set; }
public virtual ICollection<JobTag> JobTags { get; set; }
}
public partial class JobTag
{
public JobTag()
{
this.JobPosts = new HashSet<JobPost>();
}
public int Id { get; set; }
public string Tag { get; set; }
public virtual ICollection<JobPost> JobPosts { get; set; }
}
请注意,这两个实体之间有一个 ICollection 属性。
ASP.NET MVC5 和多对多关系
在网上冲浪,您会找到许多关于 Model View Controller ASP.NET 模式以及如何轻松创建 MVC 项目的有趣文章和帖子。但在真实世界中,情况有所不同,我们需要改进我们的方法。
在 ASP.NET MVC 中,我们可以使用 Scaffolding 快速生成应用程序的基本框架,我们可以对其进行编辑和自定义。因此,我们可以自动创建控制器和强类型视图来对我们的实体执行基本的 CRUDL 操作。不幸的是,ASP.NET MVC 的 scaffolding 无法处理多对多关系。我认为主要原因是存在太多种类的多对多用户界面创建/编辑方式。
另一个问题与自动 ASP.NET MVC Model Binding(模型绑定)有关。我们知道,模型绑定允许您将 HTTP 请求数据映射到模型。模型绑定使您能够轻松处理表单数据,因为请求数据(POST/GET)会自动传输到您指定的模型。ASP.NET MVC 在后台借助 Default Binder 来实现这一点。如果我们有一个多对多关系的用户界面,我们将有一种允许用户进行多项选择的界面。因此,我们需要一个复杂的类型来绑定所选项目,如 Check Box Group 或 List,或 Multiple List Box。但让我们看看实际的问题。
我们将使用 Scaffolding 来生成一个基于 Entity Framework 的基本控制器,包含 CRUDL 方法和相关视图。右键单击 Controllers 文件夹并选择 Add > Controller。在 Add Scaffold 中,选择“MVC5 Controller with views, using Entity Framework”。将接下来的表单设置为下图所示,然后单击 Add。
注意:如果您收到一条错误消息,提示“There was an error getting the type...”(获取类型时出错...),请确保在添加类后构建了 Visual Studio 项目。Scaffolding 使用反射来查找类。
MVC5 Scaffolding 将在 Controllers 文件夹中生成一个文件 `JobPostController.cs`,在 Views 文件夹的 `JobPost` 子文件夹中生成相关 CRUDL 方法的视图。在运行应用程序之前,我们必须修改 `RouteConfig.cs` 文件(位于 App_Start),将 `JobPostController` 设置为默认控制器。
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "JobPost", action = "Index", id = UrlParameter.Optional }
);
}
}
现在您可以运行应用程序并浏览页面。这太神奇了……!!但是……我们没有看到任何多对多关系的痕迹!!!
视图模型方法
为了克服这些问题,我的方法基于 ViewModel(视图模型)实体。视图模型表示您想在视图中显示的数据。简单来说,它是一个在特定视图中使用的模型数据的类。在我们的例子中,我们的视图模型包含 JobPost 的数据、所有可用 JobTag 的数据以及选定的 JobTag 的数据。
我们开始修改 Index 视图(它与 JobPostController 的 index Action 相关联),让它在 Listbox 中显示与 JobPost 关联的 JobTag 列表。因此,我们在 index.cshtml 中的表格中添加了一行。
// This row to the header table
<th>
@Html.DisplayNameFor(model => model.JobTags)
</th>
// This row to the body table
<td>
@Html.ListBox("Id", new SelectList(item.JobTags,"Id","Tag"))
</td>
现在我们可以运行应用程序了……在 Index 视图中,您将看到一个包含该帖子所有 Tag 的 ListBox。这很简单。
下一步是修改编辑视图。当用户编辑一个职位发布时,我们希望显示一个 ListBox,其中包含所有可用的工作标签列表,其中选中的是与该职位发布关联的那些。用户可以更改选择或其他数据,并将其提交回控制器以持久化数据。
第一个问题是将所有可用的工作标签列表传递给视图。第二个问题是将关联的标签标记为已选。现在轮到 Model View 了!!
在您的解决方案中创建一个名为 ViewModel 的文件夹,并向其中添加一个名为 "JobViewMode.cs" 的类文件,代码如下:
public class JobPostViewModel
{
public JobPost JobPost { get; set; }
public IEnumerable<SelectListItem> AllJobTags { get; set; }
private List<int> _selectedJobTags;
public List<int> SelectedJobTags
{
get
{
if (_selectedJobTags == null)
{
_selectedJobTags = JobPost.JobTags.Select(m => m.Id).ToList();
}
return _selectedJobTags;
}
set { _selectedJobTags = value; }
}
}
我们的视图模型包含一个存储 JobPost 的属性,一个存储与 JobPost 关联的 JobTag 的属性(作为 JobTag ID 的 `List
我们将 GET associated 的 Action `edit` 修改如下,我们将 `JobPostViewModel` 引入而不是 `JobPost`。
// GET: /JobPost/Edit/5
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var jobPostViewModel = new JobPostViewModel {
JobPost = _db.JobPosts.Include(i => i.JobTags).First(i => i.Id == id),
};
if (jobPostViewModel.JobPost == null)
return HttpNotFound();
var allJobTagsList = _db.JobTags.ToList();
jobPostViewModel.AllJobTags = allJobTagsList.Select(o => new SelectListItem
{
Text = o.Tag,
Value = o.Id.ToString()
});
ViewBag.EmployerID =
new SelectList(db.Employers, "Id", "FullName", jobpostViewModel.JobPost.EmployerID);
return View(jobpostViewModel);
}
注意:由于您还没有更改视图的类型模型,因此您会收到来自 `return View` 字段的错误。忽略它。在您修改相关视图后,它就会消失。
在修改后的 Action 方法中,我们使用 `JobPostView` 实体。我们加载 `JobPost` 属性,其中包含选定的职位发布,并预先加载 `JobTags` 实体,并将 `AllJobTags` 属性加载为由 `JobTags` 构建的 `ListItem` 列表,并将 ViewModel 返回给视图而不是 Model。现在我们可以修改视图。我们将 ModelBinder 更改为 `ViewModelJobPost` 以及所有属性。我们添加 ListBox 绑定。
@model ManyToManyMVC5.ViewModels.JobPostViewModel
@{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>JobPost</h4>
<hr />
@Html.ValidationSummary(true)
@Html.HiddenFor(model => model.JobPost.Id)
<div class="form-group">
@Html.LabelFor(model => model.JobPost.Title, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.JobPost.Title)
@Html.ValidationMessageFor(model => model.JobPost.Title)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.JobPost.EmployerID, "EmployerID", new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.DropDownListFor(m => m.JobPost.EmployerID,
(SelectList)ViewBag.EmployerID,
Model.JobPost.Employer.Id);
@Html.ValidationMessageFor(model => model.JobPost.EmployerID)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model=>model.AllJobTags,"JobTag",new {@class="control-label col-md-2"})
<div class="col-md-10">
@Html.ListBoxFor(m => m.SelectedJobTags, Model.AllJobTags)
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
<script src="~/Scripts/jquery-2.1.1.min.js"></script>
<script src="~/Scripts/jquery.validate.min.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
我们可以通过多选来选择多个 JobTag,如下所示。
保存后,以下是我们 Index 页面上可以看到的内容。
让我们看一下背后发生机制。从编辑页面,当我们保存时,会触发一个 POST 命令,如下所示:
注意查询字符串使用了 "JobPost.<FieldName>",并且我们有多个 SelectedJobTag。
理解 MVC 自动绑定器机制非常重要。当我们单击“保存”时,Action
public ActionResult
Edit(
jobpostView)JobPostViewModel
从 `JobStatus` 控制器被调用。MVC5 由于其 ModelBinder,会自动将查询字符串中的值映射到 Action 中注入的 `JobPostViewModel` 类的相关属性。我们可以使用 Action 的 `[Bind]` 装饰器来覆盖这种机制,以处理更复杂的 ViewModel 对象。但这可能是另一篇文章的主题。我希望我给了您对 ASP.NET MVC 绑定幕后机制的初步了解。
结论
ASP.NET MVC5 及其 Scaffolding 机制在实际应用程序中常常存在巨大的局限性。在本文中,我尝试通过一个关于多对多关系实现的简单示例,向您解释如何在实际应用程序中使用 ASP.NET MVC。
这个例子确实非常基础,但我希望您已经理解了 Binder 和 ViewModel 背后的思想,这样您就可以开始自己修改随附示例项目中的“创建”功能,从而进入下一个层次。
人们可能会考虑使用 PropertyBag/ViewBag 等其他机制来代替 ViewModel,例如将其他数据传递给视图。您将失去 AutoBind 机制,并且从 S.O.L.I.D. 原则、TDD 方法和面向对象设计的角度来看,这绝对是错误的。