자습서: 관련 데이터 업데이트 - ASP.NET MVC 및 EF Core 사용

이전 자습서에서는 관련 데이터를 표시했습니다. 이 자습서에서는 외래 키 필드 및 탐색 속성을 업데이트하여 관련된 데이터를 업데이트합니다.

다음 그림에서는 사용할 일부 페이지를 보여 줍니다.

Course Edit page

Edit Instructor page

이 자습서에서는 다음을 수행합니다.

  • 강좌 페이지 사용자 지정
  • 강사 편집 페이지 추가
  • 편집 페이지에 강좌 추가
  • 삭제 페이지 업데이트
  • 만들기 페이지에 사무실 위치 및 강좌 추가

필수 조건

강좌 페이지 사용자 지정

Course 엔터티가 만들어질 때 기존 부서에 대한 관계가 있어야 합니다. 이를 수행하기 위해 스캐폴드 코드는 컨트롤러 메서드 및 부서를 선택하기 위한 드롭다운 목록을 포함하는 만들기 및 편집 보기를 포함합니다. 드롭다운 목록은 Course.DepartmentID 외래 키 속성을 설정하고, 이는 적절한 Department 엔터티로 Department 탐색 속성을 로드하기 위해 필요한 모든 Entity Framework입니다. 스캐폴드 코드를 사용하지만 오류 처리를 추가하고 드롭다운 목록을 정렬하도록 약간 변경합니다.

에서 CoursesController.cs네 개의 Create 및 Edit 메서드를 삭제하고 다음 코드로 바꿉다.

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(nameof(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()
        .FirstOrDefaultAsync(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
        .FirstOrDefaultAsync(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(nameof(Index));
    }
    PopulateDepartmentsDropDownList(courseToUpdate.DepartmentID);
    return View(courseToUpdate);
}

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

PopulateDepartmentsDropDownList 메서드는 이름을 기준으로 정렬하는 모든 부서의 목록을 가져오고, 드롭다운 목록에 대한 SelectList 컬렉션을 만들고, 컬렉션을 ViewBag의 보기에 전달합니다. 메서드는 호출 코드가 드롭다운 목록이 렌더링될 때 선택될 항목을 지정하도록 허용하는 선택적 selectedDepartment 매개 변수를 허용합니다. 보기가 "DepartmentID"라는 이름을 <select> 태그 도우미에 전달하면 도우미는 "DepartmentID"라는 SelectList에 대한 ViewBag 개체에서 보는 것을 압니다.

새 강좌의 경우 부서는 아직 설정되지 않기 때문에 HttpGet Create 메서드는 선택한 항목을 설정하지 않고 PopulateDepartmentsDropDownList 메서드를 호출합니다.

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

HttpGet Edit 메서드는 편집 중인 강좌에 이미 할당되어 있는 부서의 ID에 따라 선택한 항목을 설정합니다.

public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var course = await _context.Courses
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.CourseID == id);
    if (course == null)
    {
        return NotFound();
    }
    PopulateDepartmentsDropDownList(course.DepartmentID);
    return View(course);
}

CreateEdit 모두에 대한 HttpPost 메서드는 오류가 발생한 후 페이지를 다시 표시할 때 선택한 항목을 설정하는 코드를 포함합니다. 이렇게 하면 오류 메시지를 표시하기 위해 페이지가 다시 표시될 때 선택한 부서에 관계 없이 선택된 상태로 유지됩니다.

세부 정보 및 삭제 메서드에 AsNoTracking 추가

강좌 세부 정보 및 삭제 페이지의 성능을 최적화하기 위해 Details 및 HttpGet Delete 메서드에 AsNoTracking 호출을 추가합니다.

public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var course = await _context.Courses
        .Include(c => c.Department)
        .AsNoTracking()
        .FirstOrDefaultAsync(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()
        .FirstOrDefaultAsync(m => m.CourseID == id);
    if (course == null)
    {
        return NotFound();
    }

    return View(course);
}

강좌 보기 수정

부서 Views/Courses/Create.cshtml드롭다운 목록에 "부서 선택" 옵션을 추가하고, 캡션 DepartmentID에서 Department변경하고, 유효성 검사 메시지를 추가합니다.

<div class="form-group">
    <label asp-for="Department" class="control-label"></label>
    <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>

에서 Views/Courses/Edit.cshtml방금 했던 부서 필드에 대해 동일하게 변경합니다.Create.cshtml

또한 제목 Views/Courses/Edit.cshtml 필드 앞에 강좌 번호 필드를 추가합니다. 강좌 번호는 기본 키이기 때문에 표시되지만 변경될 수 없습니다.

<div class="form-group">
    <label asp-for="CourseID" class="control-label"></label>
    <div>@Html.DisplayFor(model => model.CourseID)</div>
</div>

편집 보기에 강좌 번호에 대해 이미 숨겨진 필드(<input type="hidden">)가 있습니다. 사용자가 편집 페이지에서 저장을 클릭할 때 강좌 번호가 게시된 데이터에 삽입되도록 하지 않으므로 <label> 태그 도우미를 추가하는 것은 숨겨진 필드에 대한 필요성을 없애지 않습니다.

에서 Views/Courses/Delete.cshtml위쪽에 강좌 번호 필드를 추가하고 부서 ID를 부서 이름으로 변경합니다.

@model ContosoUniversity.Models.Course

@{
    ViewData["Title"] = "Delete";
}

<h2>Delete</h2>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Course</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.CourseID)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.CourseID)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Credits)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Credits)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Department)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Department.Name)
        </dd>
    </dl>
    
    <form asp-action="Delete">
        <div class="form-actions no-color">
            <input type="submit" value="Delete" class="btn btn-default" /> |
            <a asp-action="Index">Back to List</a>
        </div>
    </form>
</div>

에서 Views/Courses/Details.cshtml방금 변경한 Delete.cshtml것과 동일하게 변경합니다.

강좌 페이지 테스트

앱을 실행하고, 강좌 탭을 선택하고, 새로 만들기를 클릭하고, 새 강좌에 대한 데이터를 입력합니다.

Course Create page

만들기를 클릭합니다. 강좌 인덱스 페이지가 목록에 추가된 새 강좌로 표시됩니다. 인덱스 페이지 목록의 부서 이름은 관계가 올바르게 설정되었음을 표시하는 탐색 속성에서 제공됩니다.

강좌 인덱스 페이지의 강좌에서 편집을 클릭합니다.

Course Edit page

페이지에서 데이터를 변경하고 저장을 클릭합니다. 강좌 인덱스 페이지가 업데이트된 강좌 데이터로 표시됩니다.

강사 편집 페이지 추가

강사 레코드를 편집할 때 강사의 사무실 할당을 업데이트할 수 있습니다. Instructor 엔터티는 OfficeAssignment 엔터티와 일 대 영 또는 일 대 일 관계가 있으며, 이는 코드가 다음의 경우를 처리해야 함을 의미합니다.

  • 사용자가 사무실 할당 선택을 취소하고 원래 값이 있었던 경우 OfficeAssignment 엔터티를 삭제합니다.

  • 사용자가 사무실 할당 값을 입력하고 원래 값이 비어 있었던 경우 새 OfficeAssignment 엔터티를 만듭니다.

  • 사용자가 사무실 할당의 값을 변경하는 경우 기존 OfficeAssignment 엔터티의 값을 변경합니다.

강사 컨트롤러 업데이트

InstructorsController.cs에서 강사 엔터티의 OfficeAssignment 탐색 속성을 로드하고 AsNoTracking을 호출하도록 HttpGet Edit 메서드에서 코드를 변경합니다.

public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var instructor = await _context.Instructors
        .Include(i => i.OfficeAssignment)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);
    if (instructor == null)
    {
        return NotFound();
    }
    return View(instructor);
}

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)
        .FirstOrDefaultAsync(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(nameof(Index));
    }
    return View(instructorToUpdate);
}

코드는 다음을 수행합니다.

  • 서명은 이제 HttpGet Edit 메서드와 동일하므로 메서드 이름을 EditPost로 변경합니다(ActionName 특성은 /Edit/ URL이 여전히 사용됨을 지정함).

  • OfficeAssignment 탐색 속성에 대한 즉시 로드를 사용하여 데이터베이스에서 현재 Instructor 엔터티를 가져옵니다. 이는 HttpGet Edit 메서드에서 수행한 것과 동일합니다.

  • 모델 바인더의 값으로 검색된 Instructor 엔터티를 업데이트합니다. TryUpdateModel 오버로드를 통해 포함하려는 속성을 선언할 수 있습니다. 이렇게 하면 두 번째 자습서에 설명된 대로 초과 게시를 방지합니다.

    if (await TryUpdateModelAsync<Instructor>(
        instructorToUpdate,
        "",
        i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))
    
  • 사무실 위치가 비어 있는 경우 OfficeAssignment 테이블의 관련된 행이 삭제되도록 Instructor.OfficeAssignment 속성을 Null로 설정합니다.

    if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
    {
        instructorToUpdate.OfficeAssignment = null;
    }
    
  • 변경 내용을 데이터베이스에 저장합니다.

강사 편집 보기 업데이트

저장 Views/Instructors/Edit.cshtml단추 앞 의 끝에 사무실 위치를 편집하기 위한 새 필드를 추가합니다.

<div class="form-group">
    <label asp-for="OfficeAssignment.Location" class="control-label"></label>
    <input asp-for="OfficeAssignment.Location" class="form-control" />
    <span asp-validation-for="OfficeAssignment.Location" class="text-danger" />
</div>

앱을 실행하고, 강사 탭을 선택한 다음, 강사에서 편집을 클릭합니다. 사무실 위치를 변경하고 저장을 클릭합니다.

Instructor Edit page

편집 페이지에 강좌 추가

강사는 강좌 수에 관계 없이 가르칠 수 있습니다. 이제 다음 스크린샷에 표시된 것처럼 확인란 그룹을 사용하여 강좌 할당을 변경하는 기능을 추가하여 강사 편집 페이지를 향상시킵니다.

Instructor Edit page with courses

CourseInstructor 엔터티 간에는 다 대 다 관계가 있습니다. 관계를 추가하고 제거하려면 CourseAssignments 조인 엔터티 집합에서 엔터티를 추가하고 제거합니다.

강사에게 할당된 강좌를 변경할 수 있도록 하는 UI는 확인란의 그룹입니다. 데이터베이스의 모든 강좌에 대한 확인란이 표시되고 강사에게 현재 할당되어 있는 것이 선택됩니다. 사용자는 확인란을 선택하거나 선택 취소하여 과정 할당을 변경할 수 있습니다. 강좌의 수가 훨씬 큰 경우 보기에서 데이터를 나타내는 다른 방법을 사용하길 원할 것입니다. 하지만 관계를 만들거나 삭제하기 위해 조인 엔터티를 조작하는 동일한 메서드를 사용합니다.

강사 컨트롤러 업데이트

확인란의 목록에 대한 보기에 데이터를 제공하려면 보기 모델 클래스를 사용합니다.

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

에서 InstructorsController.csHttpGet 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.CourseAssignments).ThenInclude(i => i.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(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.CourseAssignments.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)
        });
    }
    ViewData["Courses"] = viewModel;
}

코드는 Courses 탐색 속성에 대해 즉시 로드를 추가하고 새 PopulateAssignedCourseData 메서드를 호출하여 AssignedCourseData 보기 모델 클래스를 사용하여 확인란 배열에 대한 정보를 제공합니다.

PopulateAssignedCourseData 메서드의 코드는 보기 모델 클래스를 사용하는 강좌의 목록을 로드하기 위해 모든 Course 엔터티를 통해 읽습니다. 각 강좌의 경우 코드는 강좌가 강사의 Courses 탐색 속성에 있는지 여부를 확인합니다. 강좌가 강사에게 할당되었는지 여부를 확인할 때 효율적인 조회를 만들기 위해 강사에게 할당된 강좌는 HashSet 컬렉션에 배치됩니다. Assigned 속성은 강사에게 할당된 강좌에 대해 true로 설정됩니다. 보기는 이 속성을 사용하여 선택된 것으로 표시되어야 하는 확인란을 결정합니다. 마지막으로 목록은 ViewData의 보기에 전달됩니다.

다음으로 사용자가 저장을 클릭할 때 실행되는 코드를 추가합니다. 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.CourseAssignments)
            .ThenInclude(i => i.Course)
        .FirstOrDefaultAsync(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(nameof(Index));
    }
    UpdateInstructorCourses(selectedCourses, instructorToUpdate);
    PopulateAssignedCourseData(instructorToUpdate);
    return View(instructorToUpdate);
}
private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
    if (selectedCourses == null)
    {
        instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
        return;
    }

    var selectedCoursesHS = new HashSet<string>(selectedCourses);
    var instructorCourses = new HashSet<int>
        (instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
    foreach (var course in _context.Courses)
    {
        if (selectedCoursesHS.Contains(course.CourseID.ToString()))
        {
            if (!instructorCourses.Contains(course.CourseID))
            {
                instructorToUpdate.CourseAssignments.Add(new CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID = course.CourseID });
            }
        }
        else
        {

            if (instructorCourses.Contains(course.CourseID))
            {
                CourseAssignment courseToRemove = instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID == course.CourseID);
                _context.Remove(courseToRemove);
            }
        }
    }
}

메서드 서명은 이제 HttpGet Edit 메서드와 다르므로 메서드 이름은 EditPost에서 Edit로 다시 변경됩니다.

보기에는 강좌 엔터티의 컬렉션이 없으므로 모델 바인더는 CourseAssignments 탐색 속성을 자동으로 업데이트할 수 없습니다. CourseAssignments 탐색 속성을 업데이트하는 데 모델 바인더를 사용하는 대신 새 UpdateInstructorCourses 메서드에서 해당 작업을 수행합니다. 따라서 모델 바인딩에서 CourseAssignments 속성을 제외해야 합니다. 명시적 승인이 필요한 오버로드를 사용 중이고 CourseAssignments가 포함 목록에 있지 않으므로 TryUpdateModel을 호출하는 코드를 변경할 필요가 없습니다.

확인란이 선택되지 않은 경우 UpdateInstructorCourses의 코드는 빈 컬렉션으로 CourseAssignments 탐색 속성을 초기화하고 다음을 반환합니다.

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

    var selectedCoursesHS = new HashSet<string>(selectedCourses);
    var instructorCourses = new HashSet<int>
        (instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
    foreach (var course in _context.Courses)
    {
        if (selectedCoursesHS.Contains(course.CourseID.ToString()))
        {
            if (!instructorCourses.Contains(course.CourseID))
            {
                instructorToUpdate.CourseAssignments.Add(new CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID = course.CourseID });
            }
        }
        else
        {

            if (instructorCourses.Contains(course.CourseID))
            {
                CourseAssignment courseToRemove = instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID == course.CourseID);
                _context.Remove(courseToRemove);
            }
        }
    }
}

그런 다음, 코드는 데이터베이스의 모든 강좌를 반복하고 현재 강사에게 할당된 것과 보기에서 선택되었던 것에 대해 각 강좌를 확인합니다. 효율적인 조회를 수행하기 위해 후자의 두 컬렉션은 HashSet 개체에 저장됩니다.

강좌에 대한 확인란이 선택됐지만 강좌가 Instructor.CourseAssignments 탐색 속성에 없는 경우 강좌는 탐색 속성의 컬렉션에 추가됩니다.

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

    var selectedCoursesHS = new HashSet<string>(selectedCourses);
    var instructorCourses = new HashSet<int>
        (instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
    foreach (var course in _context.Courses)
    {
        if (selectedCoursesHS.Contains(course.CourseID.ToString()))
        {
            if (!instructorCourses.Contains(course.CourseID))
            {
                instructorToUpdate.CourseAssignments.Add(new CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID = course.CourseID });
            }
        }
        else
        {

            if (instructorCourses.Contains(course.CourseID))
            {
                CourseAssignment courseToRemove = instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID == course.CourseID);
                _context.Remove(courseToRemove);
            }
        }
    }
}

강좌에 대한 확인란이 선택되지 않았지만 강좌가 Instructor.CourseAssignments 탐색 속성에 있는 경우 강좌는 탐색 속성에서 제거됩니다.

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

    var selectedCoursesHS = new HashSet<string>(selectedCourses);
    var instructorCourses = new HashSet<int>
        (instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
    foreach (var course in _context.Courses)
    {
        if (selectedCoursesHS.Contains(course.CourseID.ToString()))
        {
            if (!instructorCourses.Contains(course.CourseID))
            {
                instructorToUpdate.CourseAssignments.Add(new CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID = course.CourseID });
            }
        }
        else
        {

            if (instructorCourses.Contains(course.CourseID))
            {
                CourseAssignment courseToRemove = instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID == course.CourseID);
                _context.Remove(courseToRemove);
            }
        }
    }
}

강사 보기 업데이트

Views/Instructors/Edit.cshtml에서 사무실 필드에 대한 div 요소 후와 저장 단추에 대한 div 요소 전에 다음 코드를 즉시 추가하여 확인란의 배열로 강좌 필드를 추가합니다.

참고 항목

Visual Studio에서 코드를 붙여넣을 때 줄 바꿈이 코드를 중단하는 방식으로 변경될 수 있습니다. 붙여넣은 후 코드가 다르게 보이면 Ctrl+Z를 한 번 눌러 자동 서식 지정을 실행 취소합니다. 여기와 같은 모양이 되도록 줄 바꿈을 수정합니다. 들여쓰기는 완벽할 필요가 없지만 @:</tr><tr>, @:<td>, @:</td>@:</tr> 줄은 표시된 것처럼 각각 한 줄에 있어야 합니다. 그렇지 않으면 런타임 오류가 발생합니다. 선택된 새 코드의 블록과 함께 Tab 키를 세 번 눌러 기존 코드와 함께 새 코드를 정렬합니다. 이 문제는 Visual Studio 2019에서 해결되었습니다.

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

이 코드는 세 개의 열이 있는 HTML 테이블을 만듭니다. 각 열은 강좌 번호 및 제목으로 구성된 캡션이 뒤에 오는 확인란입니다. 확인란은 모두 모델 바인더에게 그룹으로 간주된다는 것을 알리는 동일한 이름(“selectedCourses”)을 갖습니다. 각 확인란의 값 특성은 CourseID의 값으로 설정됩니다. 페이지가 게시되면 모델 바인더는 선택된 확인란에 대한 CourseID 값으로 구성된 배열을 컨트롤러에 전달합니다.

확인란이 처음으로 렌더링될 때 강사에게 할당된 강좌에 대한 것은 해당 항목을 선택하는 특성을 선택했습니다(선택된 것으로 표시).

앱을 실행하고, 강사 탭을 선택하고, 강사에서 편집을 클릭하여 편집 페이지를 봅니다.

Instructor Edit page with courses

일부 강좌 할당을 변경하고 저장을 클릭합니다. 변경 내용은 인덱스 페이지에 반영됩니다.

참고 항목

강사 강좌 데이터를 편집하기 위해 여기에 적용되는 방법은 제한된 수의 강좌가 있는 경우에 잘 작동합니다. 훨씬 큰 컬렉션의 경우 다른 UI 및 다른 업데이트 메서드가 필요합니다.

삭제 페이지 업데이트

에서 InstructorsController.cs메서드를 DeleteConfirmed 삭제하고 그 자리에 다음 코드를 삽입합니다.

[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
    Instructor instructor = await _context.Instructors
        .Include(i => i.CourseAssignments)
        .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(nameof(Index));
}

이 코드로 다음이 변경됩니다.

  • CourseAssignments 탐색 속성에 대해 즉시 로드를 수행합니다. 이를 포함해야 합니다. 그렇지 않으면 EF는 관련된 CourseAssignment 엔터티에 대해 알지 못하고 삭제하지 않습니다. 읽을 필요가 없도록 하려면 여기에서 데이터베이스에 계단식 삭제를 구성할 수 있습니다.

  • 삭제될 강사가 부서의 관리자로 할당된 경우 해당 부서에서 강사 할당을 제거합니다.

만들기 페이지에 사무실 위치 및 강좌 추가

InstructorsController.cs에서 HttpGet 및 HttpPost Create 메서드를 삭제한 다음, 해당 위치에 다음 코드를 추가합니다.

public IActionResult Create()
{
    var instructor = new Instructor();
    instructor.CourseAssignments = 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.CourseAssignments = new List<CourseAssignment>();
        foreach (var course in selectedCourses)
        {
            var courseToAdd = new CourseAssignment { InstructorID = instructor.ID, CourseID = int.Parse(course) };
            instructor.CourseAssignments.Add(courseToAdd);
        }
    }
    if (ModelState.IsValid)
    {
        _context.Add(instructor);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    PopulateAssignedCourseData(instructor);
    return View(instructor);
}

이 코드는 처음에 선택된 강좌가 없다는 것을 제외하고 Edit 메서드에 대해 확인한 것과 유사합니다. 선택된 강좌가 있을 수 있기 때문이 아니라 보기의 foreach 반복에 대해 빈 컬렉션을 제공하기 위해 HttpGet Create 메서드는 PopulateAssignedCourseData 메서드를 호출합니다(그렇지 않은 경우 보기 코드는 null 참조 예외를 throw함).

HttpPost Create 메서드는 유효성 검사 오류를 확인하고 데이터베이스에 새 강사를 추가하기 전에 CourseAssignments 탐색 속성에 선택된 각 강좌를 추가합니다. 모델 오류가 있더라도 강좌가 추가되므로 모델 오류가 있는 경우(예: 사용자가 잘못된 날짜를 키 지정함) 페이지는 오류 메시지와 함께 다시 표시되고, 선택한 모든 강좌는 자동으로 다시 저장됩니다.

CourseAssignments 탐색 속성에 강좌를 추가할 수 있도록 빈 컬렉션으로 속성을 초기화해야 합니다.

instructor.CourseAssignments = new List<CourseAssignment>();

컨트롤러 코드에서 이 작업을 수행하는 대안으로 다음 예제와 같이 존재하지 않는 경우 자동으로 컬렉션을 만들도록 getter 속성을 변경하여 Instructor 모델에서 해당 작업을 수행할 수 있습니다.

private ICollection<CourseAssignment> _courseAssignments;
public ICollection<CourseAssignment> CourseAssignments
{
    get
    {
        return _courseAssignments ?? (_courseAssignments = new List<CourseAssignment>());
    }
    set
    {
        _courseAssignments = value;
    }
}

이러한 방식으로 CourseAssignments 속성을 수정하는 경우 컨트롤러에서 명시적 속성 초기화 코드를 제거할 수 있습니다.

[Views/Instructor/Create.cshtml제출] 단추 앞에 사무실 위치 텍스트 상자와 검사box를 추가합니다. 편집 페이지의 경우와 같이 Visual Studio에서 붙여넣을 때 코드의 서식을 다시 지정하는 경우 서식 지정을 수정합니다.

<div class="form-group">
    <label asp-for="OfficeAssignment.Location" class="control-label"></label>
    <input asp-for="OfficeAssignment.Location" class="form-control" />
    <span asp-validation-for="OfficeAssignment.Location" class="text-danger" />
</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>

앱을 실행하고 강사를 만들어 테스트합니다.

트랜잭션 처리

CRUD 자습서에 설명된 대로 Entity Framework는 트랜잭션을 암시적으로 구현합니다. 더 많은 컨트롤이 필요한 시나리오의 경우, 예를 들어 트랜잭션의 Entity Framework 밖에서 수행한 작업을 포함하려는 경우 트랜잭션을 참조하세요.

코드 가져오기

완성된 애플리케이션을 다운로드하거나 확인합니다.

다음 단계

이 자습서에서는 다음을 수행합니다.

  • 강좌 페이지 사용자 지정
  • 강사 편집 페이지 추가
  • 편집 페이지에 강좌 추가
  • 삭제 페이지 업데이트
  • 만들기 페이지에 사무실 위치 및 강좌 추가

동시성 충돌을 처리하는 방법을 알아보려면 다음 자습서로 진행합니다.