Phần 7: Thêm chức năng tìm kiếm vào ứng dụng ASP.NET Core MVC
Trong phần này, bạn thêm khả năng tìm kiếm vào action method (phương thức hành động) Index để có thể tìm kiếm phim theo thể loại hoặc tên.
Cập nhật method Index trong Controllers/MoviesController.cs với đoạn code sau:
public async Task<IActionResult> Index(string searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
return View(await movies.ToListAsync());
}Dòng sau trong action method Index tạo một truy vấn LINQ (Language Integrated Query - truy vấn tích hợp ngôn ngữ) để chọn các phim:
var movies = from m in _context.Movie
select m;Truy vấn chỉ được định nghĩa tại thời điểm này, nó chưa được thực thi trên cơ sở dữ liệu.
Nếu tham số searchString chứa một chuỗi, truy vấn phim được sửa đổi để lọc theo giá trị của chuỗi tìm kiếm:
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}Đoạn code s => s.Title!.ToUpper().Contains(searchString.ToUpper()) ở trên là một Lambda Expression (biểu thức lambda). Lambda được sử dụng trong các truy vấn LINQ dựa trên method làm đối số cho các method truy vấn chuẩn như method Where hoặc Contains (được sử dụng trong code trên). Các truy vấn LINQ không được thực thi khi chúng được định nghĩa hoặc khi chúng được sửa đổi bằng cách gọi một method như Where, Contains, hoặc OrderBy. Thay vào đó, việc thực thi truy vấn bị trì hoãn (deferred execution). Điều đó có nghĩa là việc đánh giá một biểu thức bị trì hoãn cho đến khi giá trị thực tế của nó được lặp qua hoặc method ToListAsync được gọi.
Method Contains được chạy trên cơ sở dữ liệu, không phải trong code C#. Phân biệt chữ hoa/thường trong truy vấn phụ thuộc vào cơ sở dữ liệu và collation (quy tắc đối chiếu). Trên SQL Server, Contains ánh xạ tới SQL LIKE, không phân biệt chữ hoa/thường. SQLite với collation mặc định là hỗn hợp giữa phân biệt và không phân biệt chữ hoa/thường, tùy thuộc vào truy vấn.
Điều hướng đến /Movies/Index. Thêm một query string (chuỗi truy vấn) như ?searchString=Ghost vào URL. Các phim được lọc sẽ được hiển thị.
Nếu bạn thay đổi chữ ký của method Index để có một tham số tên là id, tham số id sẽ khớp với placeholder (giữ chỗ) {id} tùy chọn cho các route (tuyến đường) mặc định được thiết lập trong Program.cs.
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");Thay đổi tham số thành id và thay đổi tất cả các lần xuất hiện của searchString thành id.
Method Index trước đó:
public async Task<IActionResult> Index(string searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
return View(await movies.ToListAsync());
}Method Index được cập nhật với tham số id:
public async Task<IActionResult> Index(string id)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(id))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(id.ToUpper()));
}
return View(await movies.ToListAsync());
}Bây giờ bạn có thể truyền tiêu đề tìm kiếm như là route data (một phân đoạn URL) thay vì là giá trị query string.
Tuy nhiên, bạn không thể kỳ vọng người dùng sẽ sửa đổi URL mỗi khi họ muốn tìm kiếm phim. Vì vậy, bây giờ bạn sẽ thêm các phần tử UI (giao diện người dùng) để giúp họ lọc phim. Nếu bạn đã thay đổi chữ ký của method Index để kiểm tra cách truyền tham số ID được liên kết với route, hãy đổi lại để nó nhận một tham số tên là searchString:
public async Task<IActionResult> Index(string searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
return View(await movies.ToListAsync());
}Mở file Views/Movies/Index.cshtml và thêm markup <form> được đánh dấu nổi bật trong đoạn code sau:
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index">
<p>
<label>Title: <input type="text" name="SearchString" /></label>
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">Thẻ HTML <form> sử dụng Form Tag Helper (trình trợ giúp thẻ Form), vì vậy khi bạn submit form, chuỗi lọc được POST đến action Index của movies controller. Lưu các thay đổi của bạn và sau đó kiểm tra bộ lọc.
Không có overload [HttpPost] của method Index như bạn có thể mong đợi. Bạn không cần nó, vì method không thay đổi trạng thái của ứng dụng, chỉ lọc dữ liệu.
Bạn có thể thêm method [HttpPost] Index sau.
[HttpPost]
public string Index(string searchString, bool notUsed)
{
return "From [HttpPost]Index: filter on " + searchString;
}Tham số notUsed được sử dụng để tạo một overload cho method Index. Chúng ta sẽ nói về điều đó sau trong hướng dẫn.
Tuy nhiên, ngay cả khi bạn thêm phiên bản [HttpPost] này của method Index, có một hạn chế trong cách tất cả đã được triển khai. Hãy tưởng tượng bạn muốn đánh dấu trang cho một tìm kiếm cụ thể hoặc bạn muốn gửi link cho bạn bè để họ có thể nhấp vào để xem cùng danh sách phim đã lọc. Lưu ý rằng URL cho yêu cầu HTTP POST giống với URL cho yêu cầu GET (localhost:{PORT}/Movies/Index) -- không có thông tin tìm kiếm trong URL. Thông tin chuỗi tìm kiếm được gửi đến server như là một giá trị form field (trường biểu mẫu).
Vì tham số tìm kiếm nằm trong request body chứ không phải URL, bạn không thể nắm bắt thông tin tìm kiếm đó để đánh dấu trang hoặc chia sẻ với người khác. Khắc phục điều này bằng cách chỉ định yêu cầu phải là HTTP GET trong thẻ form trong file Views/Movies/Index.cshtml.
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
<label>Title: <input type="text" name="SearchString" /></label>
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">Markup sau hiển thị sự thay đổi đối với thẻ form:
<form asp-controller="Movies" asp-action="Index" method="get">
Bây giờ khi bạn submit một tìm kiếm, URL chứa query string tìm kiếm. Tìm kiếm cũng sẽ đến action method HttpGet Index, ngay cả khi bạn có method HttpPost Index.
Thêm tìm kiếm theo thể loại
Thêm class MovieGenreViewModel sau vào thư mục Models:
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace MvcMovie.Models;
public class MovieGenreViewModel
{
public List<Movie>? Movies { get; set; }
public SelectList? Genres { get; set; }
public string? MovieGenre { get; set; }
public string? SearchString { get; set; }
}View model (mô hình hiển thị) movie-genre sẽ chứa:
- Một danh sách phim.
- Một
SelectListchứa danh sách các thể loại. Điều này cho phép người dùng chọn một thể loại từ danh sách. MovieGenre, chứa thể loại được chọn.SearchString, chứa văn bản người dùng nhập vào ô tìm kiếm.
Thay thế method Index trong MoviesController.cs bằng đoạn code sau:
// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
var movies = from m in _context.Movie
select m;
if (!string.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
if (!string.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}
var movieGenreVM = new MovieGenreViewModel
{
Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
Movies = await movies.ToListAsync()
};
return View(movieGenreVM);
}Đoạn code sau là một truy vấn LINQ truy xuất tất cả các thể loại từ cơ sở dữ liệu.
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;SelectList của các thể loại được tạo bằng cách chiếu các thể loại riêng biệt (chúng ta không muốn select list có các thể loại trùng lặp).
Khi người dùng tìm kiếm mục, giá trị tìm kiếm được giữ lại trong ô tìm kiếm.
Thêm tìm kiếm theo thể loại vào Index view
Cập nhật Index.cshtml trong Views/Movies/ như sau:
@model MvcMovie.Models.MovieGenreViewModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>
<label>Title: <input type="text" asp-for="SearchString" /></label>
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movies![0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies![0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies![0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies![0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movies!)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Details" asp-route-id="@item.Id">Details</a> |
<a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>Kiểm tra biểu thức lambda được sử dụng trong HTML Helper sau:
@Html.DisplayNameFor(model => model.Movies![0].Title)
Trong đoạn code trên, HTML Helper DisplayNameFor kiểm tra thuộc tính Title được tham chiếu trong biểu thức lambda để xác định tên hiển thị. Vì biểu thức lambda được kiểm tra thay vì được đánh giá, bạn không nhận được lỗi vi phạm truy cập khi model, model.Movies, hoặc model.Movies[0] là null hoặc rỗng. Khi biểu thức lambda được đánh giá (ví dụ: @Html.DisplayFor(modelItem => item.Title)), các giá trị thuộc tính của model được đánh giá. Dấu ! sau model.Movies là toán tử null-forgiving (bỏ qua null), được sử dụng để khai báo rằng Movies không phải null.
Kiểm tra ứng dụng bằng cách tìm kiếm theo thể loại, theo tiêu đề phim, và theo cả hai.