Phần 7, Razor Pages với EF Core trong ASP.NET Core - Cập nhật dữ liệu liên quan
Tổng quan
Hướng dẫn này trình bày cách cập nhật dữ liệu liên quan (related data) trong ứng dụng web Razor Pages sử dụng Entity Framework (EF) Core. Bao gồm cập nhật quan hệ một-nhiều (courses và departments) và quan hệ nhiều-nhiều (instructors và courses).
Cập nhật trang Create và Edit cho Course
Tạo lớp cơ sở cho Course Create và Edit
Tạo file Pages/Courses/DepartmentNamePageModel.cs với code sau:
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace ContosoUniversity.Pages.Courses
{
public class DepartmentNamePageModel : PageModel
{
public SelectList DepartmentNameSL { get; set; }
public void PopulateDepartmentsDropDownList(SchoolContext _context,
object selectedDepartment = null)
{
var departmentsQuery = from d in _context.Departments
orderby d.Name // Sắp xếp theo tên.
select d;
DepartmentNameSL = new SelectList(departmentsQuery.AsNoTracking(),
nameof(Department.DepartmentID),
nameof(Department.Name),
selectedDepartment);
}
}
}Phương thức PopulateDepartmentsDropDownList tạo một SelectList chứa tên các khoa (department). Khi sử dụng Select Tag Helper với asp-for, mục được chọn được xác định bởi giá trị của thuộc tính model (ví dụ Course.DepartmentID), không phải tham số selectedDepartment.
Cập nhật Page Model cho Course Create
Cập nhật Pages/Courses/Create.cshtml.cs:
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Courses
{
public class CreateModel : DepartmentNamePageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public CreateModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
public IActionResult OnGet()
{
PopulateDepartmentsDropDownList(_context);
return Page();
}
[BindProperty]
public Course Course { get; set; }
public async Task<IActionResult> OnPostAsync()
{
var emptyCourse = new Course();
if (await TryUpdateModelAsync<Course>(
emptyCourse,
"course", // Tiền tố cho giá trị form.
s => s.CourseID, s => s.DepartmentID, s => s.Title, s => s.Credits))
{
_context.Courses.Add(emptyCourse);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
// Nạp lại dropdown departments. emptyCourse.DepartmentID xác định mục được chọn.
PopulateDepartmentsDropDownList(_context, emptyCourse.DepartmentID);
return Page();
}
}
}Các điểm chính:
- Kế thừa từ
DepartmentNamePageModel - Dùng
TryUpdateModelAsyncđể ngăn overposting (gửi dữ liệu thừa) - Xóa
ViewData["DepartmentID"]để dùng model strongly typed thay thế
Cập nhật Razor Page Course Create
Cập nhật Pages/Courses/Create.cshtml:
@page
@model ContosoUniversity.Pages.Courses.CreateModel
@{
ViewData["Title"] = "Create Course";
}
<h2>Create</h2>
<h4>Course</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Course.CourseID" class="control-label"></label>
<input asp-for="Course.CourseID" class="form-control" />
<span asp-validation-for="Course.CourseID" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Course.Title" class="control-label"></label>
<input asp-for="Course.Title" class="form-control" />
<span asp-validation-for="Course.Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Course.Credits" class="control-label"></label>
<input asp-for="Course.Credits" class="form-control" />
<span asp-validation-for="Course.Credits" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Course.Department" class="control-label"></label>
<select asp-for="Course.DepartmentID" class="form-control"
asp-items="@Model.DepartmentNameSL">
<option value="">-- Select Department --</option>
</select>
<span asp-validation-for="Course.DepartmentID" class="text-danger" />
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}Thay đổi:
- Nhãn đổi từ "DepartmentID" thành "Department"
- Dùng
DepartmentNameSLtừ lớp cơ sở thay vìViewBag - Thêm tùy chọn "Select Department"
- Thêm thông báo validation cho lựa chọn department
Cập nhật Page Model cho Course Edit
Cập nhật Pages/Courses/Edit.cshtml.cs:
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Courses
{
public class EditModel : DepartmentNamePageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public EditModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Course Course { get; set; }
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Course = await _context.Courses
.Include(c => c.Department).FirstOrDefaultAsync(m => m.CourseID == id);
if (Course == null)
{
return NotFound();
}
// Nạp dropdown departments. Course.DepartmentID xác định mục được chọn.
PopulateDepartmentsDropDownList(_context, Course.DepartmentID);
return Page();
}
public async Task<IActionResult> OnPostAsync(int? id)
{
if (id == null)
{
return NotFound();
}
var courseToUpdate = await _context.Courses.FindAsync(id);
if (courseToUpdate == null)
{
return NotFound();
}
if (await TryUpdateModelAsync<Course>(
courseToUpdate,
"course", // Tiền tố cho giá trị form.
c => c.Credits, c => c.DepartmentID, c => c.Title))
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
// Nạp lại dropdown. courseToUpdate.DepartmentID xác định mục được chọn.
PopulateDepartmentsDropDownList(_context, courseToUpdate.DepartmentID);
return Page();
}
}
}Cập nhật Razor Page Course Edit
Cập nhật Pages/Courses/Edit.cshtml:
@page
@model ContosoUniversity.Pages.Courses.EditModel
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Course</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Course.CourseID" />
<div class="form-group">
<label asp-for="Course.CourseID" class="control-label"></label>
<div>@Html.DisplayFor(model => model.Course.CourseID)</div>
</div>
<div class="form-group">
<label asp-for="Course.Title" class="control-label"></label>
<input asp-for="Course.Title" class="form-control" />
<span asp-validation-for="Course.Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Course.Credits" class="control-label"></label>
<input asp-for="Course.Credits" class="form-control" />
<span asp-validation-for="Course.Credits" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Course.Department" class="control-label"></label>
<select asp-for="Course.DepartmentID" class="form-control"
asp-items="@Model.DepartmentNameSL"></select>
<span asp-validation-for="Course.DepartmentID" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}Cập nhật trang Details và Delete cho Course
Dùng AsNoTracking() để cải thiện hiệu năng khi không cần tracking.
Cập nhật Pages/Courses/Delete.cshtml.cs:
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Course = await _context.Courses
.AsNoTracking()
.Include(c => c.Department)
.FirstOrDefaultAsync(m => m.CourseID == id);
if (Course == null)
{
return NotFound();
}
return Page();
}Cập nhật Pages/Courses/Delete.cshtml:
@page
@model ContosoUniversity.Pages.Courses.DeleteModel
@{
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.Course.CourseID)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Course.CourseID)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Course.Title)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Course.Title)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Course.Credits)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Course.Credits)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Course.Department)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Course.Department.Name)
</dd>
</dl>
<form method="post">
<input type="hidden" asp-for="Course.CourseID" />
<input type="submit" value="Delete" class="btn btn-danger" /> |
<a asp-page="./Index">Back to List</a>
</form>
</div>Cập nhật trang Create và Edit cho Instructor
Tạo lớp dữ liệu cho Assigned Courses
Tạo Models/SchoolViewModels/AssignedCourseData.cs:
namespace ContosoUniversity.Models.SchoolViewModels
{
public class AssignedCourseData
{
public int CourseID { get; set; }
public string Title { get; set; }
public bool Assigned { get; set; }
}
}Tạo lớp cơ sở Page Model cho Instructor
Tạo Pages/Instructors/InstructorCoursesPageModel.cs:
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Collections.Generic;
using System.Linq;
namespace ContosoUniversity.Pages.Instructors
{
public class InstructorCoursesPageModel : PageModel
{
public List<AssignedCourseData> AssignedCourseDataList;
public void PopulateAssignedCourseData(SchoolContext context,
Instructor instructor)
{
var allCourses = context.Courses;
var instructorCourses = new HashSet<int>(
instructor.Courses.Select(c => c.CourseID));
AssignedCourseDataList = new List<AssignedCourseData>();
foreach (var course in allCourses)
{
AssignedCourseDataList.Add(new AssignedCourseData
{
CourseID = course.CourseID,
Title = course.Title,
Assigned = instructorCourses.Contains(course.CourseID)
});
}
}
}
}Cập nhật Page Model cho Instructor Edit
Cập nhật Pages/Instructors/Edit.cshtml.cs:
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Instructors
{
public class EditModel : InstructorCoursesPageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public EditModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Instructor Instructor { get; set; }
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Instructor = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);
if (Instructor == null)
{
return NotFound();
}
PopulateAssignedCourseData(_context, Instructor);
return Page();
}
public async Task<IActionResult> OnPostAsync(int? id, string[] selectedCourses)
{
if (id == null)
{
return NotFound();
}
var instructorToUpdate = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.FirstOrDefaultAsync(s => s.ID == id);
if (instructorToUpdate == null)
{
return NotFound();
}
if (await TryUpdateModelAsync<Instructor>(
instructorToUpdate,
"Instructor",
i => i.FirstMidName, i => i.LastName,
i => i.HireDate, i => i.OfficeAssignment))
{
if (String.IsNullOrWhiteSpace(
instructorToUpdate.OfficeAssignment?.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
UpdateInstructorCourses(selectedCourses, instructorToUpdate);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
UpdateInstructorCourses(selectedCourses, instructorToUpdate);
PopulateAssignedCourseData(_context, instructorToUpdate);
return Page();
}
public 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 _context.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Add(course);
}
}
else
{
if (instructorCourses.Contains(course.CourseID))
{
var courseToRemove = instructorToUpdate.Courses.Single(
c => c.CourseID == course.CourseID);
instructorToUpdate.Courses.Remove(courseToRemove);
}
}
}
}
}
}Các điểm chính:
- Dùng eager loading cho navigation properties
OfficeAssignmentvàCourses TryUpdateModelAsyncngăn overposting- Nếu địa điểm văn phòng trống, đặt
Instructor.OfficeAssignmentthành null - Phương thức
UpdateInstructorCoursesáp dụng các lựa chọn checkbox vào danh sách courses
Cập nhật Razor Page Instructor Edit
Cập nhật Pages/Instructors/Edit.cshtml:
@page
@model ContosoUniversity.Pages.Instructors.EditModel
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Instructor</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Instructor.ID" />
<div class="form-group">
<label asp-for="Instructor.LastName" class="control-label"></label>
<input asp-for="Instructor.LastName" class="form-control" />
<span asp-validation-for="Instructor.LastName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.FirstMidName" class="control-label"></label>
<input asp-for="Instructor.FirstMidName" class="form-control" />
<span asp-validation-for="Instructor.FirstMidName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.HireDate" class="control-label"></label>
<input asp-for="Instructor.HireDate" class="form-control" />
<span asp-validation-for="Instructor.HireDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.OfficeAssignment.Location" class="control-label"></label>
<input asp-for="Instructor.OfficeAssignment.Location" class="form-control" />
<span asp-validation-for="Instructor.OfficeAssignment.Location" class="text-danger" />
</div>
<div class="form-group">
<div class="table">
<table>
<tr>
@{
int cnt = 0;
foreach (var course in Model.AssignedCourseDataList)
{
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>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}Các checkbox:
- Tất cả có cùng name ("selectedCourses") để model binder xử lý như một nhóm
- Thuộc tính value được đặt thành
CourseID - Được chọn nếu
course.Assignedlà true - Model binder truyền mảng chỉ chứa các giá trị được chọn
Cập nhật Page Model cho Instructor Create
Cập nhật Pages/Instructors/Create.cshtml.cs:
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Instructors
{
public class CreateModel : InstructorCoursesPageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
private readonly ILogger<InstructorCoursesPageModel> _logger;
public CreateModel(SchoolContext context,
ILogger<InstructorCoursesPageModel> logger)
{
_context = context;
_logger = logger;
}
public IActionResult OnGet()
{
var instructor = new Instructor();
instructor.Courses = new List<Course>();
// Cung cấp collection rỗng cho vòng lặp foreach
// trong trang Create Razor.
PopulateAssignedCourseData(_context, instructor);
return Page();
}
[BindProperty]
public Instructor Instructor { get; set; }
public async Task<IActionResult> OnPostAsync(string[] selectedCourses)
{
var newInstructor = new Instructor();
if (selectedCourses.Length > 0)
{
newInstructor.Courses = new List<Course>();
// Nạp collection với một lần gọi DB.
_context.Courses.Load();
}
// Thêm các Courses đã chọn vào instructor mới.
foreach (var course in selectedCourses)
{
var foundCourse = await _context.Courses.FindAsync(int.Parse(course));
if (foundCourse != null)
{
newInstructor.Courses.Add(foundCourse);
}
else
{
_logger.LogWarning("Course {course} not found", course);
}
}
try
{
if (await TryUpdateModelAsync<Instructor>(
newInstructor,
"Instructor",
i => i.FirstMidName, i => i.LastName,
i => i.HireDate, i => i.OfficeAssignment))
{
_context.Instructors.Add(newInstructor);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return RedirectToPage("./Index");
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
PopulateAssignedCourseData(_context, newInstructor);
return Page();
}
}
}Cải tiến chính:
- Thêm logging cho các thông báo cảnh báo và lỗi
- Dùng
Load()để lấy tất cả courses trong một lần gọi database - Dùng
FindAsync()để lấy các entity đang được tracked
Cập nhật trang Delete cho Instructor
Cập nhật Pages/Instructors/Delete.cshtml.cs:
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Instructors
{
public class DeleteModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public DeleteModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Instructor Instructor { get; set; }
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Instructor = await _context.Instructors.FirstOrDefaultAsync(m => m.ID == id);
if (Instructor == null)
{
return NotFound();
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Instructor instructor = await _context.Instructors
.Include(i => i.Courses)
.SingleAsync(i => i.ID == id);
if (instructor == null)
{
return RedirectToPage("./Index");
}
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 RedirectToPage("./Index");
}
}
}Các điểm chính:
- Dùng eager loading cho navigation property
Courses - Xóa phân công instructor khỏi các department mà họ đang là quản trị viên
- Dùng cấu hình cascade delete cho courses
Tóm tắt
Hướng dẫn này trình bày:
- Tạo lớp cơ sở để chia sẻ code chung giữa các trang Create và Edit
- Dùng
SelectListcho dropdown list với dữ liệu liên quan - Xử lý quan hệ một-nhiều (Courses và Departments)
- Xử lý quan hệ nhiều-nhiều (Instructors và Courses) với checkbox
- Dùng
TryUpdateModelAsyncđể ngăn overposting - Quản lý quan hệ một-hoặc-không (Office Assignments)
- Dùng
AsNoTracking()cho các thao tác chỉ đọc - Eager loading đúng cách với các câu lệnh
Include()