次の方法で共有


チュートリアル: ASP.NET MVC アプリで EF を使用して関連データを更新する

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

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

Course_create_page

Instructor_edit_page_with_courses

コースで講師が編集する

このチュートリアルでは、次の作業を行いました。

  • Courses ページをカスタマイズする
  • Instructors ページにオフィスを追加する
  • Instructors ページにコースを追加する
  • DeleteConfirmed を更新する
  • オフィスの場所とコースを Create ページに追加する

前提条件

Courses ページをカスタマイズする

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

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

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 /* dex */)
    {
        //Log the error (uncomment dex variable name 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)
{
    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, ActionName("Edit")]
[ValidateAntiForgeryToken]
public ActionResult EditPost(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    var courseToUpdate = db.Courses.Find(id);
    if (TryUpdateModel(courseToUpdate, "",
       new string[] { "Title", "Credits", "DepartmentID" }))
    {
        try
        {
            db.SaveChanges();

            return RedirectToAction("Index");
        }
        catch (RetryLimitExceededException /* dex */)
        {
            //Log the error (uncomment dex variable name 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(courseToUpdate.DepartmentID);
    return View(courseToUpdate);
}

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

ファイルの先頭に次の using ステートメントを追加します。

using System.Data.Entity.Infrastructure;

PopulateDepartmentsDropDownList メソッドは名前で並べ替えたすべての部門のリストを取得し、ドロップダウン リスト用に SelectList コレクションを作成し、そのコレクションを ViewBag プロパティでビューに渡します。 このメソッドは、ドロップダウン リストがレンダリングされるときに選択される項目を指定するためのコード呼び出しを許可する、省略可能な selectedDepartment パラメーターを受け取ります。 ビューでは名前 DepartmentIDDropDownList ヘルパーに渡します。これで、ヘルパーは ViewBag オブジェクト中から DepartmentID という名前の SelectList を検索することを認識します。

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

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

HttpGet Editメソッドは、編集中のコースに既に割り当てられている部門の 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);
}

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

catch (RetryLimitExceededException /* dex */)
{
    //Log the error (uncomment dex variable name 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);

このコードは、エラー メッセージを表示するためにページを再表示するときに、選択されていた部門が選択されたままになることを保証します。

Course ビューは既に部門フィールドのドロップダウン リストでスキャフォールディングされていますが、このフィールドの DepartmentID キャプションは必要ないため、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")
}

Views\Course\Edit.cshtml で同じ変更を行います。

キー値がデータベースによって生成され、変更できず、ユーザーに表示される意味のある値ではないため、通常、スキャフォールディングでは主キーはスキャフォールディングされません。 Course エンティティの場合、DatabaseGeneratedOption.None 属性はユーザーが主キー値を入力できることを意味することを理解しているため、スキャフォールディングには CourseID フィールドのテキスト ボックスが含まれています。 しかし、数値が他のビューで表示する必要がある意味のあるものであることを理解していないため、手動で追加する必要があります。

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

<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 ヘルパーを追加しても、ユーザーが [Edit] ページで [保存] をクリックしたときに、ポストされたデータにコース番号が含まれないため、隠しフィールドの必要性はなくなりません。

Views\Course\Delete.cshtmlViews\Course\Details.cshtml で、部署名キャプションを "Name" から "Department" に変更し、Title フィールドの前にコース番号フィールドを追加します。

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

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

Value 設定
番号 1000」と入力します。
Title [代数] を入力します。
クレジット 4」と入力します。
部署 [数学] を選択します。

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

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

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

Instructors ページにオフィスを追加する

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

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

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

{
    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, ActionName("Edit")]
[ValidateAntiForgeryToken]
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.SaveChanges();

          return RedirectToAction("Index");
       }
       catch (RetryLimitExceededException /* dex */)
      {
         //Log the error (uncomment dex variable name 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.");
      }
   }
   return View(instructorToUpdate);
}

RetryLimitExceededException への参照には using ステートメントが必要です。これを追加するには、RetryLimitExceededException にマウスポインターを合わせます。  再試行例外メッセージ というメッセージが表示されます。

[考えられる修正内容の表示][System.Data.Entity.Infrastructure の使用] を選択します。

再試行の例外を解決する

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

  • 署名が HttpGet メソッドと同じになっているため、メソッド名を EditPost に変更します (ActionName 属性は /Edit/ URL が引き続き使用されることを指定します)。

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

  • モデル バインダーからの値を使用して、取得した Instructor エンティティを更新します。 TryUpdateModel オーバーロードを使用すると、含めるプロパティを一覧表示できます。 これにより、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="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>

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

Instructors ページにコースを追加する

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

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で、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();
    if (instructor == null)
    {
        return HttpNotFound();
    }
    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](保存) をクリックしたときに実行されるコードを追加します。 EditPost メソッドを次のコードで置き換え、Instructor エンティティの Courses ナビゲーション プロパティを更新する新しいメソッドを追加します。 変更が強調表示されます。

[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.SaveChanges();

            return RedirectToAction("Index");
        }
        catch (RetryLimitExceededException /* dex */)
        {
            //Log the error (uncomment dex variable name 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);
         }
      }
   }
}

メソッドシグネチャは HttpGet Edit メソッドと異なっているので、メソッド名は EditPost から Editに変更されます。

ビューには Course エンティティのコレクションがないため、モデル バインダーは Courses ナビゲーション プロパティを自動的に更新できません。 モデル バインダーを使用する代わりに、新しい UpdateInstructorCourses メソッドで Courses ナビゲーション プロパティを更新します。 そのため、モデル バインドから 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 で、次のコードを フィールドの 要素の直後、divSaveOfficeAssignment ボタンの 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> の行は、示されているようにそれぞれ 1 行にする必要があります。そうしないと、ランタイム エラーが発生します。

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

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

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

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

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

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

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

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

注: インストラクター コース データを編集するためにここで採用されている方法は、コースの数が限られている場合にはうまく機能します。 非常に大きいコレクションの場合、別の 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();

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

このコードにより、次の変更が行われます。

  • インストラクターが任意の部門の管理者として割り当てられている場合、インストラクターの割り当てをその部門から削除します。 このコードがないと、部門の管理者として割り当てられたインストラクターを削除しようとすると、参照整合性エラーが発生します。

このコードでは、複数の部門の管理者として割り当てられた 1 人のインストラクターのシナリオは処理されません。 最後のチュートリアルでは、そのシナリオが発生しないようにするコードを追加します。

オフィスの場所とコースを Create ページに追加する

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 メソッドは、コースが選択されている可能性があるためではなく、ビュー内のforeach ループに空のコレクションを提供するために、PopulateAssignedCourseData メソッドを呼び出します (それ以外の場合、ビュー コードは null 参照例外をスローします)。

HttpPost Create メソッドは、検証エラーをチェックし、データベースに新しいインストラクターを追加するテンプレート コードの前に、選択した各コースを Courses ナビゲーション プロパティに追加します。 モデル エラーが発生した (たとえばユーザーが無効な日付をキー指定した) 場合に、エラー メッセージとともにページが再表示され、行ったコースの選択がすべて自動的に復元されるように、コースはモデル エラーが発生しても追加されます。

コースを Courses ナビゲーション プロパティに追加できるようにするには、プロパティを空のコレクションとして初期化する必要があることに注意してください。

instructor.Courses = new List<Course>();

コントローラー コードでこれを行うための別の方法として、Instructor モデルでこれを行うことができます。このためには、プロパティ ゲッターを変更して、コレクションが存在しない場合に自動的に作成するようにします。次の例に示します。

private ICollection<Course> _courses;
public virtual ICollection<Course> Courses 
{ 
    get
    {
        return _courses ?? (_courses = new List<Course>());
    }
    set
    {
        _courses = value;
    } 
}

Courses プロパティをこの方法で変更する場合、コントローラー内の明示的なプロパティの初期化コードを削除することができます。

Views\Instructor\Create.cshtml で、オフィスの場所のテキスト ボックスとチェック ボックスを採用日フィールドの後、[Submit]\(送信\) ボタンの前に追加します。

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

コードを貼り付けた後、[編集] ページで前に行ったように改行とインデントを修正します。

[作成] ページを実行し、インストラクターを追加します。

トランザクションの処理

基本的な CRUD 機能のチュートリアルで説明したように既定では、Entity Framework はトランザクションを暗黙的に実装します。 たとえば、Entity Framework の外部で行われる操作をトランザクションに含めたい場合など、より詳細な制御が必要なシナリオについては、MSDN の「トランザクションの操作」をご覧ください。

コードを取得する

完成したプロジェクトをダウンロードする

その他のリソース

他の Entity Framework リソースへのリンクは、「ASP.NET データ アクセス - 推奨リソース」にあります。

次のステップ

このチュートリアルでは、次の作業を行いました。

  • Courses ページをカスタマイズした
  • Instructors ページにオフィスを追加済み
  • Instructors ページにコースを追加済み
  • 更新された DeleteConfirmed
  • オフィスの場所とコースを Create ページに追加済み

次の記事に進み、非同期プログラミング モデルを実装する方法について説明します。