The Contoso University sample web application demonstrates how to create ASP.NET Core 1.0 MVC web applications using Entity Framework Core 1.0 and Visual Studio 2015. For information about the tutorial series, see the first tutorial in the series.html
In the previous tutorial you displayed related data; in this tutorial you’ll update related data by updating foreign key fields and navigation properties.ios
在前面的教程中完成了显示关系数据的工做。在本教程中,将经过更新外键字段和导航属性更新关系数据。web
The following illustrations show some of the pages that you’ll work with.redis
下面的几张图展现了将要处理的几个页面。数据库
Sections:c#
When a new course entity is created, it must have a relationship to an existing department. To facilitate this, the scaffolded code includes controller methods and Create and Edit views that include a drop-down list for selecting the department. The drop-down list sets the Course.DepartmentID
foreign key property, and that’s all the Entity Framework needs in order to load the Department
navigation property with the appropriate Department entity. You’ll use the scaffolded code, but change it slightly to add error handling and sort the drop-down list.并发
建立一个课程实体时,必须有一个指向已有系的关系。为了实现该功能,基架生成的控制器内的方法以及Create和Edit视图,包含一个下拉列表以选择系的信息。下拉列表设置了Course.DepartmentID
外键属性,这就是全部EF所须要的------为了加载适当的Department实体的Department导航属性。mvc
In CoursesController.cs, delete the four Create and Edit methods and replace them with the following code:app
在CoursesController.cs文件中,删除四个Create以及Edit方法,将其替换为下来代码:asp.net
public IActionResult Create() { PopulateDepartmentsDropDownList(); return View(); } [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Create([Bind("CourseID,Credits,DepartmentID,Title")] Course course) { if (ModelState.IsValid) { _context.Add(course); await _context.SaveChangesAsync(); return RedirectToAction("Index"); } PopulateDepartmentsDropDownList(course.DepartmentID); return View(course); } public async Task<IActionResult> Edit(int? id) { if (id == null) { return NotFound(); } var course = await _context.Courses .AsNoTracking() .SingleOrDefaultAsync(m => m.CourseID == id); if (course == null) { return NotFound(); } PopulateDepartmentsDropDownList(course.DepartmentID); return View(course); } [HttpPost, ActionName("Edit")] [ValidateAntiForgeryToken] public async Task<IActionResult> EditPost(int? id) { if (id == null) { return NotFound(); } var courseToUpdate = await _context.Courses .SingleOrDefaultAsync(c => c.CourseID == id); if (await TryUpdateModelAsync<Course>(courseToUpdate, "", c => c.Credits, c => c.DepartmentID, c => c.Title)) { try { await _context.SaveChangesAsync(); } catch (DbUpdateException /* ex */) { //Log the error (uncomment ex variable name and write a log.) ModelState.AddModelError("", "Unable to save changes. " + "Try again, and if the problem persists, " + "see your system administrator."); } return RedirectToAction("Index"); } PopulateDepartmentsDropDownList(courseToUpdate.DepartmentID); return View(courseToUpdate); }
After the Edit
HttpPost method, create a new method that loads department info for the drop-down list.
在Edit HttpPost方法后,新建一个方法,目的是为下拉列表加载系的信息。
private void PopulateDepartmentsDropDownList(object selectedDepartment = null) { var departmentsQuery = from d in _context.Departments orderby d.Name select d; ViewBag.DepartmentID = new SelectList(departmentsQuery.AsNoTracking(), "DepartmentID", "Name", selectedDepartment); }
The PopulateDepartmentsDropDownList
method gets a list of all departments sorted by name, creates a SelectList
collection for a drop-down list, and passes the collection to the view in ViewBag
. The method accepts the optional selectedDepartment
parameter that allows the calling code to specify the item that will be selected when the drop-down list is rendered. The view will pass the name “DepartmentID” to the <select>
tag helper, and the helper then knows to look in the ViewBag
object for a SelectList
named “DepartmentID”.
PopulateDepartmentsDropDownList 方法获取按名字排序的系名表,为下拉列表新建了一个SelectList集合,并将集合传递给ViewBag中的视图。该方法接收可选的SelectedDepartment参数,当下拉列表完成指定后,该参数容许调用的代码指定被选择的项目。视图将把“DepartmentID”名传递给<select>标签助手,接着标签助手就知道从ViewBag对象中找到名为“DepartmentID”的SelectList。
The HttpGet Create
method calls the PopulateDepartmentsDropDownList
method without setting the selected item, because for a new course the department is not established yet:
HttpGet的Create方法调用PopulateDepartmentsDropDownList 方法时没有设置被选项,由于对于一个新课程来讲,尚未设置系信息:
public IActionResult Create() { PopulateDepartmentsDropDownList(); return View(); }
The HttpGet Edit
method sets the selected item, based on the ID of the department that is already assigned to the course being edited:
HttpGet的Edit方法基于系的ID设置被选项,该ID值已经指向了正在编辑中的课程:
public async Task<IActionResult> Edit(int? id) { if (id == null) { return NotFound(); } var course = await _context.Courses .AsNoTracking() .SingleOrDefaultAsync(m => m.CourseID == id); if (course == null) { return NotFound(); } PopulateDepartmentsDropDownList(course.DepartmentID); return View(course); }
The HttpPost methods for both Create
and Edit
also include code that sets the selected item when they redisplay the page after an error. This ensures that when the page is redisplayed to show the error message, whatever department was selected stays selected.
在一个错误后再次显示在页面上时,Create和Edit的HttpPost方法一样也包括了设置被选项的代码。这样就保证了当页面被从新显示一个错误信息时,不管选择了哪一个系,将仍然停留在被选项上。
To enable the Course Details and Delete pages to display department data, open CoursesController.cs
and add eager loading for department data, as shown below. Also add AsNoTracking
to optimize performance.
要使课程的Details和Delete页面显示系的数据,请打开CoursesController.cs 文件,接着为系的数据添加预加载功能,以下所示。一样为了优化性能添加AsNoTracking。
public async Task<IActionResult> Details(int? id) { if (id == null) { return NotFound(); } var course = await _context.Courses .Include(c => c.Department) .AsNoTracking() .SingleOrDefaultAsync(m => m.CourseID == id); if (course == null) { return NotFound(); } return View(course); } public async Task<IActionResult> Delete(int? id) { if (id == null) { return NotFound(); } var course = await _context.Courses .Include(c => c.Department) .AsNoTracking() .SingleOrDefaultAsync(m => m.CourseID == id); if (course == null) { return NotFound(); } return View(course); }
In Views/Courses/Create.cshtml, add a field for the course ID before the Credits field:
在Views/Courses/Create.cshtml中,在Credits字段前添加一个course ID字段:
<div class="form-group"> <label asp-for="CourseID" class="col-md-2 control-label"></label> <div class="col-md-10"> <input asp-for="CourseID" class="form-control" /> <span asp-validation-for="CourseID" class="text-danger" /> </div> </div>
The scaffolder doesn’t scaffold a primary key because typically the key value is generated by the database and can’t be changed and isn’t a meaningful value to be displayed to users. For Course entities you do need a text box in the Create view for the CourseID field because the DatabaseGeneratedOption.None
attribute means the user enters the primary key value.
基架不会为主键建立代码,由于通常状况下主键值是由数据库产生的,而且不能被改变,还有对用户来讲,它不表明着具体的什么含义。对课程实体来讲,在Create视图中确实须要一个文本框来显示CourseID,由于DatabaseGeneratedOption.None 属性意味着需由用户输入主键的值。
In Views/Courses/Create.cshtml, add a “Select Department” option to the Department drop-down list, and change the caption for the field from DepartmentID to Department.
在Views/Courses/Create.cshtml中,给Department下拉列表增长一个“Select Department”选项,而后将表头字段从DepartmentID变动为Department。
<div class="form-group"> <label asp-for="Department" class="col-md-2 control-label"></label> <div class="col-md-10"> <select asp-for="DepartmentID" class ="form-control" asp-items="ViewBag.DepartmentID"> <option value="">-- Select Department --</option> </select> <span asp-validation-for="DepartmentID" class="text-danger" /> </div> </div>
In Views/Courses/Edit.cshtml, make the same change for the Department field that you just did in Create.cshtml.
在Views/Courses/Edit.cshtml中, 作一样的变动。
Also in Views/Courses/Edit.cshtml, add a course number field before the Credits field. Because it’s the primary key, it’s displayed, but it can’t be changed.
一样在View/Courses/Edit.cshtml中,在Credits字段前面添加课程代码字段。由于它是主键,同时也被显示出来,但并不能被改变。
<div class="form-group"> <label asp-for="CourseID" class="col-md-2 control-label"></label> <div class="col-md-10"> @Html.DisplayFor(model => model.CourseID) </div> </div>
There’s already a hidden field (<input type="hidden">
) for the course number in the Edit view. Adding a <label>
tag helper doesn’t eliminate the need for the hidden field because it doesn’t cause the course number to be included in the posted data when the user clicks Save on the Edit page.
在Edit视图中已经有了一个处理课程代码的隐藏字段(<input type="hidden">)。请添加一个<label>标签助手,同时不要忽略对该隐藏字段的需求,由于当点击Edit页面上的Save按钮时,该隐藏字段所包含的课程编码不会包含在发送的数据中。
In Views/Course/Delete.cshtml, add a course number field at the top and a department name field before the title field.
在Views/Course/delete.cshtml中,向顶部添加一个课程编码字段,并在表头字段前增长系名称字段。
<dl class="dl-horizontal"> <dt> @Html.DisplayNameFor(model => model.CourseID) </dt> <dd> @Html.DisplayFor(model => model.CourseID) </dd> <dt> @Html.DisplayNameFor(model => model.Credits) </dt> <dd> @Html.DisplayFor(model => model.Credits) </dd> <dt> @Html.DisplayNameFor(model => model.Department) </dt> <dd> @Html.DisplayFor(model => model.Department.Name) </dd> <dt> @Html.DisplayNameFor(model => model.Title) </dt> <dd> @Html.DisplayFor(model => model.Title) </dd> </dl>
In Views/Course/Details.cshtml, make the same change that you just did for Delete.cshtml.
Run the Create page (display the Course Index page and click Create New) and enter data for a new course:
运行Create页面(显示Course Index页面,并点击Create New),而后输入新的课程数据:
Click Create. The Courses Index page is displayed with the new course added to the list. The department name in the Index page list comes from the navigation property, showing that the relationship was established correctly.
点击Create。课程Index页面显示出来,其中有已添加进表中的新课程。Index页面中所属系的名字来自于导航属性,显示了正确的关系配置。
Run the Edit page (click Edit on a course in the Course Index page ).
运行Edit页面(点击课程Index页面中课程后面的Edit)。
Change data on the page and click Save. The Courses Index page is displayed with the updated course data.
When you edit an instructor record, you want to be able to update the instructor’s office assignment. The Instructor entity has a one-to-zero-or-one relationship with the OfficeAssignment entity, which means your code has to handle the following situations:
当编辑一条讲师记录时,你想更新讲师办公室的安排。讲师实体与办公室安排实体之间有1-0或-1的关系,这意味着你的代码必须处理一下问题:
In InstructorsController.cs, change the code in the HttpGet Edit
method so that it loads the Instructor entity’s OfficeAssignment
navigation property and calls AsNoTracking
:
在InstructorsController.cs中,将代码变成HttpGet的Edit方法,以便加载讲师实体的办公室安排导航属性,而且要调用AsNoTarcking:
public async Task<IActionResult> Edit(int? id) { if (id == null) { return NotFound(); } var instructor = await _context.Instructors .Include(i => i.OfficeAssignment) .AsNoTracking() .SingleOrDefaultAsync(m => m.ID == id); if (instructor == null) { return NotFound(); } return View(instructor); }
Replace the HttpPost Edit
method with the following code to handle office assignment updates:
用下列代码替换HttpPost的Edit方法,处理办公室安排的更新:
[HttpPost, ActionName("Edit")] [ValidateAntiForgeryToken] public async Task<IActionResult> EditPost(int? id) { if (id == null) { return NotFound(); } var instructorToUpdate = await _context.Instructors .Include(i => i.OfficeAssignment) .SingleOrDefaultAsync(s => s.ID == id); if (await TryUpdateModelAsync<Instructor>( instructorToUpdate, "", i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment)) { if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location)) { instructorToUpdate.OfficeAssignment = null; } try { await _context.SaveChangesAsync(); } catch (DbUpdateException /* ex */) { //Log the error (uncomment ex variable name and write a log.) ModelState.AddModelError("", "Unable to save changes. " + "Try again, and if the problem persists, " + "see your system administrator."); } return RedirectToAction("Index"); } return View(instructorToUpdate); }
The code does the following:
该代码段作了以下工做:
Changes the method name to EditPost
because the signature is now the same as the HttpGet Edit
method (the ActionName
attribute specifies that the /Edit/
URL is still used).
Gets the current Instructor entity from the database using eager loading for the OfficeAssignment
navigation property. This is the same as what you did in the HttpGet Edit
method.
Updates the retrieved Instructor entity with values from the model binder. The TryUpdateModel
overload enables you to whitelist the properties you want to include. This prevents over-posting, as explained in the second tutorial.
从绑定模型中更新取回的讲师实体的值。TryUpdateModel重载使你可将想要属性列入白名单。这样作会阻止过多发布,在第二个教程中进行了解释。
if (await TryUpdateModelAsync<Instructor>( instructorToUpdate, "", i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))
If the office location is blank, sets the Instructor.OfficeAssignment property to null so that the related row in the OfficeAssignment table will be deleted.
若是办公室位置是空白的话,将Instructor.OfficeAssignment属性数值为null,以便删除OfficeAssignment表中的关系行。
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location)) { instructorToUpdate.OfficeAssignment = null; }
Saves the changes to the database.
In Views/Instructors/Edit.cshtml, add a new field for editing the office location, at the end before the Save button :
在View/Instructors/Edit.cshtml文件中,在文件的末尾Save按钮以前,添加一个编辑办公室位置的新字段:
<div class="form-group"> <label asp-for="OfficeAssignment.Location" class="col-md-2 control-label"></label> <div class="col-md-10"> <input asp-for="OfficeAssignment.Location" class="form-control" /> <span asp-validation-for="OfficeAssignment.Location" class="text-danger" /> </div> </div>
Run the page (select the Instructors tab and then click Edit on an instructor). Change the Office Location and click Save.
运行页面,变动Office Location,并点击Save。
Instructors may teach any number of courses. Now you’ll enhance the Instructor Edit page by adding the ability to change course assignments using a group of check boxes, as shown in the following screen shot:
讲师们能够教必定数量的课程。如今要增长讲师Edit页面的功能,添加一组复选框实现变动课程安排的功能,下面截屏进行了展现:
The relationship between the Course and Instructor entities is many-to-many. To add and remove relationships, you add and remove entities to and from the InstructorCourses join entity set.
课程和讲师实体间有多对多的关系。要添加并删除关系,就是从InstructorCourse的联合集合中添加或删除实体。
The UI that enables you to change which courses an instructor is assigned to is a group of check boxes. A check box for every course in the database is displayed, and the ones that the instructor is currently assigned to are selected. The user can select or clear check boxes to change course assignments. If the number of courses were much greater, you would probably want to use a different method of presenting the data in the view, but you’d use the same method of manipulating a join entity to create or delete relationships.
该界面实现了该功能,即经过一组复选框来变动课程和讲师的分配。该界面显示了表明数据库中每门课程的一个复选框,当前已分配到该讲师的课程的复选框已经选中了。用户可经过选择或清除复选框来变动课程安排。若是课程编码太大了,你有可能想使用不一样的方法替代视图中的数据,可是要使用相同的方法操做一个联合实体来建立或删除关系。
To provide data to the view for the list of check boxes, you’ll use a view model class.
要向视图中的复选框列表提供数据,要使用到视图模型类。
Create AssignedCourseData.cs in the SchoolViewModels folder and replace the existing code with the following code:
在SchoolViewModels文件夹中新建AssignedCourseData.cs文件,并用下列代码替换初始代码:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace ContosoUniversity.Models.SchoolViewModels { public class AssignedCourseData { public int CourseID { get; set; } public string Title { get; set; } public bool Assigned { get; set; } } }
In InstructorsController.cs, replace the HttpGet Edit
method with the following code. The changes are highlighted.
在InstructorsController.cs中,用下列代码替换HttpGet的Edit方法。其变化见高亮代码。
public async Task<IActionResult> Edit(int? id) { if (id == null) { return NotFound(); } var instructor = await _context.Instructors .Include(i => i.OfficeAssignment) .Include(i => i.Courses).ThenInclude(i => i.Course) .AsNoTracking() .SingleOrDefaultAsync(m => m.ID == id); if (instructor == null) { return NotFound(); } PopulateAssignedCourseData(instructor); return View(instructor); } private void PopulateAssignedCourseData(Instructor instructor) { var allCourses = _context.Courses; var instructorCourses = new HashSet<int>(instructor.Courses.Select(c => c.Course.CourseID)); var viewModel = new List<AssignedCourseData>(); foreach (var course in allCourses) { viewModel.Add(new AssignedCourseData { CourseID = course.CourseID, Title = course.Title, Assigned = instructorCourses.Contains(course.CourseID) }); } ViewData["Courses"] = viewModel; }
The code adds eager loading for the Courses
navigation property and calls the new PopulateAssignedCourseData
method to provide information for the check box array using the AssignedCourseData
view model class.
该代码增长了Course导航属性的预加载,调用一个新的PopulateAssignedCourseData 方法(利用AssignedCourseData视图模型类向复选框队列提供信息)。
The code in the PopulateAssignedCourseData
method reads through all Course entities in order to load a list of courses using the view model class. For each course, the code checks whether the course exists in the instructor’s Courses
navigation property. To create efficient lookup when checking whether a course is assigned to the instructor, the courses assigned to the instructor are put into a HashSet
collection. The Assigned
property is set to true for courses the instructor is assigned to. The view will use this property to determine which check boxes must be displayed as selected. Finally, the list is passed to the view in ViewData
.
PopulateAssignedCourseData 方法中的代码读取了全部课程实体,其目的是使用视图模型类加载一个课程列表。对于每一个课程,代码都检查该课程是否存在于讲师的Courses导航属性中。检查一个课程是否分配到了该讲师时,要建立高效的查找,分配到该讲师的课程被放入了一个HashSet集合中。若是已经完成分配,Assigned属性被设置为true。视图将使用该属性来肯定设置哪个复选框为选中状态。最后,该列表被传送到ViewData中的视图。
Next, add the code that’s executed when the user clicks Save. Replace the EditPost
method with the following code, and add a new method that updates the Courses
navigation property of the Instructor entity.
接下来,添加用户点击Save按钮所执行的代码。将EditPost方法替换成下列代码,再添加一个新的方法,来更新讲师实体中的Courses导航属性。
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Edit(int? id, string[] selectedCourses) { if (id == null) { return NotFound(); } var instructorToUpdate = await _context.Instructors .Include(i => i.OfficeAssignment) .Include(i => i.Courses) .ThenInclude(i => i.Course) .SingleOrDefaultAsync(m => m.ID == id); if (await TryUpdateModelAsync<Instructor>( instructorToUpdate, "", i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment)) { if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location)) { instructorToUpdate.OfficeAssignment = null; } UpdateInstructorCourses(selectedCourses, instructorToUpdate); try { await _context.SaveChangesAsync(); } catch (DbUpdateException /* ex */) { //Log the error (uncomment ex variable name and write a log.) ModelState.AddModelError("", "Unable to save changes. " + "Try again, and if the problem persists, " + "see your system administrator."); } return RedirectToAction("Index"); } return View(instructorToUpdate); } private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate) { if (selectedCourses == null) { instructorToUpdate.Courses = new List<CourseAssignment>(); return; } var selectedCoursesHS = new HashSet<string>(selectedCourses); var instructorCourses = new HashSet<int> (instructorToUpdate.Courses.Select(c => c.Course.CourseID)); foreach (var course in _context.Courses) { if (selectedCoursesHS.Contains(course.CourseID.ToString())) { if (!instructorCourses.Contains(course.CourseID)) { instructorToUpdate.Courses.Add(new CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID = course.CourseID }); } } else { if (instructorCourses.Contains(course.CourseID)) { CourseAssignment courseToRemove = instructorToUpdate.Courses.SingleOrDefault(i => i.CourseID == course.CourseID); _context.Remove(courseToRemove); } } } }
The method signature is now different from the HttpGet Edit
method, so the method name changes from EditPost
back to Edit
.
如今,该方法的签名已与HttpGet的Edit方法不一样了,因此该方法的名称也从EditPost改回Edit。
Since the view doesn’t have a collection of Course entities, the model binder can’t automatically update the Courses
navigation property. Instead of using the model binder to update the Courses
navigation property, you do that in the new UpdateInstructorCourses
method. Therefore you need to exclude the Courses
property from model binding. This doesn’t require any change to the code that calls TryUpdateModel
because you’re using the whitelisting overload and Courses
isn’t in the include list.
由于视图尚未课程实体集合,绑定模型不能自动地更新Courses导航属性。不使用绑定模型更新Courses导航属性,而是要在新的UpdateInstructorCourses 方法中完成该工做。所以,须要在绑定模型中排除Courses属性。不须要改变任何调用TryUpdateModel的代码,由于你正在使用白名单重载,而Courses并再也不包含列表中。
If no check boxes were selected, the code in UpdateInstructorCourses
initializes the Courses
navigation property with an empty collection and returns:
若是复选框没有被选中,UpdateInstructorCourses 中的代码用空集初始化并返回Courses导航属性。
private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate) { if (selectedCourses == null) { instructorToUpdate.Courses = new List<CourseAssignment>(); return; } var selectedCoursesHS = new HashSet<string>(selectedCourses); var instructorCourses = new HashSet<int> (instructorToUpdate.Courses.Select(c => c.Course.CourseID)); foreach (var course in _context.Courses) { if (selectedCoursesHS.Contains(course.CourseID.ToString())) { if (!instructorCourses.Contains(course.CourseID)) { instructorToUpdate.Courses.Add(new CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID = course.CourseID }); } } else { if (instructorCourses.Contains(course.CourseID)) { CourseAssignment courseToRemove = instructorToUpdate.Courses.SingleOrDefault(i => i.CourseID == course.CourseID); _context.Remove(courseToRemove); } } } }
The code then loops through all courses in the database and checks each course against the ones currently assigned to the instructor versus the ones that were selected in the view. To facilitate efficient lookups, the latter two collections are stored in HashSet
objects.
接下来,该代码会遍历数据库中的全部课程,检查每门课程是否与视图中选择的讲师变量匹配。为了实现高效的遍历,后面的两个集合被存储在HashSet对象中。
If the check box for a course was selected but the course isn’t in the Instructor.Courses
navigation property, the course is added to the collection in the navigation property.
若是一门课程的复选框被选中了,可是该课程没有在Instructor.Courses导航属性中,则该门课程就会被添加进导航属性中的集合。
private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate) { if (selectedCourses == null) { instructorToUpdate.Courses = new List<CourseAssignment>(); return; } var selectedCoursesHS = new HashSet<string>(selectedCourses); var instructorCourses = new HashSet<int> (instructorToUpdate.Courses.Select(c => c.Course.CourseID)); foreach (var course in _context.Courses) { if (selectedCoursesHS.Contains(course.CourseID.ToString())) { if (!instructorCourses.Contains(course.CourseID)) { instructorToUpdate.Courses.Add(new CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID = course.CourseID }); } } else { if (instructorCourses.Contains(course.CourseID)) { CourseAssignment courseToRemove = instructorToUpdate.Courses.SingleOrDefault(i => i.CourseID == course.CourseID); _context.Remove(courseToRemove); } } } }
If the check box for a course wasn’t selected, but the course is in the Instructor.Courses
navigation property, the course is removed from the navigation property.
若是一门课程的复选框没有被选中,可是该课程已存在于Instructor.courses导航属性中,该门课程就会被从导航属性中删除。
private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate) { if (selectedCourses == null) { instructorToUpdate.Courses = new List<CourseAssignment>(); return; } var selectedCoursesHS = new HashSet<string>(selectedCourses); var instructorCourses = new HashSet<int> (instructorToUpdate.Courses.Select(c => c.Course.CourseID)); foreach (var course in _context.Courses) { if (selectedCoursesHS.Contains(course.CourseID.ToString())) { if (!instructorCourses.Contains(course.CourseID)) { instructorToUpdate.Courses.Add(new CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID = course.CourseID }); } } else { if (instructorCourses.Contains(course.CourseID)) { CourseAssignment courseToRemove = instructorToUpdate.Courses.SingleOrDefault(i => i.CourseID == course.CourseID); _context.Remove(courseToRemove); } } } }
In Views/Instructors/Edit.cshtml, add a Courses field with an array of check boxes by adding the following code immediately after the div
elements for the Office field and before the div
element for the Save button.
在Views/Instuctors/Edit.cshtml文件中,添加带有一个复选框队列的Courses字段,添加位置是在office字段的div元素以后,Save按钮的div元素以前。
Note 注意
Open the file in a text editor such as Notepad to make this change. If you use Visual Studio, line breaks will be changed in a way that breaks the code. If that happens, fix the line breaks so that they look like what you see here. The indentation doesn’t have to be perfect, but the @</tr><tr>
, @:<td>
, @:</td>
, and @:</tr>
lines must each be on a single line as shown or you’ll get a runtime error. After editing the file in a text editor, you can open it in Visual Studio, highlight the block of new code, and press Tab twice to line up the new code with the existing code.
请在某些文本编辑器中打开该文件进行修改,例如Notepad等。若是使用VS进行编辑,换行符会发生变化打乱代码。若是已经这样作了,将换行符修改为下面你看到的样式。缩进并不须要修正,可是@</tr><tr>、 @:<td>、
@:</td>、
以及@:</tr>
等行必须在同一行中,不然将会致使运行时错误。在文本编辑器中完成编辑后,再在VS中开打,高亮部分是新代码,按两次Tab键修正新代码与源代码的排列。
<div class="form-group"> <div class="col-md-offset-2 col-md-10"> <table> <tr> @{ int cnt = 0; List<ContosoUniversity.Models.SchoolViewModels.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>
This code creates an HTML table that has three columns. In each column is a check box followed by a caption that consists of the course number and title. The check boxes all have the same name (“selectedCourses”), which informs the model binder that they are to be treated as a group. The value attribute of each check box is set to the value of CourseID
. When the page is posted, the model binder passes an array to the controller that consists of the CourseID
values for only the check boxes which are selected.
该部分代码建立了一个三列的HTML表。在每一个列中,有一个复选框,其后跟着由课程编码和标题组成的标题。全部复选框都有相同的名字“SelectedCourses”,这样会通知绑定模型将它们做为一个组来对待。每一个复选框的值都被设置为一个CourseID的值。当页面被发送后,模型绑定向控制器传递一个队列,其中包含了被选中复选框的CourseID值。
When the check boxes are initially rendered, those that are for courses assigned to the instructor have checked attributes, which selects them (displays them checked).
Run the Instructor Index page, and click Edit on an instructor to see the Edit page.
Change some course assignments and click Save. The changes you make are reflected on the Index page.
注意:由于只有有限的课程,因此这里编辑讲师课程数据所采起的方法工做的很是好。对于很是大的集合,将会须要不一样的界面以及不一样的更新方法。
In InstructorsController.cs, delete the DeleteConfirmed
method and insert the following code in its place.
在InstrctorsController.cs中,删除DeleteConfirmed方法,并在原处插入下列代码。
[HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] public async Task<IActionResult> DeleteConfirmed(int id) { Instructor instructor = await _context.Instructors .Include(i => i.Courses) .SingleAsync(i => i.ID == id); var departments = await _context.Departments .Where(d => d.InstructorID == id) .ToListAsync(); departments.ForEach(d => d.InstructorID = null); _context.Instructors.Remove(instructor); await _context.SaveChangesAsync(); return RedirectToAction("Index"); }
This code makes the following changes:
该处的代码作了以下变动:
Courses
navigation property. You have to include this or EF won’t know about related CourseAssignment
entities and won’t delete them. To avoid needing to read them here you could configure cascade delete in the database.In InstructorController.cs, delete the HttpGet and HttpPost Create
methods, and then add the following code in their place:
在instructorcontroller.cs文件中,删除HttpGet和HttpPost的Create方法,而后在原处添加下列代码:
public IActionResult Create() { var instructor = new Instructor(); instructor.Courses = new List<CourseAssignment>(); PopulateAssignedCourseData(instructor); return View(); } // POST: Instructors/Create [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Create([Bind("FirstMidName,HireDate,LastName,OfficeAssignment")] Instructor instructor, string[] selectedCourses) { if (selectedCourses != null) { instructor.Courses = new List<CourseAssignment>(); foreach (var course in selectedCourses) { var courseToAdd = new CourseAssignment { InstructorID = instructor.ID, CourseID = int.Parse(course) }; instructor.Courses.Add(courseToAdd); } } if (ModelState.IsValid) { _context.Add(instructor); await _context.SaveChangesAsync(); return RedirectToAction("Index"); } return View(instructor); }
This code is similar to what you saw for the Edit
methods except that initially no courses are selected. The HttpGet Create
method calls the PopulateAssignedCourseData
method not because there might be courses selected but in order to provide an empty collection for the foreach
loop in the view (otherwise the view code would throw a null reference exception).
该代码与你在Edit方法中看到的较为类似,除了初始化中没有被选择的课程。HttpGet的Create方法调用PopulateAssignedCouseData方法,不是由于课程有可能被选中,而是为了给view中的foreach循环提供一个空集合(不然视图代码将抛出一个null引用异常)。
The HttpPost Create
method adds each selected course to the Courses
navigation property before it checks for validation errors and adds the new instructor to the database. Courses are added even if there are model errors so that when there are model errors (for an example, the user keyed an invalid date), and the page is redisplayed with an error message, any course selections that were made are automatically restored.
在检查验证错误和向数据库添加新讲师数据以前,HttpPose的Create方法向课程导航属性添加每一个被选中的课程。即便产生的模型错误,添加课程信息的操做仍然被执行了,所以,当产生模型错误时(例如:用户键入了错误的日期),页面须要再次显示,并带有错误信息提示,任何已做出的课程选择都已被自动存储了。
Notice that in order to be able to add courses to the Courses
navigation property you have to initialize the property as an empty collection:
注意到为了能向课程导航属性添加课程,你必须将该属性初始化为一个空集:
instructor.Courses = new List<Course>();
As an alternative to doing this in controller code, you could do it in the Instructor model by changing the property getter to automatically create the collection if it doesn’t exist, as shown in the following example:
要实现该目的的另外一个方法是,在讲师模型中将get属性修改成自动建立集合(若是不存在的话),以下所示:
private ICollection<Course> _courses; public ICollection<Course> Courses { get { return _courses ?? (_courses = new List<Course>()); } set { _courses = value; } }
If you modify the Courses
property in this way, you can remove the explicit property initialization code in the controller.
若是用这种方法修改课程属性,能够删除控制器中的属性的显式初始化代码。
In Views/Instructor/Create.cshtml, add an office location text box and check boxes for courses after the hire date field and before the Submit button. As in the case of the Edit page, this will work better if you do it in a text editor such as Notepad.
在View/Instructor/Create.cshtml文件中,在租借日期字段以后,Submit按钮以前,增长办公室位置的文本框。如同在Edit页面中同样,最好在一个文本编辑器中(例如Notepad)完成该项工做。
<div class="form-group"> <label asp-for="OfficeAssignment.Location" class="col-md-2 control-label"></label> <div class="col-md-10"> <input asp-for="OfficeAssignment.Location" class="form-control" /> <span asp-validation-for="OfficeAssignment.Location" class="text-danger" /> </div> </div> <div class="form-group"> <div class="col-md-offset-2 col-md-10"> <table> <tr> @{ int cnt = 0; List<ContosoUniversity.Models.SchoolViewModels.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>
Test by running the Create page and adding an instructor.
As explained in the CRUD tutorial, the Entity Framework implicitly implements transactions. For scenarios where you need more control – for example, if you want to include operations done outside of Entity Framework in a transaction – see Transactions.
和在增删查改教程中解释的同样,EF隐式实现事务处理。在须要更多控制的场景下,例如:若是事务中须要在EF以外进行操做,请参看 TanSactions章节。
You have now completed the introduction to working with related data. In the next tutorial you’ll see how to handle concurrency conflicts.
如今已经完成了关系数据的介绍工做。在下个教程中,你将看到如何处理并发冲突。