您的位置:首页 > 编程语言 > ASP

[渣译文] 使用 MVC 5 的 EF6 Code First 入门 系列:为ASP.NET MVC应用程序更新相关数据

2014-05-13 10:23 405 查看
这是微软官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻译,这里是第八篇:为ASP.NET MVC应用程序更新相关数据

原文:

Updating Related Data with the Entity Framework in an ASP.NET MVC Application
译文版权所有,谢绝全文转载——但您可以在您的网站上添加到该教程的链接。

在之前的教程中您已经成功显示了相关数据。在本教程中你将学习如何对相关数据进行更新。对于大多数关系,可以从主键或者导航属性来进行更新。对于多对多关系,实体框架不会直接公开连接表,所以你可以从相应的导航属性添加和移除实体。

下面的截图显示了你将要实现的页面。

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@ViewBag.Title - Contoso 大学</title>
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
@Html.ActionLink("Contoso 大学", "Index", "Home", null, new { @class = "navbar-brand" })
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li>@Html.ActionLink("主页", "Index", "Home")</li>
<li>@Html.ActionLink("关于", "About", "Home")</li>
<li>@Html.ActionLink("学生", "Index", "Student")</li>
<li>@Html.ActionLink("教师", "Index", "Instructor")</li>
<li>@Html.ActionLink("课程", "Index", "Course")</li>
<li>@Html.ActionLink("系", "Index", "Department")</li>
</ul>
</div>
</div>
</div>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>© @DateTime.Now.Year - Contoso 大学</p>
</footer>
</div>

@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")
@RenderSection("scripts", required: false)
</body>
</html>


View Code

以及Course的模型, public int CourseID { get; set; }的Display特性应为[Display(Name = "编号")]

如果你在看到下记日期之前就跟随教程进行了演练,请将上面两点更正,谢谢。

2014-5-12

运行应用程序,打开课程的创建页面(显示课程索引页面并单击创建新的)并输入新课程的数据:



单击创建,课程索引页会显示你刚才新建的课程。同时索引页面的洗名称是来自导航属性的,表示关系已经正确建立。



点击编辑超链接来运行编辑页。



更改页面上的数据并保存,检查数据是否被正确地保存并显示。

为讲师添加编辑页面

当您编辑一名讲师的记录时,你希望能够更新讲师的办公室分配情况。讲师实体和办公室分配实体之间有一个一到零或一的关系。这意味着您必须处理下列情况:

如果用户清除了办公室分配情况并且讲师原来拥有一个,您必须移除并删除这个OfficeAssignment实体。

如果用户输入了一个办公室并且原来讲师并没有分配,您必须新建一个OfficeAssignment实体。

如果用户更改办公室分配值,你必须更改已经存在的OfficeAssignment实体。

打开InstructorController.cs,检查Edit的HttpGet 方法:

public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Instructor instructor = db.Instructors.Find(id);
if (instructor == null)
{
return HttpNotFound();
}
ViewBag.ID = new SelectList(db.OfficeAssignments, "InstructorID", "Location", instructor.ID);
return View(instructor);
}


脚手架生成的代码并不是你想要的。它设置了一个下拉列表,但你需要一个文半框。使用下面的代码替换原来的:

public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Instructor instructor = db.Instructors
.Include(i => i.OfficeAssignment)
.Where(i => i.ID == id)
.Single();
if (instructor == null)
{
return HttpNotFound();
}
return View(instructor);
}


这段代码删除了ViewBag语句并针对关联的OfficeAssignment实体添加了预先加载的。你不能在Find方法上使用预先加载。所以这里使用了Where和Single方法来选择讲师。

下面的代码替换HttpPost的Edit方法。用来处理办公室分配更新:

[HttpPost, ValidateAntiForgeryToken, ActionName("Edit")]
public ActionResult EditPost(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var instructorToUpdate = db.Instructors
.Include(i => i.OfficeAssignment)
.Where(i => i.ID == id)
.Single();
if (TryUpdateModel(instructorToUpdate, "",
new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" }))
{
try
{
if (string.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
db.Entry(instructorToUpdate).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
catch (RetryLimitExceededException)
{
ModelState.AddModelError("", "无法保存更改,请重试或联系管理员");
}
}
return View(instructorToUpdate);
}


然后添加下列引用:

using System.Data.Entity.Infrastructure;


这段代码执行了以下操作:

将方法名称变更为EditPost因为签名现在和HttpGet方法的一样。(依然使用ActionName特性指定的Edit URL)

使用延迟加载来从数据库中获取当前讲师实体的OfficeAssignment导航属性。和你在HttpGet Edit方法中所做的一样。

从模型绑定器来更新检索到的Instructor实体,使用TryUpdateModel重载允许你指定你想要包括的属性值的白名单,这样可以防止过多发布攻击,如教程第二节中所述。

if (TryUpdateModel(instructorToUpdate, "",
new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" }))


如果办公室位置为空,将Instructor.OfficeAssignment属性设置为null,在OfficeAssignment表中的相关行都将被删除。

if (string.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
{
instructorToUpdate.OfficeAssignment = null;
}


将所做的更改保存到数据库中。

在Edit视图中,在雇佣日期字段的div元素之后,添加一个新的字段来编辑办公室地址:

<div class="form-group">
@Html.LabelFor(model => model.OfficeAssignment.Location, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.OfficeAssignment.Location)
@Html.ValidationMessageFor(model => model.OfficeAssignment.Location)
</div>
</div>


运行该页面(选择教师选项卡,然后点击编辑讲师),更改办公室位置并保存。



为教师编辑页面添加课程分配

教师能够教授任意数量的课程。现在您会通过使用一组复选框来添加更改课程分配的功能,如下所示:



Course和Instructor实体之间的关系是多对多,这意味着您不需要直接访问连接表中的外键属性。相反,你可以从Istructor.Courses导航属性中添加和移除实体。

UI使您能够更改使用一组复选框来表示哪些课程是已经分配给教师的。在数据库中的每一门课程都使用一个复选框来显示,包括哪些已经分配给教师的。用户可以通过选择或清除复选框来更改课程分配。如果课程数目太多,你可能想要在视图中使用不同的显示数据的方法,但你会用同样的方法来操作导航属性以创建或删除关系。

为了给视图提供复选框的列表,您会使用ViewModel类,在ViewModels文件夹中创建AssignedCourseData.cs并使用下面的代码替换自动生成的:

namespace ContosoUniversity.ViewModels
{
public class AssignedCourseData
{
public int CourseID { get; set; }
public string Title { get; set; }
public bool Assigned { get; set; }
}
}


在InstructorController.cs中,使用下面的代码替换HttpGet的Edit方法,高亮部分是你进行的更改:

public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Instructor instructor = db.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.Where(i => i.ID == id)
.Single();
PopulateAssignedCourseData(instructor);
if (instructor == null)
{
return HttpNotFound();
}
return View(instructor);
}

private void PopulateAssignedCourseData(Instructor instructor)
{
var allCourse = db.Courses;
var instructorCourses = new HashSet<int>(instructor.Courses.Select(c => c.CourseID));
var viewModel = new List<AssignedCourseData>();
foreach (var course in allCourse)
{
viewModel.Add(new AssignedCourseData
{
CourseID = course.CourseID,
Title = course.Title,
Assigned = instructorCourses.Contains(course.CourseID)
});
}
ViewBag.Courses = viewModel;
}


该代码对Courses导航属性进行了预先加载,并且调用了一个新的PopulateAssignedCourseData方法使用AssignedCourseData视图模型类来为复选框数组提供信息。

PopulateAssignedCourse方法中的代码通过读取所有Course实体并使用模型视图类以加载列表。在每个课程中,代码检查讲师的Courses导航属性中是否存在该课程。为了创建一个高效的检查一个课程是否指派给教师,已经分配的课程被放入一个HashSet集合。当课程已分配时,Assigned属性为True。视图会使用该属性来确定哪些复选框应当显示为已选定。最后,该列表作为ViewBag属性被传递到视图上。

下一步,添加用户单击保存时应当执行的代码。调用一个新方法来更新Instructor实体的Courses导航属性,使用下面的代码替换EditPost方法,高亮部分是你进行的更改:

        [HttpPost, ValidateAntiForgeryToken]
public ActionResult Edit(int? id,string[] selectedCourses)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var instructorToUpdate = db.Instructors
.Include(i => i.OfficeAssignment)
.Include(i =>i.Courses)
.Where(i => i.ID == id)
.Single();
if (TryUpdateModel(instructorToUpdate, "", new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" }))
{
try
{
if (string.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location)) { instructorToUpdate.OfficeAssignment = null; }
UpdateInstructorCourses(selectedCourses, instructorToUpdate);
db.Entry(instructorToUpdate).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
catch (RetryLimitExceededException)
{
ModelState.AddModelError("", "无法保存更改,请重试或联系管理员");
}
}
PopulateAssignedCourseData(instructorToUpdate);
return View(instructorToUpdate);
}

private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.Courses = new List<Course>();
return;
}

var selectedCoursesHS = new HashSet<string>(selectedCourses);
var instructorCourses = new HashSet<int>(instructorToUpdate.Courses.Select(c => c.CourseID));
foreach (var course in db.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Add(course);
}
}
else
{
if (instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Remove(course);
}
}
}
}


由于现在方法签名和HttpGet的Edit方法不同,所以该方法的名称也从EditPost返回到Edit。

由于视图没有课程实体的集合,所以模型绑定器不能自动更新Courses导航属性。不同于使用模型绑定器来更新Course导航属性,你将在UpdateInstructorCourses方法中进行更新。因此,您需要将Course属性从模型绑定器中排除。这不需要更改任何代码,因为你正在使用的白名单重载列表中没有包含Courses。

如果没有复选框被选中,UpdateInstructorCourses中的代码使用一个空集合来初始化Courses导航属性。

if (selectedCourses == null)
{
instructorToUpdate.Courses = new List<Course>();
return;
}


该代码通过循环数据库中的所有课程,检查哪些课程是分配给教师的来决定是否在视图中应当选中它们。为了进行高效查找,它们都存储在HashSet对象中。

如果某个课程的复选框被选中但该课程并不在Instructor.Courses导航属性中,课程将被添加到导航属性的集合。

if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Add(course);
}
}


如果课程的复选框没有被选中,但课程是在Instructor.Courses导航属性中,该课程将被从导航属性中移除。

else
{
if (instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Remove(course);
}
}


在Edit视图中,在办公室分配字段的div元素之后,保存按钮之前插入一个Courses字段的复选框组。

<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<table>
<tr>
@{
int cnt = 0;
List<ContosoUniversity.ViewModels.AssignedCourseData> courses = ViewBag.Courses;

foreach (var course in courses)
{
if (cnt++ % 3 == 0)
{
@:</tr><tr>

}
@:<td>
<input type="checkbox"
name="selectedCourses"
value="@course.CourseID"
@(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) />
@course.CourseID @:  @course.Title
@:</td>
}
@:</tr>
}
</table>
</div>
</div>


如果你在粘贴代码后发现换行与缩进不像上图中那样,你必须手动修复成上面代码所示的那样。代码缩进可能不完美,但你要保证@:</tr><tr>、@:<td>、@:</td>和@:</tr>在一行上,否则就会出现运行时错误。

这段代码创建了一个HTML表格,其中包含三列。在每一列中显示了课程的编号和标题以及一个复选框。所有的复选框都使用同一个name"selectedCourses",通知模型绑定器将它们作为一个组来进行处理。每个复选框的Value属性被设定为CourseID的值,当页面提交时,模型绑定器将一个仅包含了已选择复选框的CourseID值作为数组传递给控制器。

复选框最初呈现时,已经分配给教师的课程会带有checked特性,被设置为选中状态。

在你更改课程分配后,你会想要能够返回索引页来验证这些更改。因此,您需要将课程列添加到页面的表格中。在这种情况下你不需要使用ViewBag对象,因为你想要显示的信息已经在Instructor实体的Courses导航属性中并作为模型传递给视图了。

在Views\Instructor\Index.cshtml中,在办公室标题后添加课程标题,如下图所示:

<table class="table">
<tr>
<th>
Last Name
</th>
<th>
First Name
</th>
<th>
Hire Date
</th>
<th>
Office
</th>
<th>
Courses
</th>
<th></th>
</tr>


然后在办公室地址详细单元格后添加一个新的单元格来显示课程:

<td>
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
</td>
<td>
@{
foreach (var course in item.Courses)
{
@course.CourseID @: @course.Title <br />
}
}
</td>
<td>
@Html.ActionLink("Select", "Index", new { id = item.ID }) |
@Html.ActionLink("Edit", "Edit", new { id = item.ID }) |
@Html.ActionLink("Details", "Details", new { id = item.ID }) |
@Html.ActionLink("Delete", "Delete", new { id = item.ID })
</td>


运行应用程序,在教师索引页上,你可以看到分配给每个教师的课程:



更改某位教师的课程分配并保存,查看更改是否已经成功保存到数据库。



注意:这里使用复选框的方式仅针对数量有限的课程,对于更大的集合,你可能需要不同的UI及更新方法。

更新DeleteConfirmed方法

在InstructorController.cs中,更改Deleteconfirmed方法,如下面的代码所示:

[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
Instructor instructor = db.Instructors
.Include(i => i.OfficeAssignment)
.Where(i => i.ID == id)
.Single();
instructor.OfficeAssignment = null;
db.Instructors.Remove(instructor);

var department = db.Departments
.Where(d => d.InstructorID == id)
.SingleOrDefault();
if (department != null)
{
department.InstructorID = null;
}
db.SaveChanges();
return RedirectToAction("Index");
}


这段代码进行了两处更改:

当教师被删除时,办公室分配记录也被删除(如果有)。

如果教师被分配作为系主任,则从该系中移除该教师。如果在没有该段代码的情况下你尝试删除以为已经被分配为系主任的教师,你会收到一个完整性错误。

将办公地点和课程添加到创建页面

在InstructorController.cs,修改HttpGet和HttpPost的Create方法,如下面代码所示:

public ActionResult Create()
{
var instructor = new Instructor();
instructor.Courses = new List<Course>();
PopulateAssignedCourseData(instructor);
return View();
}

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "LastName,FirstMidName,HireDate,OfficeAssignment")]Instructor instructor, string[] selectedCourses)
{
if (selectedCourses != null)
{
instructor.Courses = new List<Course>();
foreach (var course in selectedCourses)
{
var courseToAdd = db.Courses.Find(int.Parse(course));
instructor.Courses.Add(courseToAdd);
}
}
if (ModelState.IsValid)
{
db.Instructors.Add(instructor);
db.SaveChanges();
return RedirectToAction("Index");
}
PopulateAssignedCourseData(instructor);
return View(instructor);
}


这段代码和之前你在Edit方法中看到的类似,除了最初没有课程被选择。HttpGet的Create方法调用PopulateAssignedCourseData方法不是因为有可能有课程被选择,而是为了提供一个空集合用于在视图中循环。(否则会抛出一个空引用异常)

HttpPost的Create方法在将每个选择的课程添加到课程导航属性及将新教师添加到数据库前进行错误检查。在模型有错误(例如,用户输入的无效日期)时,课程不会被添加。在页面重新显示一条错误信息时,所做的任何课程选择都会被还原。

请注意,为了能将课程添加到Courses导航属性中,你必须初始化一个空集合:

instructor.Courses = new List<Course>();


作为另一种替代方法,你可以在Course模型中修改属性getter设置器来在它不存在时自动创建一个集合,如下面的代码所示:

private ICollection<Course> _courses;
public virtual ICollection<Course> Courses
{
get
{
return _courses ?? (_courses = new List<Course>());
}
set
{
_courses = value;
}
}


如果您使用上面的方法修改了模型代码,您可以再控制器中删除初始化空集合的代码。

在Views\Instructor\Create.cshtml中,在雇佣日期和提交按钮之间添加办公室地址和课程,如下面的代码所示:

<div class="form-group">
@Html.LabelFor(model => model.OfficeAssignment.Location, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.OfficeAssignment.Location)
@Html.ValidationMessageFor(model => model.OfficeAssignment.Location)
</div>
</div>

<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<table>
<tr>
@{
int cnt = 0;
List<ContosoUniversity.ViewModels.AssignedCourseData> courses = ViewBag.Courses;

foreach (var course in courses)
{
if (cnt++ % 3 == 0)
{
@:</tr><tr>
}
@:<td>
<input type="checkbox"
name="selectedCourses"
value="@course.CourseID"
@(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) />
@course.CourseID @:  @course.Title
@:</td>
}
@:</tr>
}
</table>
</div>
</div>


记得在粘贴代码后调整@:的缩进,跟之前你在Edit视图中所做的一样。

运行应用程序并尝试创建一名教师。



处理事务

正如在基本CRUD功能教程中所解释的那样,实体框架默认会隐式地实现事务,在你需要更多的控制时,请参阅MSDN上的Working with Transactions

总结

现在你已经完成了本教程的全部相关数据。到目前为止你都是通过同步IO来进行工作的,我们会在下一节中介绍如何通过异步IO来更有效地使用服务器资源。

作者信息


Tom Dykstra - Tom Dykstra是微软Web平台及工具团队的高级程序员,作家。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐