zoukankan      html  css  js  c++  java
  • 翻译:Contoso 大学 6 – 更新关联数据

    By Tom Dykstra, Tom Dykstra is a Senior Programming Writer on Microsoft's Web Platform & Tools Content Team.

    原文地址:http://www.asp.net/mvc/tutorials/getting-started-with-ef-using-mvc/updating-related-data-with-the-entity-framework-in-an-asp-net-mvc-application

    全文目录:Contoso 大学 - 使用 EF Code First 创建 MVC 应用

    在上一次的课程中,你已经学习了如何显示关联的数据,我们将要更新关联的数据。大多数情况下,可能就是更新表的外键字段。对于多对多的关系来说,由于 EF 并没有直接将表与表之间的连接关系暴露出来,你就必须通过显式对相关的导航属性进行添加或者删除实体来完成。

    下面的截图展示了我们马上要完成的工作。

    6-1  定制课程的创建和编辑页面

    当新的课程实体创建的时候,必须包含相关的 Department。为了达到这个目的,脚手架创建的代码,包括创建和编辑的控制器以及视图,都包含了对 Department 的下拉列表的支持。下拉列表设置 Course.DepartmentId 外键属性,这对于 EF 通过 Department 导航属性来加载 Department 实体来说是必须的。下面将要对脚手架生成的代码进行一些小的改动,增加错误的处理以及对列表内容进行排序。

    打开 CourseController.cs, 删除原来的 Edit 和 Create 方法,使用下面的代码替换它们。

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

    PopulateDepartmentsDropDownList 方法获取经过对 Name 进行排序的 Department,然后创建用于下拉列表的 SelectList 集合,通过 ViewBag 传递到视图中。这个方法包含一个参数,允许调用方可选地传递一个初始选中项目的值。

    HttpGet Create 方法调用没有设置选中项的 PopulateDepartmentsDropDownList 方法,因为对于新创建的课程来说,还没有确定归属的系。

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

    HttpGet Edit 方法则设置了当前选中的项目,基于当前被编辑课程的 DepartmentId。

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

    对于 Create 和 Edit 的 HttpPost 方法来说,在错误信息处理之后,都包含了设置当前选中项目的代码。

     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 之前增加一个新的字段,允许用户输入课程编号。想之前演示的那样,脚手架没有生成主键字段,因为主键字段对用户没有意义。所以需要添加以便用户能够输入这个值。

    <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>

    运行 Create 页面 ( 在课程的 Index 页面,点击 Create New  ),然后输入新的课程。

    点击 Create,在 Index 中应该可以看到包含新创建课程的列表。系的名称通过导航属性获取到,显示的数据是正确的。

    运行编辑页面 ( 在课程的 Index 页面中在某个课程上选择 Edit )

    修改一些数据,然后点击 Save,可以看到更新之后的课程数据。

    6-2  增加教师的编辑页面

    在修改教师信息的时候,我们希望也能够修改教师的办公室分配。教师实体 Instructor 和办公室分配 OfficeAssignment存在一对一或者一对零的关系,这意味着你必须处理如下的状态:

    • 如果教师原来存在一个办公室分配,但是用户删除了它,那么,你必须移除并且删除这个办公室分配 OfficeAssignment 实体。
    • 如果教师原来没有办公室分配,但是用户输入了一个,你必须创建一个新的办公室分配。
    • 如果用户修改了原来的办公室分配,你必须修改当前的办公分配 OfficeAssignment 实体。

    打开 InstructorController.cs,看一下 HttpGet Edit 方法。

    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 语句,增加了使用预先加载的相关 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);
    }

    代码中处理了如下的内容:

    • 从数据库中获取了教师 Instructor 实体,并且预先加载了相关的的办公室分配 OfficeAssignment 和课程 Course 导航属性。如同在 HttpGet 的 Edit 方法一样。
    • 使用通过模型绑定获取的数据,更新 Instructor 实体,除了课程 Course 导航属性之外。
    If (TryUpdateModel(instructorToUpdate, "", null, new string[] { "Courses" }))

    ( 第二和第三个参数在属性名的前面没有前缀,而且没有被包含的属性列表 ),如果验证失败, TryUpdateModel 方法返回 false,程序将直接转到方法最后的 return View 语句。

    • 如果办公位置为空,设置 Instructor.OfficeAssignment 属性为 null,在 OfficeAssignment 表相关的行将会被删除。
    if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
    {
        instructorToUpdate.OfficeAssignment = null;
    }
    • 将修改保存到数据库中。

    Views\Instructor\Edit.cshtml, 在 Hire Date 字段的 div 元素之后,增加新的字段来编辑办公位置。

    <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>

    运行页面 ( 选中教师 Instructors ,点击某个教师的 Edit 链接 )

    修改办公室位置的值,然后保存 Save。

    新的办公位置出现在 Index 页面上,打开数据库中的 OfficeAssgnment 表,可以看到表中的数据行。

    运行编辑页面,将办公位置 Office Location 清除掉,然后保存 Save。在 Index 页面上办公位置将显示为空白,在表中的行被删除了。

    6-3  在教师编辑页面增加课程分配

    教师可以教授任意数量的课程。现在你需要扩展教师的编辑页面,增加通过一系列复选框分配可能的能力,如下所示。

    在课程 Course 和教师 Instructor 之间存在多对多的关系,这意味着你不需要直接访问表之间的关联。而是通过增加或者删除 Instructor. Course 实体来完成。

    在 UI 界面上,与教师相关的课程被显示为一组复选框,在数据库中的每一门课程都有一个对应的复选框,教师当前教授的课程对应的复选框处于被选中状态。用户可以通过选中或者取消选中来改变教师教授的课程。如果课程的数量巨大,你可能需要采取其他的方式来显示这些数据,但是你可以使用类似的方式来控制导航属性以便创建和删除关系。

    为了为视图提供复选框数据,你需要使用视图模型 ViewModel,在 ViewModels 文件夹中创建 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;
    }

    新创建的方法中,读取所有的课程实体,加载到视图模型中,对于每一个课程,检查这个课程是否存在于教师实体的 Courses 导航集合属性中。为了更加有效地遍历教师讲授的课程,将教师讲授的课程通过 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);
                }
            }
        }
    }

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

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

    然后,代码遍历数据库中所有的课程,如果课程的复选框被选中了,但是没有包含在教师 Insturctor 的 Courses 集合中。这个课程将会被加入到导航属性集合中。

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

    如果课程没有被选中,但是在教师的导航属性 Courses 集合中,就从集合属性中删除掉。

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

    在 Views\Instructor\Edit.cshtml 中,在 OfficeAssignment 区域的 div 之后,增加一个 Courses 区域,通过一组复选框来显示课程的状态。

    <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>

    代码创建一个 HTML 的三列表格,每一列中显示课程的标题和编号,后面跟着一个复选框。复选框的名称是相同的 ( “selectedCourses” ),这样在模型绑定的时候就会被连接成一组。复选框的 value 属性设置为 CourseID。当提交页面的时候,选中的复选框代表的 CourseID 将以数组的形式传递给控制器。

    在复选框被初始化的时候,选中课程对应的复选框的 checked 属性被设置为选中状态。

    在修改了课程状态之后,当回到 Index 页面的时候,需要验证这些修改。因此,需要在页面的表格中增加一列,在这里不需要使用 ViewBag,因为需要的信息已经通过教师实体 Instructor 的导航属性 Courses 传递到页面了。

    在 Views\Instuctor\Index.cshtml 中,在 <th>Office</th> 之后,增加一个 <th>Course</th> 的标题列,如下所示。

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

    然后,在办公位置的单元格之后增加一个详细内容的单元格。

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

    运行 Instructor 的 Index 页面,检查教师的授课情况。

    点击 Edit 查看编辑页面。

    修改一些课程的授课情况,然后保存 Save,修改的结果在 Index 页面中可以看到。

    你已经完成了修改关联数据的工作。通过目前的课程你已经可以完成增、删、改、查所有的操作,但是还没有考虑并发问题。下一次我们将探讨并发问题,展示处理并发的方式,然后对已经完成的实体的增、删、改、查的代码增加并发处理。

  • 相关阅读:
    LeetCode 81 Search in Rotated Sorted Array II(循环有序数组中的查找问题)
    LeetCode 80 Remove Duplicates from Sorted Array II(移除数组中出现两次以上的元素)
    LeetCode 79 Word Search(单词查找)
    LeetCode 78 Subsets (所有子集)
    LeetCode 77 Combinations(排列组合)
    LeetCode 50 Pow(x, n) (实现幂运算)
    LeetCode 49 Group Anagrams(字符串分组)
    LeetCode 48 Rotate Image(2D图像旋转问题)
    LeetCode 47 Permutations II(全排列)
    LeetCode 46 Permutations(全排列问题)
  • 原文地址:https://www.cnblogs.com/haogj/p/2456181.html
Copyright © 2011-2022 走看看