为ASP.NET MVC应用程序更新相关数据

这是微软官方教程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

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

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

为课程自定义创建和编辑页

当创建新的课程实体时,他必须拥有一个和已存在系的关系。为此,脚手架代码创建的控制器方法及新建和编辑视图种豆包含了用于选择系的下拉列表。下拉列表用来设置Course.DepartmentID外键属性,这对于实体框架通过Department导航属性来加载Department实体是必须的。你将使用脚手架代码,但需要对其做一些小的改动来增加错误处理和对列表内容进行排序。

在Coursecontroller.cs中,删除之前的Create和Edit方法,并添加下面的代码:

        private void PopulateDepartmentsDropDownList(object selectedDrpaerment = null)
        {
            var departmentsQuery = from d in db.Departments
                                   orderby d.Name
                                   select d;
            ViewBag.DepartmentID = new SelectList(departmentsQuery, "DepartmentID", "Name", selectedDrpaerment);
        }

        public ActionResult Create()
        {
            PopulateDepartmentsDropDownList();
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create([Bind(Include = "CourseID,Title,Credits,DepartmentID")]Course course)
        {
            try
            {
                if (ModelState.IsValid)
                {
                    db.Courses.Add(course);
                    db.SaveChanges();
                    return RedirectToAction("Index");
                }
            }
            catch (RetryLimitExceededException)
            {
                ModelState.AddModelError("", "无法保存数据,请重试或联系管理员。");
            }
            PopulateDepartmentsDropDownList(course.DepartmentID);
            return View(course);
        }

        public ActionResult Edit(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            Course course = db.Courses.Find(id);
            if (course == null)
            {
                return HttpNotFound();
            }
            PopulateDepartmentsDropDownList(course.DepartmentID);
            return View(course);
        }

        [HttpPost,ValidateAntiForgeryToken]
        public ActionResult Edit([Bind(Include="CourseID,Title,Credits,DepartmentID")]Course course)
        {
            try
            {
                if (ModelState.IsValid)
                {
                    db.Entry(course).State = EntityState.Modified;
                    db.SaveChanges();
                    return RedirectToAction("Index");
                }
            }
            catch (RetryLimitExceededException)
            {
                ModelState.AddModelError("", "无法保存更改,请重试或联系管理员。");
            }
            PopulateDepartmentsDropDownList(course.DepartmentID);
            return View(course);
        }

在文件的开头增加以下引用:

using System.Data.Entity.Infrastructure;

PopulateDepartmentsDropDownList方法获取所有的系列表并按照名称进行排序来创建一个下拉列表。并通过ViewBag属性传递到视图上。该方法接收一个可选参数selectedDepartment,在下拉列表渲染时允许调用代码指定被选择的项目。视图将传递DepartmentID名称给下拉列表帮助器,然后帮助器知道应当使用DepartmentID名来在ViewBag中对象进行下拉列表的查找。

HttpGet Create方法调用PopulateDepartmentsDropDownList方法,但并不设置已选项目,因为对于一个新的课程来说,尚未确定其所属的系。

        public ActionResult Create()
        {
            PopulateDepartmentsDropDownList();
            return View();
        }

HttpGetEdit方法设置所选的项目,基于已经分配给正在编辑的课程的系ID:

        public ActionResult Edit(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            Course course = db.Courses.Find(id);
            if (course == null)
            {
                return HttpNotFound();
            }
            PopulateDepartmentsDropDownList(course.DepartmentID);
            return View(course);
        }

Create和Edit的HttpPost方法还包括当出现了错误后,重新显示页面时要再设置一次所选项目的代码:

            catch (RetryLimitExceededException)
            {
                ModelState.AddModelError("", "无法保存更改,请重试或联系管理员。");
            }
            PopulateDepartmentsDropDownList(course.DepartmentID);
            return View(course);

这段代码确保当页面重新显示错误信息时,已经被选择的系保持被选择状态。

Course视图已经基于系字段来使用脚手架构建了一个下拉列表。但你并不想使用系ID来作为标题,所以在Views\Course\Create.cshtml中进行以下高亮部分的更改:

@model ContosoUniversity.Models.Course

@{
    ViewBag.Title = "Create";
}

<h2>Create</h2>


@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()
    
    <div class="form-horizontal">
        <h4>Course</h4>
        <hr />
        @Html.ValidationSummary(true)

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

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

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

        <div class="form-group">
            <label class="control-label col-md-2" for="DepartmentID">Department</label> 
            <div class="col-md-10">
                @Html.DropDownList("DepartmentID", String.Empty)
                @Html.ValidationMessageFor(model => model.DepartmentID)
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

之后在Edit视图中进行相同的更改。

通常脚手架不会使用主键来生成字段,因为主键值是由数据库生成的,无法更改且对用户显示也没有意义。对于课程实体脚手架代码包含了一个用于CourseID的文本框,因为DatabaseGeneratedOption.None特性意味着用户应当可以输入主键值。但它并不明白因为该号码只有在你想要让其显示在某些特定视图中才是有意义的。所以您需要手动添加它。

在Edit视图中,在标题字段之前添加课程编号字段。

        <div class="form-group">
            @Html.LabelFor(model => model.CourseID, new { @class = "Control-label col-md-2" })
            <div class="col-md-10">
                @Html.DisplayFor(model => model.CourseID)
            </div>
        </div>

Edit视图中已经有一个课程编号的隐藏字段(Html.HiddenFor帮助器)。为隐藏字段添加一个Html.LabelFor帮助器是没必要的。因为它不会导致当用户点击保存时将课程编号包含在要发送的数据中。

在Delete和Details视图中,更改系名称的标题从"Name"到"Department"并在标题字段之前添加一个课程编号字段。

         <dt>
            Department
        </dt>

        <dd>
            @Html.DisplayFor(model => model.Department.Name)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.CourseID)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.CourseID)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Title)
        </dt>

勘误注意:

之前的Layout页面因为疏忽路由参数写错了,请使用下面的代码替换布局页面。

 <!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>&copy; @DateTime.Now.Year - Contoso 大学</p>
        </footer>
    </div>

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

以及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平台及工具团队的高级程序员,作家。