チュートリアル: 関連データを更新する - ASP.NET MVC と EF Core
前のチュートリアルでは、関連データを表示しました。このチュートリアルでは、外部キー フィールドとナビゲーション プロパティを更新することで関連データを更新します。
以下の図は、使用するページの一部を示しています。
このチュートリアルでは、次の作業を行いました。
- Courses ページをカスタマイズする
- Instructors/Edit ページを追加する
- Edit ページにコースを追加する
- Delete ページを更新する
- オフィスの場所とコースを Create ページに追加する
必須コンポーネント
Courses ページをカスタマイズする
新しい Course
エンティティが作成されると、既存の部門とのリレーションシップが必要になります。 これを容易にするため、スキャフォールディング コードには、コントローラーのメソッドと、部門を選択するためのドロップダウン リストを含む Create ビューと Edit ビューが含まれます。 ドロップダウン リストは、Course.DepartmentID
外部キー プロパティを設定します。これは、Department
ナビゲーション プロパティを適切な Department
エンティティとともに読み込むためにすべての Entity Framework で必要です。 このスキャフォールディング コードを使用しますが、エラー処理を追加し、ドロップダウン リストを並べ替えるために少し変更します。
CoursesController.cs
で、4 つの 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>
タグ ヘルパーに渡すと、ヘルパーは ViewBag
オブジェクト内で "DepartmentID" という名前の SelectList
を探すようになります。
新しいコースには部門がまだ確立されていないため、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);
}
Create
と Edit
の両方の HttpPost メソッドには、エラーの発生後にページを再表示するときに選択した項目を設定するコードも含まれています。 これにより、エラー メッセージを表示するためにページを再表示するときに、選択されていた部門が選択されたままになることを保証します。
.AsNoTracking を Details メソッドと Delete メソッドに追加する
Course の Details ページと Delete ページのパフォーマンスを最適化するため、AsNoTracking
呼び出しを Details
メソッドと HttpGet Delete
メソッドに追加します。
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);
}
Course ビューを変更する
Views/Courses/Create.cshtml
で、 [Department](部門) ドロップダウン リストに [Select Department](部門を選択) オプションを追加し、キャプションを [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
で行ったのと同じ変更を [Department](部門) フィールドに加えます。
また、Views/Courses/Edit.cshtml
で、 [Title](タイトル) フィールドの前にコース番号フィールドを追加します。 コース番号は主キーであるため表示されますが、変更することはできません。
<div class="form-group">
<label asp-for="CourseID" class="control-label"></label>
<div>@Html.DisplayFor(model => model.CourseID)</div>
</div>
Edit ビューには、コース番号の隠しフィールド (<input type="hidden">
) が既にあります。 <label>
タグ ヘルパーを追加しても、ユーザーが [Edit] ページで [保存] をクリックしたときに、ポストされたデータにコース番号が含まれないため、隠しフィールドの必要性はなくなりません。
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 ページをテストする
アプリを実行して、 [Courses] タブを選択し、 [新規作成] をクリックして新しいコースのデータを入力します。
Create をクリックしてください。 Courses/Index ページには、リストに追加された新しいコースが表示されます。 Index ページのリストの部門名は、ナビゲーション プロパティから取得され、リレーションシップが正常に確立されていることを示しています。
Courses/Index ページのコースで [Edit] をクリックします。
ページ上のデータを変更し、 [Save](保存) をクリックします。 Courses/Index ページには、更新されたコース データが表示されます。
Instructors/Edit ページを追加する
インストラクター レコードを編集するときに、インストラクターのオフィスの割り当ての更新が必要な場合があります。 Instructor
エンティティには、OfficeAssignment
エンティティとの一対ゼロまたは一対一のリレーションシップがあります。これは、コードで次の状況を処理する必要があることを意味します。
ユーザーが元は値のあったオフィスの割り当てをクリアする場合は、
OfficeAssignment
エンティティを削除する。ユーザーが元は空白だったオフィスの割り当ての値を入力する場合は、新しい
OfficeAssignment
エンティティを作成する。ユーザーがオフィスの割り当ての値を変更する場合は、既存の
OfficeAssignment
エンティティの値を変更する。
Instructors コントローラーを更新する
InstructorsController.cs
で、HttpGet Edit
メソッド内のコードを変更し、Instructor エンティティの OfficeAssignment
ナビゲーション プロパティを読み込んで AsNoTracking
を呼び出すようにします。
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
エンティティをデータベースから取得します。 これは、HttpGetEdit
メソッドで行ったのと同じです。モデル バインダーからの値を使用して、取得した
Instructor
エンティティを更新します。TryUpdateModel
オーバーロードでは、含めたいプロパティを宣言できるようになります。 これにより、2 番目のチュートリアルで説明したように、過剰ポスティングを防止します。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; }
データベースへの変更を保存します。
Instructors/Edit ビューを更新する
Views/Instructors/Edit.cshtml
で、オフィスの場所を編集するための新しいフィールドを、 [Save](保存) ボタンの直前に追加します。
<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>
アプリを実行し、 [Instructors](インストラクター) タブを選択し、インストラクターで [Edit](編集) をクリックします。 [Office Location](オフィスの場所) を変更し、 [Save](保存) をクリックします。
Edit ページにコースを追加する
インストラクターは、任意の数のコースを担当する場合があります。 次のスクリーン ショットに示すように、チェック ボックスのグループを使用して、コースの割り当てを変更する機能を追加して、Instructor/Edit ページを拡張します。
Course
と Instructor
のエンティティ間のリレーションシップは、多対多です。 リレーションシップの追加と削除を行うには、CourseAssignments
結合エンティティ セットに対してエンティティを追加および削除します。
インストラクターに割り当てられるコースを変更できるようにする UI は、チェック ボックスのグループです。 データベース内のすべてのコースのチェック ボックスが表示され、インストラクターに現在割り当てられているコースが選択されます。 ユーザーはチェック ボックスをオンまたはオフにしてコースの割り当てを変更できます。 コースの数が非常に多い場合は、ビューにデータを表示する別のメソッドを使用したいと思うかもしれませんが、結合エンティティを操作してリレーションシップを作成または削除するのと同じメソッドを使用します。
Instructors コントローラーを更新する
チェック ボックスのリストのためにデータをビューに提供するには、ビュー モデル クラスを使用します。
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.cs
で、HttpGet 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
でビューに渡されます。
次に、ユーザーが [Save](保存) をクリックしたときに実行されるコードを追加します。 EditPost
メソッドを次のコードで置き換え、Instructor エンティティの 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
に戻します。
ビューには Course エンティティのコレクションがないため、モデル バインダーは CourseAssignments
ナビゲーション プロパティを自動的に更新できません。 モデル バインダーを使用する代わりに、新しい UpdateInstructorCourses
メソッドで CourseAssignments
ナビゲーション プロパティを更新します。 そのため、モデル バインドから CourseAssignments
プロパティを除外する必要があります。 これを行うために、TryUpdateModel
を呼び出すコードを変更する必要はありません。これは、明示的な承認を必要とするオーバーロードを使用していて、CourseAssignments
がインクルード リスト内にないためです。
チェック ボックスが選択されていない場合、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);
}
}
}
}
その後コードは、データベース内のすべてのコースをループ処理し、各コースを現在インストラクターに割り当てられているコースとビューで選択されているコースを比較してチェックします。 検索を効率化するため、最後の 2 つのコレクションが 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);
}
}
}
}
Instructor ビューを更新する
Views/Instructors/Edit.cshtml
で、次のコードを Office フィールドの div
要素のすぐ後ろ、かつ [保存] ボタンの div
要素の前に追加することで、チェックボックスの配列を含む Courses フィールドを追加します。
Note
Visual Studio にコードを貼り付けると、改行がコードを分割するように変更される場合があります。 貼り付けた後でコードが変化している場合は、Ctrl + Z キーを 1 回押して、自動書式設定を元に戻してください。 これにより、改行がここに示されているように修正されます。 インデントは完璧である必要はありませんが、@:</tr><tr>
、@:<td>
、@:</td>
、および @:</tr>
の行は、示されているようにそれぞれ 1 行にする必要があります。そうしないと、ランタイム エラーが発生します。 新しいコードのブロックを選択して、Tab キーを 3 回押して、新しいコードと既存のコードを並べます。 この問題は、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>
このコードは、3 つの列を含む HTML テーブルを作成します。 各列には、チェック ボックスとその後に続くキャプションがあります。キャプションは、コース番号とタイトルから構成されます。 チェック ボックスはすべて同じ名前 ("selectedCourses") を持ち、これらをグループとして扱うようにモデル バインダーに通知します。 各チェック ボックスの value 属性は CourseID
の値に設定されます。 ページがポストされると、モデル バインダーは、選択されたチェック ボックスの CourseID
値のみで構成される配列をコントローラーに渡します。
チェック ボックスが最初にレンダリングされるときに、インストラクターに割り当てられるコースのチェック ボックスが checked 属性を持ち、選択されます (チェック ボックスがオンになった状態で表示されます)。
アプリを実行し、 [Instructors](インストラクター) タブを選択し、インストラクターで [Edit](編集) をクリックして Edit ページを表示します。
一部のコース割り当てを変更し、[Save](保存) をクリックします。 行った変更が Index ページに反映されます。
Note
インストラクター コース データを編集するためにここで採用されている方法は、コースの数が限られている場合にはうまく機能します。 非常に大きいコレクションの場合、別の UI と別の更新方法が必要になる場合があります。
Delete ページを更新する
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
エンティティが認識されず、削除されません。 ここでこれらを読み取らなくても済むようにするには、データベースで連鎖削除を構成します。削除されるインストラクターが任意の部門の管理者として割り当てられている場合、インストラクターの割り当てをその部門から削除します。
オフィスの場所とコースを Create ページに追加する
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
メソッドでご覧になったものと似ていますが、最初にコースが選択されていない点が異なります。 HttpGet Create
メソッドは、PopulateAssignedCourseData
メソッドを呼び出します。これはコースが選択されている可能性があるからではなく、ビュー内の foreach
ループに空のコレクションを提供するためです (そうしないと、コードの表示で null 参照例外がスローされる場合があります)。
HttpPost Create
メソッドは、選択した各コースを CourseAssignments
ナビゲーション プロパティに追加してから、検証エラーをチェックし、データベースに新しいインストラクターを追加します。 モデル エラーが発生した (たとえばユーザーが無効な日付をキー指定した) 場合に、エラー メッセージとともにページが再表示され、行ったコースの選択がすべて自動的に復元されるように、コースはモデル エラーが発生しても追加されます。
コースを CourseAssignments
ナビゲーション プロパティに追加できるようにするには、プロパティを空のコレクションとして初期化する必要があることに注意してください。
instructor.CourseAssignments = new List<CourseAssignment>();
コントローラー コードでこれを行うための別の方法として、Instructor
モデルでこれを行うことができます。このためには、プロパティ ゲッターを変更して、コレクションが存在しない場合に自動的に作成するようにします。次の例に示します。
private ICollection<CourseAssignment> _courseAssignments;
public ICollection<CourseAssignment> CourseAssignments
{
get
{
return _courseAssignments ?? (_courseAssignments = new List<CourseAssignment>());
}
set
{
_courseAssignments = value;
}
}
CourseAssignments
プロパティをこの方法で変更する場合、コントローラー内の明示的なプロパティの初期化コードを削除することができます。
Views/Instructor/Create.cshtml
で、オフィスの場所のテキスト ボックスとチェック ボックスを [Submit](送信) ボタンの前のコースに追加します。 Edit ページの場合と同様に、コードを貼り付けたときに 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 の外部で行われる操作をトランザクションに含めたい場合など、より詳細な制御が必要なシナリオについては、「Using Transactions」(トランザクションの使用) をご覧ください。
コードを取得する
次の手順
このチュートリアルでは、次の作業を行いました。
- Courses ページをカスタマイズした
- Instructors/Edit ページを追加した
- Edit ページにコースを追加した
- Delete ページを更新した
- オフィスの場所とコースを Create ページに追加した
コンカレンシーの競合を処理する方法について学習するには、次のチュートリアルに進んでください。
ASP.NET Core