Trong bài viết trước, bạn đã tạo một ứng dụng MVC cho phép lưu trữ và hiển thị dữ liệu dùng Entity Framework và SQL Server Compact. Trong phần này bạn sẽ xem lại và tùy biến các câu lệnh cho phép xem, thêm, xóa, sửa dữ liệu mà trình hỗ trợ của MVC đã tự động tạo cho bạn trong các view và controller.
Ghi chú: Trong thực tế, người ta thường dùng mẫu thiết kế Repository để tạo lớp trừu tượng giữa controller và DAL. Để giữ cho bài viết được đơn giản, bạn sẽ không xây dựng một repository cho tới các bài viết sau trong cùng loạt bài này.
(CRUD: Create, Read, Update, Delete)
Trong bài này, bạn sẽ tạo các trang web sau:
Tạo một trang hiển thị thông tin chi tiết
Vì Enrollments là một tập hợp nên mã tự động sinh ra cho trang Index bỏ qua thuộc tính này, tuy nhiên trong trang Details bạn sẽ hiển thị nội dung của nó dưới dạng một bảng HTML.
Trong Controllers\StudentController.cs, action method của trang Details sẽ giống như ví dụ sau:
public ViewResult Details(int id) { Student student = db.Students.Find(id); return View(student); }
Đoạn lệnh trên dùng phương thức Find để lấy về một thực thể Student tương ứng với khóa bạn truyền vào thông qua thuộc tính id. Giá trị của id đến từ tham số trên liên kết đến trang Details trên trang Index.
Mở Views\Student\Details.cshtml. Các trường được hiển thị nhờ phương thức DisplayFor, giống như ví dụ dưới đây:
<div class="display-label">LastName</div> <div class="display-field"> @Html.DisplayFor(model => model.LastName) </div>
Để hiển thị danh sách các Enrollment, thêm đoạn lệnh sau vào phía sau trường EnrollmentDate, ngay phía trước thẻ đóng fieldset:
<div class="display-label"> @Html.LabelFor(model => model.Enrollments) </div> <div class="display-field"> <table> <tr> <th>Course Title</th> <th>Grade</th> </tr> @foreach (var item in Model.Enrollments) { <tr> <td> @Html.DisplayFor(modelItem => item.Course.Title) </td> <td> @Html.DisplayFor(modelItem => item.Grade) </td> </tr> } </table> </div>
Đoạn lệnh này duyệt qua toàn bộ các thực thể có trong thuộc tính Enrollments. Với mỗi thực thể Enrollment, nó sẽ hiển thị tên khóa học và xếp loại. Tên khóa khọc được lấy về từ thực thể Course của Enrollments. Tất cả dữ liệu này sẽ được lấy về từ CSDL một cách tự động khi cần (nói cách khác, bạn đang dùng lazy loading, vì bạn không chỉ ra nó là eager loading nên khi lần đầu tiên truy cập vào thuộc tính này, một câu query sẽ được gửi đến CSDL để lấy dữ liệu. Bạn có thể đọc thêm về lazy loading và eager loading trong bài “Đọc các dữ liệu quan hệ” trong cùng loạt bài này).
Chạy trang trên bằng cách nhấn vào tab Students và nhấn lên Details. Bạn sẽ thấy một danh sách các khóa học:
Creating a Create Page Tạo một trang tạo mới
Trong Controllers\StudentController.cs, thay thế phương thức action HttpPost
Create
với đoạn lệnh sau để thêm một khối try-catch
vào trong phương thức:
[HttpPost] public ActionResult Create(Student student) { try { if (ModelState.IsValid) { db.Students.Add(student); db.SaveChanges(); return RedirectToAction("Index"); } } catch (DataException) { //Log the error (add a variable name after DataException) ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator."); } return View(student); }
Đoạn lệnh trên thêm một thực thể Student được tạo bởi ASP.NET MVC model binder vào tập Students và lưu vào CSDL. (Model binder chỉ đến một tính năng có trong ASP.NET MVC cho phép dễ dàng làm việc với dữ liệu được gửi lên từ một form; trong đó một model binder sẽ chuyển các giá trị gửi lên từ form thành kiểu có trong .NET Framework và đưa chúng vào các action method dưới dạng các tham số. Trong trường hợp này, model binder khởi tạo một thực thể Student cho bạn và dùng các giá trị từ tập FormCollection.
Đoạn try-catch là thứ duy nhất khác nhau giữa đoạn lệnh bạn thay thế và đoạn được tự động sinh ra bởi trình hỗ trợ. Nếu một exception thừa kế từ DataException xảy ra khi đang lưu lại thay đổi, một thông báo lỗi sẽ xuất hiện. Các dạng lỗi này xảy ra thường do một yếu tố từ bên ngoài hơn là lỗi lập trình, vậy nên người dùng được khuyên nên thử lại lần nữa. Đoạn lệnh trong Views\Student\Create.cshtml tương tự như cái bạn thấy trong Details.cshtml, ngoại từ EditorFor và ValidationMessageFor được dùng cho mỗi trường thay vì DisplayFor. Ví dụ sau cho thấy những thay đổi đó:
<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>
Không có thay đổi trong file Create.cshtml.
Nhấn lên tab Students và nhấn tiếp Create New.
Phần kiểm tra tính hợp lệ của dữ liệu mặc nhiên được thực hiện. Nhập tên và một ngày không hợp lệ, sau đó nhấn Create, bạn sẽ thấy hiện ra thông báo lỗi.
Trong trường hợp này bạn sẽ thấy thông báo được tạo bởi trình kiểm tra javascript chạy trên máy người dùng. Nhưng dữ liệu cũng được kiểm tra trên server. Do vậy ngay cả nếu phần kiểm tra bằng javascript không được thực hiện, dữ liệu lỗi cũng vẫn bị chặn lại và một exception sẽ xảy ra trên phía server.
Đổi ngày sang một giá trị hợp lệ như 9/1/2005 và nhấn Create để xem sinh viên mới xuất hiện trên trang Index.
Tạo một trang cập nhật
Trong Controllers\StudentController.cs, phương thức HttpGet
Edit
(cái không có thuộc tính HttpPost
) dùng phương thức Find
để lấy về thực thể Student
được chọn, như bạn đã thấy trong phương thức Details
. Bạn không cần thay đổi phương thức này.
Tuy nhiên, thay thế phương thức action HttpPost
Edit
với đoạn lệnh sau để thêm vào khối try-catch
:
[HttpPost] public ActionResult Edit(Student student) { try { if (ModelState.IsValid) { db.Entry(student).State = EntityState.Modified; db.SaveChanges(); return RedirectToAction("Index"); } } catch (DataException) { //Log the error (add a variable name after DataException) ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator."); } return View(student); }
Đoạn lệnh trên tương tự với cái bạn thấy trong phương thức HttpPost Create. Tuy nhiên, thay vì thêm một thực thể được tạo mởi model binder vào tập thực thể, đoạn lệnh trên đặt một cờ của thực thể để chỉ ra nó đã bị thay đổi. Khi phương thức SaveChanges được gọi, cờ Modified làm cho Entity Framework tạo ra các phát biểu để cập nhật dòng tương ứng trong CSDL. Tất cả các cột sẽ được cập nhật, bao gồm cả các cột người dùng không thay đổi, và bỏ qua các phép kiểm tra xem dữ liệu có bị thay đổi bởi người dùng khác hay không. (Bạn sẽ được học về xử lý thay đổi dữ liệu đồng thời trong bài Handling Concurrency trong cùng loạt bài này).
Trạng thái của thực thể và các phương thức Attach, Save Changes
Đối tượng database context quản lý các thực thể trong bộ nhớ để biết nó có được đồng bộ với dòng tương ứng trong CSDL hay không, và thông tin này được sử dụng để xác định các thao tác cần làm khi phương thức SaveChanges được gọi. Lấy ví dụ, khi bạn truyền một đối tượng vào cho phương thức Add, trạng thái của nó sẽ là Added. Sau đó nếu bạn gọi SaveChanges, câu lệnh INSERT sẽ được sử dụng.
Một thực thể có thể mang một trong những trạng thái sau:
Added
. Thực thể chưa tồn tại trong CSDL. Phương thức SaveChanges sẽ tạo ra phát biểu INSERT.Unchanged
. Không cần làm gì với thực thể mang trạng thái này. Khi đọc từ CSDL, thực thể sẽ mang trạng thái này.Modified
. Đã bị thay đổi, SaveChanges sẽ tạo ra câu lệnh UPDATE.Deleted
. Thực thể đã được đánh dấu xóa. SaveChanges sẽ tạo ra câu lệnh DELETE.Detached
. The entity isn’t being tracked by the database context. Thực thể hiện không bị quản lý bởi database context.
Trong một ứng dụng desktop, trạng thái của các đối tượng được đặt tự động. Trong kiểu ứng dụng này, bạn sẽ đọc một thực thể và tạo thay đổi đến một vài thuộc tính của nó. Điều này làm cho trạng thái của nó thay đổi thành Modified. Sau đó khi gọi SaveChanges, Entity Framework sẽ tạo nên câu lệnh UPDATE để cập nhật các thuộc tính mà bạn thay đổi.
Tuy nhiên trong một ứng dụng web, quá trình này bị ngắt quãng vì đối tượng database context bị giải phóng sau khi tạo xong trang web. Khi phương thức action được gọi, nó sẽ tạo ra một request mới và bạn có một instance mới của đối tượng database context, do vậy bạn phải đặt lại bằng tay trạng thái của thực thể thành Modified. Sau đó khi gọi SaveChanges, Entity Framework sẽ cập nhật lại tất cả các cột của dòng tương ứng trong CSDL, bởi vì đối tượng context không có cách nào để biết thuộc tính nào đã bị thay đổi.
Nếu bạn muốn câu lệnh SQL UPDATE chỉ cập nhật lại các trường đã bị thay đổi, bạn có thể lưu lại giá trị gốc bằng cách nào đó (như dùng hidden) và lấy các giá trị đó khi [HttpPost] Edit được gọi. Sau đó bạn tạo một thực thể Student mới với các giá trị gốc, rồi gọi phương thức Attach trên đối tượng đó, cập nhật các giá trị mới và gọi SaveChanges(). Để có thêm thông tin, bạn có thể xem các bài viết Add/Attach and Entity States và Local Data trên blog của nhóm Entity Framework.
Đoạn lệnh trong Views\Student\Edit.cshtml tương tự như cái bạn thấy trong Create.cshtml, và không có thay đổi gì khác.
Chạy trang và bằng cách chọn tab Students tab và click lên một liên kết Edit.
Thử thay đổi dữ liệu và nhấn Save. Bạn sẽ thấy dữ liệu đã thay đổi lại trên trang Index.
Tạo trang xóa dữ liệu
Trong Controllers\StudentController.cs, đoạn code sinh ra trong phương thức HttpGet
Delete
dùng Find
để lấy về thực thể Student
được chọn, như bạn thấy trong Details
và Edit
. Tuy nhiên, để tạo một thông báo lỗi khi lời gọi SaveChanges thất bại, bạn sẽ thêm một vài tính năng vào phương thức này và view tương ứng của nó.
Như bạn thấy trong các thao tác tạo và cập nhật, các phương thức xóa cũng yêu cầu hai phương thức action. Phương thức dùng để phản hồi lại yêu cầu GET hiển thị một view cho phép người dùng xác nhận lại thao tác xóa, nếu người dùng chấp nhận, một yêu cầu POST sẽ được tạo. Khi điều này xảy ra, phương thức HttpPost Delete được gọi và dữ liệu khi đó mới thực sự bị xóa.
Bạn sẽ thêm một khối try-catch vào phương thức HttpPost Delete để bắt các lỗi có thể xảy ra khi cập nhật CSDL. Nếu lỗi xảy ra, phương thức HttpPost Delete gọi HttpGet Delete, truyền tham số cho nó để chỉ ra rằng đã có lỗi xảy ra. Phương thức HttpGet Delete sau đó sẽ hiển thị lại trang xác nhận cùng với thông báo lỗi, cho phép người dùng hủy bỏ yêu cầu hay thử lại một lần nữa.
Thay thế phương thức HttpGet Delete với đoạn lệnh sau, cho phép quản lý việc hiển thị thông báo lỗi:
public ActionResult Delete(int id, bool? saveChangesError) { if (saveChangesError.GetValueOrDefault()) { ViewBag.ErrorMessage = "Unable to save changes. Try again, and if the problem persists see your system administrator."; } return View(db.Students.Find(id)); }
Đoạn lệnh trên nhận thêm vào một tham số tùy chọn kiểu boolean chỉ ra nó có phải được gọi sau một lệnh lưu dữ liệu thất bại. Tham số này sẽ là null (hoặc false) khi HttpGet Delete được gọi để phản hồi một yêu cầu bình thường. Khi nó được gọi bởi HttpPost Delete để phản hồi một lỗi cập nhật, tham số này sẽ là true và một thông báo lỗi sẽ được chuyển đến cho view tương ứng.
Thay thế phương thức HttpPost Delete (đổi tên thành DeleteConfirmed
) với đoạn lệnh sau, nó cho phép thực hiện thao tác xóa và bắt các lỗi cập nhật CSDL.
[HttpPost, ActionName("Delete")] public ActionResult DeleteConfirmed(int id) { try { Student student = db.Students.Find(id); db.Students.Remove(student); db.SaveChanges(); } catch (DataException) { //Log the error (add a variable name after DataException) return RedirectToAction("Delete", new System.Web.Routing.RouteValueDictionary { { "id", id }, { "saveChangesError", true } }); } return RedirectToAction("Index"); }
This code retrieves the selected entity, then calls the Remove
method to set the entity’s status to Deleted
. When SaveChanges
is called, a SQL DELETE
command is generated.
Đoạn lệnh trên lấy về thực thể được chọn, sau đó gọi phương thức Remove để đặt trạng thái của thực để về Deleted. Khi SaveChanges được gọi, một câu SQL DELETE sẽ được sinh ra và thực thi.
Nếu muốn nâng cao hiệu năng khi ứng dụng phải xử lý khối lượng lớn dữ liệu, bạn có thể tránh việc thực thi câu SQL để lấy về dòng dữ liệu cần xóa bằng cách thay thể các đoạn lệnh gọi Find và Remove bằng đoạn lệnh sau:
Student studentToDelete = new Student() { StudentID = id }; db.Entry(studentToDelete).State = EntityState.Deleted;
Đoạn lệnh trên khởi tạo một thực thể Student với duy nhất khóa chính và đặt trạng thái của nó về Deleted. Đó là tất cả những gì Entity Framework cần để xóa thực thể.
Như đã nói đến, phương thức HttpGet Delete không thực hiện việc xóa dữ liệu. Thực hiện một thao tác xóa trong một phản hồi đến một yêu cầu dạng GET (hay sửa, tạo hoặc bất cứ thao tác nào làm thay đổi dữ liệu) có thể mang những rủi ro về bảo mật. Để có thêm thông tin, xin hãy đọc bài ASP.NET MVC Tip #46 — Don’t use Delete Links because they create Security Holes trên blog của Stephen Walther.
Trong Views\Student\Delete.cshtml, thêm đoạn lệnh sau vào giữa h2 và h3:
<p class="error">@ViewBag.ErrorMessage</p>
Chạy trang bằng cách chọn tab Students và nhấn một liên kết Delete:
Nhấn Delete. Trang Index sẽ hiển thị lại mà không có sinh viên đã bị xóa (Bạn sẽ thấy một ví dụ về việc bắt lỗi trong bài Handling Concurrency cũng thuộc loạt bài này).
Đảm bảo rằng kết nối đến CSDL đã được đóng
Để đảm bảo các kết nối đến CSDL đã được đóng và tài nguyên do chúng chiếm giữ đã được giải phóng, bạn phải đảm bảo đối tượng context phải bị hủy. Đó là vì sao chúng ta sửa lại phương thức Dispose ở cuối lớp StudentController trong file StudentController.cs, như bạn thấy trong ví dụ dưới đây:
protected override void Dispose(bool disposing) { db.Dispose(); base.Dispose(disposing); }
Lớp Controller cơ sở đã implement IDisposable, do vậy đoạn code này đơn giản thêm một phương thức override lại Dispose(bool)
để thực hiện việc giải phóng đối tượng context.
Giờ bạn đã có đầy đủ các trang để thực hiện các thao tác cập nhật, thêm mới và xóa các thực thể Student. Trong bài tiếp theo bạn sẽ mở rộng chức năng của trang Index bằng việc thêm vào các tính năng sắp xếp và phân trang.
Các liên kết đến Entity Framework có thể được tìm thấy trong bài cuối của loạt bài này.
Người dịch: Đào Hải Nam