次の方法で共有


ASP.NET MVC アプリケーションでの Entity Framework を使用した関連データの更新 (6/10)

著者: Tom Dykstra

Contoso University のサンプル Web アプリケーションでは、Entity Framework 5 Code First と Visual Studio 2012 を使用して ASP.NET MVC 4 アプリケーションを作成する方法を示します。 チュートリアル シリーズについては、シリーズの最初のチュートリアルを参照してください。

Note

解決できない問題が発生した場合は、完了した章をダウンロードして、問題を再現してみてください。 通常、コードを完成したコードと比較することで、問題の解決策を見つけることができます。 一般的なエラーとその解決方法については、「エラーと回避策」をご覧ください。

前のチュートリアルでは、関連データを表示しました。このチュートリアルでは、関連データを更新します。 ほとんどのリレーションシップでは、適切な外部キー フィールドを更新することで、これを行うことができます。 多対多リレーションシップの場合、Entity Framework は結合テーブルを直接公開しないため、適切なナビゲーション プロパティとの間でエンティティを明示的に追加および削除する必要があります。

以下の図は、使用するページを示しています。

Screenshot that shows the Create Course page.

Screenshot that displays the Instructor Edit page.

Courses の Create ページと Edit ページをカスタマイズする

新しいコース エンティティが作成されると、既存の部門とのリレーションシップが必要になります。 これを容易にするため、スキャフォールディング コードには、コントローラーのメソッドと、部門を選択するためのドロップダウン リストを含む Create ビューと Edit ビューが含まれます。 ドロップダウン リストは、Course.DepartmentID 外部キー プロパティを設定します。これは、Department ナビゲーション プロパティを適切な Department エンティティとともに読み込むためにすべての Entity Framework で必要です。 このスキャフォールディング コードを使用しますが、エラー処理を追加し、ドロップダウン リストを並べ替えるために少し変更します。

"CourseController.cs" で、4 つの Edit および Create メソッドを削除し、次のコードに置き換えます。

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 (DataException /* dex */)
   {
      //Log the error (uncomment dex variable name after DataException and add a line here to write a log.)
      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]
[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 (DataException /* dex */)
   {
      //Log the error (uncomment dex variable name after DataException and add a line here to write a log.)
      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 メソッドは名前で並べ替えたすべての部門のリストを取得し、ドロップダウン リスト用に SelectList コレクションを作成し、そのコレクションを ViewBag プロパティでビューに渡します。 このメソッドは、ドロップダウン リストがレンダリングされるときに選択される項目を指定するためのコード呼び出しを許可する、省略可能な selectedDepartment パラメーターを受け取ります。 ビューではヘルパーに名前 DepartmentIDDropDownList ヘルパーに渡します。これで、ヘルパーは ViewBag オブジェクト中から DepartmentID という名前の SelectList を検索することを認識します。

新しいコースの場合、部門がまだ設立されていないため、HttpGetCreate メソッドは、選択した項目を設定せずに PopulateDepartmentsDropDownList メソッドを呼び出します。

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

HttpGetEdit メソッドは、編集中のコースに既に割り当てられている部門の ID に基づいて、選択したアイテムを設定します。

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

CreateEdit の両方の HttpPost メソッドには、エラーの発生後にページを再表示するときに選択した項目を設定するコードも含まれています。

catch (DataException /* dex */)
{
    //Log the error (uncomment dex variable name after DataException and add a line here to write a log.)
    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" で、強調表示されたコードを追加して、タイトル フィールドの前に新しいコース番号フィールドを作成します。 前のチュートリアルで説明したように、主キー フィールドは既定ではスキャフォールディングされませんが、この主キーは意味があるため、ユーザーがキー値を入力できるようにします。

@model ContosoUniversity.Models.Course

@{
    ViewBag.Title = "Create";
}

<h2>Create</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>Course</legend>

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

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

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

        <div class="editor-label">
            @Html.LabelFor(model => model.DepartmentID, "Department")
        </div>
        <div class="editor-field">
            @Html.DropDownList("DepartmentID", String.Empty)
            @Html.ValidationMessageFor(model => model.DepartmentID)
        </div>

        <p>
            <input type="submit" value="Create" />
        </p>
    </fieldset>
}

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

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

"Views\Course\Edit.cshtml"、"Views\Course\Delete.cshtml"、"Views\Course\Details.cshtml"で、Title フィールドの前にコース番号フィールドを追加します。 これは主キーであるため、表示されますが、変更できません。

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

[作成] ページを実行 (コース インデックス ページを表示し、[新規作成] をクリック) し、新しいコースのデータを入力します。

Course_create_page

Create をクリックしてください。 コース/インデックス ページには、リストに追加された新しいコースが表示されます。 Index ページのリストの部門名は、ナビゲーション プロパティから取得され、リレーションシップが正常に確立されていることを示しています。

Course_Index_page_showing_new_course

[編集] ページを実行 (コース インデックス ページを表示し、コースの [編集] をクリック) します。

Course_edit_page

ページ上のデータを変更し、 [Save](保存) をクリックします。 コース インデックス ページには、更新されたコース データが表示されます。

インストラクタの編集ページの追加

インストラクター レコードを編集するときに、インストラクターのオフィスの割り当ての更新が必要な場合があります。 Instructor エンティティには、OfficeAssignment エンティティとの一対ゼロまたは一対一のリレーションシップがあります。これは、コードで次の状況を処理する必要があることを意味します。

  • 元は値のあったオフィスの割り当てをクリアする場合は、OfficeAssignment エンティティを削除します。
  • 元は空白だったオフィスの割り当ての値を入力する場合は、新しい OfficeAssignment エンティティを作成します。
  • ユーザーがオフィスの割り当ての値を変更する場合は、既存の OfficeAssignment エンティティの値を変更します。

"InstructorController.cs" を開き、HttpGetEdit メソッドを見てください。

public ActionResult Edit(int id = 0)
{
    Instructor instructor = db.Instructors.Find(id);
    if (instructor == null)
    {
        return HttpNotFound();
    }
    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)
        .Where(i => i.InstructorID == id)
        .Single();
    return View(instructor);
}

このコードは ViewBag ステートメントを削除し、関連付けられている OfficeAssignment エンティティの一括読み込みを追加します。 Find メソッドでは一括読み込みを実行できないため、代わりにインストラクターを選択するためにメソッドと Where および Single メソッドを使用します。

HttpPostEdit メソッドを次のコードに置き換えます。 オフィスの割り当ての更新を処理します。

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(int id, FormCollection formCollection)
{
   var instructorToUpdate = db.Instructors
       .Include(i => i.OfficeAssignment)
       .Where(i => i.InstructorID == 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 (DataException /* dex */)
      {
         //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
         ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
      }
   }
   ViewBag.InstructorID = new SelectList(db.OfficeAssignments, "InstructorID", "Location", id);
   return View(instructorToUpdate);
}

このコードは次のことを行います。

  • OfficeAssignment ナビゲーション プロパティの一括読み込みを使用して、現在の Instructor エンティティをデータベースから取得します。 これは、HttpGetEdit メソッドで行ったものと同じです。

  • モデル バインダーからの値を使用して、取得した Instructor エンティティを更新します。 TryUpdateModel オーバーロードを使用すると、含めるプロパティを "safelist" できます。 これにより、2 番目のチュートリアルで説明したように、過剰ポスティングを防止します。

    if (TryUpdateModel(instructorToUpdate, "",
          new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" }))
    
  • オフィスの場所が空白の場合は、OfficeAssignment テーブル内の関連する行が削除されるように、Instructor.OfficeAssignment プロパティを null に設定します。

    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>

ページを実行 ([インストラクター] タブを選択し、[編集] をクリック) します。 [Office Location](オフィスの場所) を変更し、 [Save](保存) をクリックします。

Changing_the_office_location

インストラクターの編集ページへのコース割り当ての追加

インストラクターは、任意の数のコースを担当する場合があります。 次のスクリーン ショットに示すように、チェック ボックスのグループを使用して、コースの割り当てを変更する機能を追加して、Instructor/Edit ページを拡張します。

Screenshot that shows the Instructor Edit page with courses.

CourseInstructor エンティティの間のリレーションシップは多対多であるため、結合テーブルに直接アクセスできません。 代わりに、Instructor.Courses ナビゲーション プロパティとの間でエンティティを追加および削除します。

インストラクターに割り当てられるコースを変更できるようにする UI は、チェック ボックスのグループです。 データベース内のすべてのコースのチェック ボックスが表示され、インストラクターに現在割り当てられているコースが選択されます。 ユーザーは、チェック ボックスをオンまたはオフにしてコースの割り当てを変更できます。 コースの数が非常に多い場合は、ビューにデータを表示する別のメソッドを使用したいと考えますが、結合エンティティを操作してリレーションシップを作成または削除する場合と同じメソッドを使用します。

チェック ボックスのリストのためにデータをビューに提供するには、ビュー モデル クラスを使用します。 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" で、HttpGetEdit メソッドを次のコードで置き換えます。 変更が強調表示されます。

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 ナビゲーション プロパティに一括読み込みを追加し、新しい PopulateAssignedCourseData メソッドを呼び出して、AssignedCourseData ビュー モデル クラスを使用してチェック ボックス配列に情報を提供します。

PopulateAssignedCourseData メソッド内のコードは、ビュー モデル クラスを使用してコースのリストを読み込むため、すべての Course エンティティを読み取ります。 各コースに対し、コードはそのコースがインストラクターの Courses ナビゲーション プロパティ内に存在しているかどうかをチェックします。 コースがインストラクターに割り当てられているかどうかをチェックするときに、効率的な参照を作成するため、インストラクターに割り当てられているコースが HashSet コレクション内に配置されます。 インストラクターが割り当てられているコースに対し、Assigned プロパティが true に設定されます。 ビューは、このプロパティを使用して、どのチェック ボックスを選択済みとして表示する必要があるかを判断します。 最後に、リストは ViewBag プロパティでビューに渡されます。

次に、ユーザーが [Save](保存) をクリックしたときに実行されるコードを追加します。 HttpPostEdit メソッドを次のコードで置き換え、Instructor エンティティの Courses ナビゲーション プロパティを更新する新しいメソッドを追加します。 変更が強調表示されます。

[HttpPost]
[ValidateAntiForgeryToken]
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, "", 
      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 (DataException /* dex */)
      {
         //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
         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);
         }
      }
   }
}

ビューには Course エンティティのコレクションがないため、モデル バインダーは Courses ナビゲーション プロパティを自動的に更新できません。 コース ナビゲーション プロパティを更新するためにモデル バインダーを使用する代わりに、新しい UpdateInstructorCourses メソッドでそれを行います。 そのため、モデル バインドから Courses プロパティを除外する必要があります。 安全にリスト化するオーバーロードを使用しており、Courses がインクルードリストに入っていないため、TryUpdateModel を呼び出すコードを変更する必要はありません。

チェック ボックスが選択されていない場合、UpdateInstructorCourses のコードは空のコレクションを使用して Courses ナビゲーション プロパティを初期化します。

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

その後コードは、データベース内のすべてのコースをループ処理し、各コースを現在インストラクターに割り当てられているコースとビューで選択されているコースを比較してチェックします。 検索を効率化するため、最後の 2 つのコレクションが 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);
    }
}

"Views\Instructor\Edit.cshtml" で、OfficeAssignment フィールドの div 要素の直後に次の強調表示されたコードを追加して、チェック ボックスの配列を含む Courses フィールドを追加します。

@model ContosoUniversity.Models.Instructor

@{
    ViewBag.Title = "Edit";
}

<h2>Edit</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>Instructor</legend>

        @Html.HiddenFor(model => model.InstructorID)

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

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

        <div class="editor-label">
            @Html.LabelFor(model => model.HireDate)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.HireDate)
            @Html.ValidationMessageFor(model => model.HireDate)
        </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>

        <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>
        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
}

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

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

このコードは、3 つの列を含む HTML テーブルを作成します。 各列には、チェック ボックスとその後に続くキャプションがあります。キャプションは、コース番号とタイトルから構成されます。 チェック ボックスはすべて同じ名前 ("selectedCourses") を持ち、これらをグループとして扱うようにモデル バインダーに通知します。 各チェック ボックスの value 属性は、CourseID. 値に設定されます。ページが投稿されると、モデル バインダーは、選択されたチェック ボックスのみの CourseID 値からなる配列をコントローラーに渡します。

チェック ボックスが最初にレンダリングされる場合、インストラクターに割り当てられるコースのチェック ボックスが checked 属性を持ち、選択されます (チェック ボックスがオンになった状態で表示されます)。

コースの割り当てを変更した後、サイトが Index ページに戻った際に変更を確認できるようにしたいと考えています。 そのため、そのページのテーブルに列を追加する必要があります。 この場合、表示する情報は、モデルとしてページに渡す Instructor エンティティの Courses ナビゲーション プロパティに既に含まれているため、ViewBag オブジェクトを使用する必要はありません。

"Views\Instructor\Index.cshtml" で、次の例に示すように、Office 見出しの直後に Courses 見出しを追加します。

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

次に、オフィスの所在詳細セルの直後に新しい詳細セルを追加します。

@model ContosoUniversity.ViewModels.InstructorIndexData

@{
    ViewBag.Title = "Instructors";
}

<h2>Instructors</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table>
    <tr>
        <th></th>
        <th>Last Name</th>
        <th>First Name</th>
        <th>Hire Date</th>
        <th>Office</th>
        <th>Courses</th>
    </tr>
    @foreach (var item in Model.Instructors)
    {
        string selectedRow = "";
        if (item.InstructorID == ViewBag.InstructorID)
        {
            selectedRow = "selectedrow";
        } 
        <tr class="@selectedRow" valign="top">
            <td>
                @Html.ActionLink("Select", "Index", new { id = item.InstructorID }) | 
                @Html.ActionLink("Edit", "Edit", new { id = item.InstructorID }) | 
                @Html.ActionLink("Details", "Details", new { id = item.InstructorID }) | 
                @Html.ActionLink("Delete", "Delete", new { id = item.InstructorID })
            </td>
            <td>
                @item.LastName
            </td>
            <td>
                @item.FirstMidName
            </td>
            <td>
                @String.Format("{0:d}", item.HireDate)
            </td>
            <td>
                @if (item.OfficeAssignment != null)
                { 
                    @item.OfficeAssignment.Location  
                }
            </td>
            <td>
                @{
                foreach (var course in item.Courses)
                {
                    @course.CourseID @:  @course.Title <br />
                }
                }
            </td>
        </tr> 
    }
</table>

@if (Model.Courses != null)
{ 
    <h3>Courses Taught by Selected Instructor</h3> 
    <table>
        <tr>
            <th></th>
            <th>ID</th>
            <th>Title</th>
            <th>Department</th>
        </tr>

        @foreach (var item in Model.Courses)
        {
            string selectedRow = "";
            if (item.CourseID == ViewBag.CourseID)
            {
                selectedRow = "selectedrow";
            } 
        
            <tr class="@selectedRow">

                <td>
                    @Html.ActionLink("Select", "Index", new { courseID = item.CourseID })
                </td>
                <td>
                    @item.CourseID
                </td>
                <td>
                    @item.Title
                </td>
                <td>
                    @item.Department.Name
                </td>
            </tr> 
        }

    </table> 
}
@if (Model.Enrollments != null)
{ 
    <h3>Students Enrolled in Selected Course</h3> 
    <table>
        <tr>
            <th>Name</th>
            <th>Grade</th>
        </tr>
        @foreach (var item in Model.Enrollments)
        { 
            <tr>
                <td>
                    @item.Student.FullName
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Grade)
                </td>
            </tr> 
        }
    </table> 
}

インストラクター インデックス ページを実行して、各インストラクターに割り当てられているコースを確認します。

Instructor_index_page

インストラクターの [編集] をクリックすると、編集ページが表示されます。

Instructor_edit_page_with_courses

一部のコース割り当てを変更し、[保存] をクリックします。 行った変更が Index ページに反映されます。

注: インストラクター コース データを編集するために採用されている方法は、コースの数が限られている場合にはうまく機能します。 非常に大きいコレクションの場合、別の UI と別の更新方法が必要になる場合があります。

Delete メソッドを更新する

インストラクターが削除された場合にオフィスの割り当てレコード (存在する場合) が削除されるように、HttpPost Delete メソッドのコードを変更します。

[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
   Instructor instructor = db.Instructors
     .Include(i => i.OfficeAssignment)
     .Where(i => i.InstructorID == id)
     .Single();

   instructor.OfficeAssignment = null;
   db.Instructors.Remove(instructor);
   db.SaveChanges();
   return RedirectToAction("Index");
}

管理者として部門に割り当てられているインストラクターを削除しようとすると、参照整合性エラーが発生します。 インストラクターが管理者として割り当てられている部門からインストラクターを自動的に削除する追加のコードについては、このチュートリアルの現在のバージョンをご覧ください。

まとめ

これで関連データを扱うための入門編が完了しました。 ここまでのチュートリアルでは、さまざまな CRUD 操作を行いましたが、まだコンカレンシーの問題には対処していません。 次のチュートリアルでは、コンカレンシーのトピックを紹介し、それを処理するためのオプションについて説明ます。1 つのエンティティ型に対して既に記述した CRUD コードにコンカレンシー処理を追加します。

他の Entity Framework リソースへのリンクは、このチュートリアル シリーズの最後にあります。