View components (thành phần view) trong ASP.NET Core
Nguồn: View components
Bởi Rick Anderson
View components
View components (thành phần view) tương tự như partial views (view một phần), nhưng mạnh mẽ hơn nhiều. View components không sử dụng model binding (ràng buộc mô hình). Chúng phụ thuộc vào dữ liệu được truyền vào khi gọi view component. Bài viết này được viết sử dụng controllers và views, nhưng view components cũng hoạt động với Razor Pages.
Một view component:
- Render một phần thay vì toàn bộ response.
- Bao gồm cùng các lợi ích về phân tách mối quan tâm (separation-of-concerns) và khả năng kiểm thử như giữa controller và view.
- Có thể có tham số và logic nghiệp vụ.
- Thường được gọi từ trang layout.
View components được thiết kế cho bất kỳ nơi nào có logic render có thể tái sử dụng quá phức tạp cho partial view, chẳng hạn như:
- Menu điều hướng động.
- Tag cloud (đám mây thẻ), nơi nó truy vấn cơ sở dữ liệu.
- Bảng đăng nhập.
- Giỏ hàng.
- Các bài viết mới xuất bản gần đây.
- Nội dung thanh bên trên blog.
- Bảng đăng nhập được render trên mỗi trang và hiển thị các liên kết đăng xuất hoặc đăng nhập, tùy thuộc vào trạng thái đăng nhập của người dùng.
Một view component gồm hai phần:
- Lớp class, thường được dẫn xuất từ ViewComponent.
- Kết quả nó trả về, thường là một view.
Giống như controllers, view component có thể là POCO, nhưng hầu hết các developer tận dụng các phương thức và thuộc tính có sẵn bằng cách dẫn xuất từ ViewComponent.
Khi xem xét liệu view components có đáp ứng thông số kỹ thuật của ứng dụng không, hãy xem xét sử dụng Razor components thay thế. Razor components cũng kết hợp markup với mã C# để tạo ra các đơn vị UI có thể tái sử dụng. Razor components được thiết kế cho năng suất của developer khi cung cấp logic UI phía client và composition. Để biết thêm thông tin, xem ASP.NET Core Razor components.
Tạo view component
Phần này chứa các yêu cầu cấp cao để tạo view component. Ở phần sau của bài viết, chúng ta sẽ xem xét từng bước chi tiết và tạo một view component.
Lớp view component
Bất kỳ cách nào sau đây đều có thể tạo một lớp view component:
- Dẫn xuất từ ViewComponent.
- Trang trí một lớp với thuộc tính
[ViewComponent], hoặc dẫn xuất từ một lớp có thuộc tính[ViewComponent]. - Tạo một lớp có tên kết thúc với hậu tố
ViewComponent.
Giống như controllers, view components phải là các lớp public, không lồng nhau và không trừu tượng. Tên view component là tên lớp với hậu tố ViewComponent bị xóa. Nó cũng có thể được chỉ định rõ ràng bằng thuộc tính Name.
Một lớp view component:
- Hỗ trợ dependency injection thông qua constructor.
- Không tham gia vào vòng đời controller, do đó filters không thể được sử dụng trong view component.
Để ngăn một lớp có hậu tố ViewComponent không phân biệt hoa thường bị coi là view component, hãy trang trí lớp đó với thuộc tính [NonViewComponent]:
using Microsoft.AspNetCore.Mvc;
[NonViewComponent]
public class ReviewComponent
{
public string Status(string name) => JobStatus.GetCurrentStatus(name);
}Các phương thức view component
Một view component định nghĩa logic của nó trong:
- Phương thức
InvokeAsynctrả vềTask<IViewComponentResult>. - Phương thức đồng bộ
Invoketrả về IViewComponentResult.
Tham số đến trực tiếp từ việc gọi view component, không phải từ model binding. View component không bao giờ xử lý trực tiếp một request. Thông thường, view component khởi tạo một model và truyền nó cho view bằng cách gọi phương thức View. Tóm lại, các phương thức view component:
- Định nghĩa phương thức
InvokeAsynctrả vềTask<IViewComponentResult>hoặc phương thức đồng bộInvoketrả vềIViewComponentResult. - Thường khởi tạo model và truyền nó cho view bằng cách gọi phương thức ViewComponent.View.
- Tham số đến từ phương thức gọi, không phải HTTP. Không có model binding.
- Không thể truy cập trực tiếp như một HTTP endpoint. Chúng thường được gọi trong một view. View component không bao giờ xử lý request.
- Bị nạp chồng (overloaded) theo chữ ký thay vì bất kỳ chi tiết nào từ HTTP request hiện tại.
Đường dẫn tìm kiếm view
Runtime tìm kiếm view theo các đường dẫn sau:
- /Views/{Controller Name}/Components/{View Component Name}/{View Name}
- /Views/Shared/Components/{View Component Name}/{View Name}
- /Pages/Shared/Components/{View Component Name}/{View Name}
- /Areas/{Area Name}/Views/Shared/Components/{View Component Name}/{View Name}
Đường dẫn tìm kiếm áp dụng cho các dự án sử dụng controllers + views và Razor Pages.
Tên view mặc định cho view component là Default, có nghĩa là các file view thường sẽ được đặt tên là Default.cshtml. Có thể chỉ định tên view khác khi tạo kết quả view component hoặc khi gọi phương thức View.
Chúng tôi khuyến nghị đặt tên file view là Default.cshtml và sử dụng đường dẫn Views/Shared/Components/{View Component Name}/{View Name}. View component PriorityList được sử dụng trong mẫu này dùng Views/Shared/Components/PriorityList/Default.cshtml cho view của view component.
Tùy chỉnh đường dẫn tìm kiếm view
Để tùy chỉnh đường dẫn tìm kiếm view, hãy sửa đổi collection ViewLocationFormats của Razor. Ví dụ, để tìm kiếm views trong đường dẫn /Components/{View Component Name}/{View Name}, hãy thêm một mục mới vào collection:
using Microsoft.EntityFrameworkCore;
using ViewComponentSample.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews()
.AddRazorOptions(options =>
{
options.ViewLocationFormats.Add("/{0}.cshtml");
});
builder.Services.AddDbContext<ToDoContext>(options =>
options.UseInMemoryDatabase("db"));
var app = builder.Build();
// Phần còn lại được bỏ qua cho ngắn gọn.Trong đoạn code trên, placeholder {0} đại diện cho đường dẫn Components/{View Component Name}/{View Name}.
Gọi view component
Để sử dụng view component, hãy gọi như sau bên trong view:
@await Component.InvokeAsync("Name of view component",
{Anonymous Type Containing Parameters})Các tham số được truyền cho phương thức InvokeAsync. View component PriorityList được phát triển trong bài viết này được gọi từ file view Views/ToDo/Index.cshtml. Trong đoạn code sau, phương thức InvokeAsync được gọi với hai tham số:
</table>
<div>
Maximum Priority: @ViewData["maxPriority"] <br />
Is Complete: @ViewData["isDone"]
@await Component.InvokeAsync("PriorityList",
new {
maxPriority = ViewData["maxPriority"],
isDone = ViewData["isDone"] }
)
</div>Gọi view component như Tag Helper
View Component có thể được gọi như một Tag Helper:
<div>
Maxium Priority: @ViewData["maxPriority"] <br />
Is Complete: @ViewData["isDone"]
@{
int maxPriority = Convert.ToInt32(ViewData["maxPriority"]);
bool isDone = Convert.ToBoolean(ViewData["isDone"]);
}
<vc:priority-list max-priority=maxPriority is-done=isDone>
</vc:priority-list>
</div>Các tham số lớp và phương thức dạng Pascal-case cho Tag Helpers được chuyển thành kebab case. Tag Helper để gọi view component sử dụng phần tử <vc></vc>. View component được chỉ định như sau:
<vc:[view-component-name] parameter1="parameter1 value" parameter2="parameter2 value"> </vc:[view-component-name]>
Để sử dụng view component như Tag Helper, hãy đăng ký assembly chứa view component bằng chỉ thị @addTagHelper. Nếu view component nằm trong một assembly có tên MyWebApp, hãy thêm chỉ thị sau vào file _ViewImports.cshtml:
@addTagHelper *, MyWebApp
View component có thể được đăng ký như Tag Helper cho bất kỳ file nào tham chiếu đến view component. Xem Managing Tag Helper Scope để biết thêm thông tin về cách đăng ký Tag Helpers.
Phương thức InvokeAsync được sử dụng trong hướng dẫn này:
@await Component.InvokeAsync("PriorityList",
new {
maxPriority = ViewData["maxPriority"],
isDone = ViewData["isDone"] }
)Trong markup trên, view component PriorityList trở thành priority-list. Các tham số cho view component được truyền dưới dạng thuộc tính theo kiểu kebab case.
Gọi view component trực tiếp từ controller
View components thường được gọi từ view, nhưng chúng có thể được gọi trực tiếp từ phương thức controller. Mặc dù view components không định nghĩa endpoints như controllers, một controller action trả về nội dung của ViewComponentResult có thể được triển khai.
Trong ví dụ sau, view component được gọi trực tiếp từ controller:
public IActionResult IndexVC(int maxPriority = 2, bool isDone = false)
{
return ViewComponent("PriorityList",
new {
maxPriority = maxPriority,
isDone = isDone
});
}Tạo view component cơ bản
Tải xuống, build và kiểm tra code khởi đầu. Đây là một dự án cơ bản với controller ToDo hiển thị danh sách các mục ToDo.
Cập nhật controller để truyền priority và trạng thái hoàn thành
Cập nhật phương thức Index để sử dụng tham số priority và trạng thái hoàn thành:
using Microsoft.AspNetCore.Mvc;
using ViewComponentSample.Models;
namespace ViewComponentSample.Controllers;
public class ToDoController : Controller
{
private readonly ToDoContext _ToDoContext;
public ToDoController(ToDoContext context)
{
_ToDoContext = context;
_ToDoContext.Database.EnsureCreated();
}
public IActionResult Index(int maxPriority = 2, bool isDone = false)
{
var model = _ToDoContext!.ToDo!.ToList();
ViewData["maxPriority"] = maxPriority;
ViewData["isDone"] = isDone;
return View(model);
}Thêm lớp ViewComponent
Thêm lớp ViewComponent vào ViewComponents/PriorityListViewComponent.cs:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ViewComponentSample.Models;
namespace ViewComponentSample.ViewComponents;
public class PriorityListViewComponent : ViewComponent
{
private readonly ToDoContext db;
public PriorityListViewComponent(ToDoContext context) => db = context;
public async Task<IViewComponentResult> InvokeAsync(
int maxPriority, bool isDone)
{
var items = await GetItemsAsync(maxPriority, isDone);
return View(items);
}
private Task<List<TodoItem>> GetItemsAsync(int maxPriority, bool isDone)
{
return db!.ToDo!.Where(x => x.IsDone == isDone &&
x.Priority <= maxPriority).ToListAsync();
}
}Ghi chú về code:
- Các lớp view component có thể được chứa trong bất kỳ thư mục nào trong dự án.
- Vì tên lớp PriorityListViewComponent kết thúc với hậu tố ViewComponent, runtime sử dụng chuỗi
PriorityListkhi tham chiếu class component từ view. - Thuộc tính
[ViewComponent]có thể thay đổi tên được sử dụng để tham chiếu view component. Ví dụ, lớp có thể được đặt tên làXYZvới thuộc tính[ViewComponent]sau:
``csharp [ViewComponent(Name = "PriorityList")] public class XYZ : ViewComponent ``
- Thuộc tính
[ViewComponent]trong code trên cho bộ chọn view component biết phải sử dụng: - Tên
PriorityListkhi tìm kiếm views liên quan đến component. - Chuỗi "PriorityList" khi tham chiếu class component từ view.
- Component sử dụng dependency injection để làm cho data context có sẵn.
InvokeAsyncexpose (công khai) một phương thức có thể được gọi từ view, và nó có thể nhận số lượng tham số tùy ý.- Phương thức
InvokeAsynctrả về tập hợp các mụcToDothỏa mãn các tham sốisDonevàmaxPriority.
Tạo Razor view cho view component
- Tạo thư mục Views/Shared/Components. Thư mục này phải được đặt tên là Components.
- Tạo thư mục Views/Shared/Components/PriorityList. Tên thư mục này phải khớp với tên lớp view component, hoặc tên lớp trừ đi hậu tố. Nếu thuộc tính
ViewComponentđược sử dụng, tên lớp sẽ cần khớp với chỉ định của thuộc tính. - Tạo Razor view
Views/Shared/Components/PriorityList/Default.cshtml:
```cshtml @model IEnumerable<ViewComponentSample.Models.TodoItem>
<h3>Priority Items</h3> <ul> @foreach (var todo in Model) { <li>@todo.Name</li> } </ul> ```
Razor view nhận danh sách TodoItem và hiển thị chúng. Nếu phương thức InvokeAsync của view component không truyền tên view, Default được sử dụng cho tên view theo quy ước.
- Thêm
divchứa lời gọi đến priority list component vào cuối fileViews/ToDo/index.cshtml:
```cshtml </table>
<div> Maximum Priority: @ViewData["maxPriority"] <br /> Is Complete: @ViewData["isDone"] @await Component.InvokeAsync("PriorityList", new { maxPriority = ViewData["maxPriority"], isDone = ViewData["isDone"] } ) </div> ```
Markup @await Component.InvokeAsync cho thấy cú pháp để gọi view components. Tham số đầu tiên là tên component chúng ta muốn gọi. Các tham số tiếp theo được truyền cho component. InvokeAsync có thể nhận số lượng tham số tùy ý.
Chỉ định tên view component
Một view component phức tạp có thể cần chỉ định view không mặc định trong một số điều kiện. Đoạn code sau cho thấy cách chỉ định view "PVC" từ phương thức InvokeAsync. Cập nhật phương thức InvokeAsync trong lớp PriorityListViewComponent.
public async Task<IViewComponentResult> InvokeAsync(
int maxPriority, bool isDone)
{
string MyView = "Default";
// Nếu yêu cầu tất cả các tác vụ đã hoàn thành, render với view "PVC".
if (maxPriority > 3 && isDone == true)
{
MyView = "PVC";
}
var items = await GetItemsAsync(maxPriority, isDone);
return View(MyView, items);
}Sao chép file Views/Shared/Components/PriorityList/Default.cshtml sang view có tên Views/Shared/Components/PriorityList/PVC.cshtml. Thêm tiêu đề để chỉ ra view PVC đang được sử dụng.
@model IEnumerable<ViewComponentSample.Models.TodoItem>
<h2> PVC Named Priority Component View</h2>
<h4>@ViewBag.PriorityMessage</h4>
<ul>
@foreach (var todo in Model)
{
<li>@todo.Name</li>
}
</ul>Kiểm tra đường dẫn view
- Thay đổi tham số priority thành ba hoặc ít hơn để view priority không được trả về.
- Tạm thời đổi tên
Views/ToDo/Components/PriorityList/Default.cshtmlthành1Default.cshtml. - Kiểm tra ứng dụng, lỗi sau sẽ xảy ra:
``txt An unhandled exception occurred while processing the request. InvalidOperationException: The view 'Components/PriorityList/Default' wasn't found. The following locations were searched: /Views/ToDo/Components/PriorityList/Default.cshtml /Views/Shared/Components/PriorityList/Default.cshtml ``
- Sao chép
Views/ToDo/Components/PriorityList/1Default.cshtmlsangViews/Shared/Components/PriorityList/Default.cshtml. - Thêm markup vào view component ToDo Shared để chỉ ra rằng view đến từ thư mục Shared.
- Kiểm tra view component Shared.
Tránh hard-coded strings (chuỗi được code cứng)
Để đảm bảo an toàn tại thời điểm biên dịch, hãy thay thế tên view component được code cứng bằng tên lớp. Cập nhật file PriorityListViewComponent.cs để không sử dụng hậu tố "ViewComponent":
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ViewComponentSample.Models;
namespace ViewComponentSample.ViewComponents;
public class PriorityList : ViewComponent
{
private readonly ToDoContext db;
public PriorityList(ToDoContext context)
{
db = context;
}
public async Task<IViewComponentResult> InvokeAsync(
int maxPriority, bool isDone)
{
var items = await GetItemsAsync(maxPriority, isDone);
return View(items);
}
private Task<List<TodoItem>> GetItemsAsync(int maxPriority, bool isDone)
{
return db!.ToDo!.Where(x => x.IsDone == isDone &&
x.Priority <= maxPriority).ToListAsync();
}
}File view:
</table>
<div>
Testing nameof(PriorityList) <br />
Maxium Priority: @ViewData["maxPriority"] <br />
Is Complete: @ViewData["isDone"]
@await Component.InvokeAsync(nameof(PriorityList),
new {
maxPriority = ViewData["maxPriority"],
isDone = ViewData["isDone"] }
)
</div>Một overload của phương thức Component.InvokeAsync nhận kiểu CLR sử dụng toán tử typeof:
</table>
<div>
Testing typeof(PriorityList) <br />
Maxium Priority: @ViewData["maxPriority"] <br />
Is Complete: @ViewData["isDone"]
@await Component.InvokeAsync(typeof(PriorityList),
new {
maxPriority = ViewData["maxPriority"],
isDone = ViewData["isDone"] }
)
</div>Thực hiện công việc đồng bộ
Framework xử lý việc gọi phương thức Invoke đồng bộ nếu không yêu cầu công việc bất đồng bộ. Phương thức sau tạo view component Invoke đồng bộ:
using Microsoft.AspNetCore.Mvc;
using ViewComponentSample.Models;
namespace ViewComponentSample.ViewComponents
{
public class PriorityListSync : ViewComponent
{
private readonly ToDoContext db;
public PriorityListSync(ToDoContext context)
{
db = context;
}
public IViewComponentResult Invoke(int maxPriority, bool isDone)
{
var x = db!.ToDo!.Where(x => x.IsDone == isDone &&
x.Priority <= maxPriority).ToList();
return View(x);
}
}
}File Razor của view component:
<div>
Testing nameof(PriorityList) <br />
Maxium Priority: @ViewData["maxPriority"] <br />
Is Complete: @ViewData["isDone"]
@await Component.InvokeAsync(nameof(PriorityListSync),
new {
maxPriority = ViewData["maxPriority"],
isDone = ViewData["isDone"] }
)
</div>View component được gọi trong file Razor (ví dụ: Views/Home/Index.cshtml) sử dụng một trong các cách tiếp cận sau:
Để sử dụng cách tiếp cận IViewComponentHelper, hãy gọi Component.InvokeAsync:
@await Component.InvokeAsync(nameof(PriorityList),
new { maxPriority = 4, isDone = true })Để sử dụng Tag Helper, hãy đăng ký assembly chứa View Component bằng chỉ thị @addTagHelper (view component nằm trong assembly có tên MyWebApp):
@addTagHelper *, MyWebApp
Sử dụng Tag Helper của view component trong file Razor markup:
<vc:priority-list max-priority="999" is-done="false"> </vc:priority-list>
Chữ ký phương thức của PriorityList.Invoke là đồng bộ, nhưng Razor tìm và gọi phương thức với Component.InvokeAsync trong file markup.