【EntityFramework系列教程六,翻译】在ASP.NET MVC程序中使用EntityFramework对相关数据进行更新

前一章你已经学会如何显示相关数据,在本章中你将学会如何更新相关数据。大部分情况下更新只需通过更新对应的外键字段即可完成,不过对“多对多”关系而言,由于EF不是直接暴露那个中间连接表,因此你不得不“显式”从对应的导航属性中增加或者删除实体得以完成。

以下一些截图是你今日要完成的任务:

Course_create_page

Course_edit_page

Instructor_edit_page_with_courses

【为Courses自定义“新增”和“编辑”页面】

当一个新课程创建之时,它总是隶属于某一个特定的系;为方便期间,自生成“创建”和“编辑”的代码架构中就包含了一个可供选择“系”的下拉列表。下拉框设置了Department的Id,这是所有EntityFramework实体都有的,为了把正确的Department加载到Course的Department导航属性中去。你只需对此代码做一些小小的变动(增加错误捕获以及对下拉列表框中的“系”排序)即可使用,代码如下:

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

[HttpPost]
public ActionResult Create(Course course)
{
    try
    {
        if (ModelState.IsValid)
        {
            db.Courses.Add(course);
            db.SaveChanges();
            return RedirectToAction("Index");
        }
    }
    catch (DataException)
    {
        //Log the error (add a variable name after DataException)
        ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
    }
    PopulateDepartmentsDropDownList(course.DepartmentID);
    return View(course);
}

public ActionResult Edit(int id)
{
    Course course = db.Courses.Find(id);
    PopulateDepartmentsDropDownList(course.DepartmentID);
    return View(course);
}

[HttpPost]
public ActionResult Edit(Course course)
{
    try
    {
        if (ModelState.IsValid)
        {
            db.Entry(course).State = EntityState.Modified;
            db.SaveChanges();
            return RedirectToAction("Index");
        }
    }
    catch (DataException)
    {
        //Log the error (add a variable name after DataException)
        ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
    }
    PopulateDepartmentsDropDownList(course.DepartmentID);
    return View(course);
}

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

“PopulateDepartmentDropdownList”方法获取了所有根据名称排序的课程,并且创建了一个SelectList列表作为Dropdownlist的数据源赋值给它,最后把整个SelectList存入ViewBag中。此方法另外还接受一个可选参数,以便下拉列表回发后它可以被置初值。

HttpGet传递方式的Create方法没有调用带参数的PopulateDepartmentDropdownlist方法,原因在于当一个课程刚被创建之时,“系”尚未确定。

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

HttpGet传递方式的Edit方法根据赋值给相关课程的Id号设置了对应的相关课程:

public ActionResult Edit(int id)
{
    Course course = db.Courses.Find(id);
    PopulateDepartmentsDropDownList(course.DepartmentID);
    return View(course);
}

对于以HttpPost方式传递的Create和Edit方法都包含了“在页面重现后,设定对应课程的”代码段,其位置位于错误捕获之后:

catch (DataException)
{
    //Log the error (add a variable name after DataException)
    ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);

此代码确保当页面回发后再次呈现之时,保留原先选择的课程不变,同时显示错误信息。

在“Views\Course\Create.cshtml”中在Title字段之前添加一个可供读者输入课程Id的字段。正如先前所说的一样,自生成的代码不会包含主键字段,但是这个字段在此处有用,因为这样可以输入课程的Id。

<div class="editor-label">
    @Html.LabelFor(model => model.CourseID)
</div>
<div class="editor-field">
    @Html.EditorFor(model => model.CourseID)
    @Html.ValidationMessageFor(model => model.CourseID)
</div>

在“Views\Course\Edit.cshtmlViews\Course\Delete.cshtml”两个页面中Title字段之前添加一个新字段用以显示课程编号,考虑是主键所以只能显示,不能被修改。

<div class="editor-label">
    @Html.LabelFor(model => model.CourseID)
</div>
<div class="editor-field">
    @Html.DisplayFor(model => model.CourseID)
</div>

运行Create页面(显示课程索引页,点击“Create New”),为创建新课程录入数据:

Course_create_page

点击Create,随着新课程的加入Course索引页加载显示完毕。在索引页中“系”名称自导航属性而来,说明表关系已经正确创建。

Course_Index_page_showing_new_course

运行“Edit”页面(显示课程索引页),点击Edit按钮:

Course_edit_page

在页面上更改一些数据并点击Save,课程索引页就显示已经更新的课程信息。

【为Instructors增加编辑页】

当你要编辑Instructor实体的时候,你应当可以更新OfficeAssignment。由于Instructor和OfficeAssignment之间是“一对一”或者“一对零”的关系,那么你必须要处理下列一些情况:

1)当一个用户清除了OfficeAssignment信息时,你应当移除此实体。

2)当一个用户添加了OfficeAssignment信息,且原来为空时,你应当为此创建一个实体。

3)当一个用户更改了OfficeAssignment信息时,你应当更新对应的实体。

打开InstructorController.cs文件看看HttpGet方式的“编辑”方法:

public ActionResult Edit(int id)
{
    Instructor instructor = db.Instructors.Find(id);
    ViewBag.InstructorID = new SelectList(db.OfficeAssignments, "InstructorID", "Location", instructor.InstructorID);
    return View(instructor);
}

自生成的代码并不是你所要的……它生成了一个下拉列表,但是你希望是一个可以输入数值的文本框。因此用以下代码进行替换:

public ActionResult Edit(int id)
{
    Instructor instructor = db.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.Courses)
        .Where(i => i.InstructorID == id)
        .Single();
    return View(instructor);
}

本代码没有使用ViewBag保存并回传数据,不过使用了“饥饿模式”对Course和OfficeAssignment”进行数据加载(目前暂且用不到Course,不过稍后会用到的)。“饥饿模式”状态下你不能用Find寻找数据,取而代之的是使用“Where”和“Single”。

用以下代码替换HttpPost方式的Edit方法,它用于专门处理OfficeAssignment的更新:

[HttpPost]
public ActionResult Edit(int id, FormCollection formCollection)
{
    var instructorToUpdate = db.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.Courses)
        .Where(i => i.InstructorID == id)
        .Single();
    if (TryUpdateModel(instructorToUpdate, "", null, new string[] { "Courses" }))
    {
        try
        {
            if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
            {
                instructorToUpdate.OfficeAssignment = null;
            }

            db.Entry(instructorToUpdate).State = EntityState.Modified;
            db.SaveChanges();

            return RedirectToAction("Index");
        }
        catch (DataException)
        {
            //Log the error (add a variable name after DataException)
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
            return View();
        }
    }
    return View(instructorToUpdate);
}

此代码做了一些事情:

1)使用“饥饿模式”在获取当前的Instructor实体同时加载了OfficeAssignment和Course等信息,这和你在HttpGet模式中的Edit方法一样。

2)从模型绑定设置中更新已获取的Instructor实体,此不包括Courses导航属性。

If (TryUpdateModel(instructorToUpdate, "", null, new string[] { "Courses" }))

3)第二个和第三个参数分别表示了“属性名称上无前缀”以及“没有需要包含的属性列表”,如果验证失败,那么TryUpdateModel将返回false,代码最终也执行return View这块。如果Office的Location为空,那么把对应的OfficeAssignment设置为null,这样相关的数据自然就被删除了。

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

4)保存更新的记录:在“”中,在Hire Date字段的div后,为编辑Office的Location添加新字段:

<div class="editor-label">
    @Html.LabelFor(model => model.OfficeAssignment.Location)
</div>
<div class="editor-field">
    @Html.EditorFor(model => model.OfficeAssignment.Location)
    @Html.ValidationMessageFor(model => model.OfficeAssignment.Location)
</div>

运行该页面(选择Instrcutors选项卡,单击Edit):

Instructor_edit_page

变更Location中的数据,点击保存:

Changing_the_office_location

那么新的Location数据就在Index页面显示出来,在Server Explorer中你也确实可以看到OfficeAssignment表中发生了同样的改变:

Server_explorer_showing_changed_office_location

返回到Edit页,清空OfficeAssignment中的内容点击Save,Index显示空白的Office的Location;与之对应数据表中也删除了该记录:

Server_explorer_showing_deleted_office_location

再次到Edit页,为Office的Location数据做一些变更,单击Save之后看到Index页中数据同样做了变更,数据表显示的记录也表示数据发生了更新:

Server_explorer_showing_added_office_location

【为Instructor页添加“赋予课程”功能】

教师们可能教授多门课程,现在你借助一组复选框为Instructor增加了选课的功能。如下所示:

Instructor_edit_page_with_courses

Course和Instructor之间是“多对多”的关系,所以你不能简单地访问“连接中间表”或者是外键;你可以从Instructors.Courses中增加或者移除相关的课程。

使得你可以任意选择教授课程的用户界面其实是一组复选框——每一个课程在界面上对应一个复选框,如果老师教授某课程,则那个对应复选框则打勾;我们可以通过打勾或者取消打勾的方式任意为某个老师指定课程,如果课程过于多的话,你或许要考虑换个界面展示这些课程;不过为创建或者删除相关的课程,你还是使用和本示例同样的方法。

为了给这一堆复选框提供数据,你需要使用到视图模型类;在ViewModel文件夹中创建“AssignedCourseData.cs”,并用以下代码进行替换:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

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)
{
    Instructor instructor = db.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.Courses)
        .Where(i => i.InstructorID == id)
        .Single();
    PopulateAssignedCourseData(instructor);
    return View(instructor);
}

private void PopulateAssignedCourseData(Instructor instructor)
{
    var allCourses = db.Courses;
    var instructorCourses = new HashSet<int>(instructor.Courses.Select(c => c.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)
        });
    }
    ViewBag.Courses = viewModel;
}

新方法中从数据库读取全部的课程信息,以便使用视图模型类加载此系列的课程信息;对于每个课程而言,代码将检测对应的Instructor中导航属性Courses是否包含该课程:为了提高效率寻找,某个Instructor所有的课程都被放入一个HashSet,并且那些已选择的课程“Assigned”属性被设置成true。视图将决定哪些复选框将是处于勾选状态的……同时,这些信息将被存储到ViewBag并且发送回页面视图中。

下一步,添加点击“Save”按钮时要执行的代码——使用以下代码替换HttpPost方式的Edit方法,它调用了一个新方法用来更新指定Instructor的导航属性Courses信息。

[HttpPost]
public ActionResult Edit(int id, FormCollection formCollection, string[] selectedCourses)
{
    var instructorToUpdate = db.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.Courses)
        .Where(i => i.InstructorID == id)
        .Single();
    if (TryUpdateModel(instructorToUpdate, "", null, new string[] { "Courses" }))
    {
        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 (DataException)
        {
            //Log the error (add a variable name after DataException)
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
       }
    }
    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);
            }
        }
    }
}

如果没有一个复选框选中,那么在“UpdateInstructorCourses”中的代码使用一个空集合初始化Courses导航属性,比如:

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

接着代码将循环遍历所有在数据表中有的课程,如果选中的课程不属于那个指定的Instructor,它将通过Courses导航属性自动加入此Instructor:

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

在“Views\Instructor\Edit.cshtml”中,在div元素后面为OfficeAssignment字段添加一系列生成复选框选课的代码:

<div class="editor-field">
    <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>

此代码创建了有着3列的表,每一列中都有一个复选框,旁边附带课程Id和名称;这些复选框的名称都是一致的,这告知模型绑定机制——它们属于同一组!每个复选框的value属性绑定了一个课程Id,这样当页面提交时,只有打勾的id才会被上传。

当这些复选框回发之后,对于那些已经被赋给当前Instructor的所有课程自然处于打勾状态。

在对教师课程信息选择做出变更之后,当页面回到Index后你应当可以检测这些变化;因此你需要在那个表中额外增加一个字段——此情况下你不必使用ViewBag,因为这些信息都已被你传递给页面的模型实体Instructor中的Courses这个导航属性所包含了。

在“Views\Instructor\Index.cshtml”中,在“<th>Office</th>”之后加上“<th>Courses</th>”:

<tr> 
    <th></th> 
    <th>Last Name</th> 
    <th>First Name</th> 
    <th>Hire Date</th> 
    <th>Office</th>
    <th>Courses</th>
</tr> 

随后紧挨着office location单元格下增加一个新的单元格,用于显示全部选定课程:

<td>
    @{
        foreach (var course in item.Courses)
        {
            @course.CourseID @:  @course.Title <br />
        }
    }
</td>

现在运行Instructor的Index页看看所有教员,以及他们任教的课程:

Instructor_index_page

点击某个教员的“Edit”查看Edit页面信息:

Instructor_edit_page_with_courses

对一些课程选择信息做一些改变,然后单击“Save”,变更信息将立即在Instructor的Index页面上反映出来。

目前为止你已经完成了处理相关数据的任务,并且直到本章节,连同先前所有教程在一起,它们教会你完成了整个增删改查的任务;不过你尚未处理“并发冲突”问题。下一章我们就要讨论此问题,并且为你的一个已经完成的实体类型增加此功能,并加以解释。

关于其它EntityFramework资源您可以本系列最后一篇末尾处找到。

posted @ 2012-04-27 17:13  Serviceboy  阅读(800)  评论(0编辑  收藏  举报