翻譯自 Waqas Anwar 2021年4月15日的文章 《A Developer’s Guide To Blazor Templated Components》 [1]
在我之前的一篇文章 Blazor 組件入門指南中,我介紹了組件參數,並向您展示了如何將數據作為參數傳遞給 Blazor 組件以定制化其功能。在這篇文章中,我將更進一步向您展示,如何將一個或多個 UI 模板作為參數傳遞給一個稱之為模板化組件的不同類型的 Blazor 組件。
Blazor 模板化組件概述
Blazor 模板化組件是一種接受將一個或多個 UI 模板作為參數的組件。這有助於組件的可重用性,因為您只需要創建一次模板化組件,然后使用該組件的每個頁面都可以提供其 UI 模板,模板化組件可以根據頁面需求渲染此 UI 模板。
本文中的模板化組件示例包括:
- 一個允許用戶指定表格表頭、行和頁腳模板的表格組件。
- 一個允許用戶呈現具有相同外觀和體驗而具有不同內容的小部件組件。
- 一個允許用戶指定一個模板來呈現項目符號或編號等列表項的列表組件。
- 一個允許用戶以列表、網格或卡片視圖來顯示數據的列表組件。
當我們創建 Blazor 組件的一個參數時,我們通常將其類型指定為 string
、int
或者其他內置 .NET 數據類型。為了創建一個模板化組件,我們需要創建類型為 RenderFragment
或 RenderFragment<T>
的組件參數。RenderFragment 允許我們提供一個可以由模板化組件渲染的 UI 內容片段(作為一個委托實現,將其內容寫入到 RenderTreeBuilder)。
[Parameter]
public RenderFragment HeaderTemplate { get; set; }
RenderFragment<T>
更進一步,允許我們傳入參數的類型 T
,可以用它來自定義模板化組件的輸出。
[Parameter]
public RenderFragment<T> RowTemplate { get; set; }
從一個實例開始
為了詳細了解模板化組件,我決定構建一個 TableWidget 模板化組件,它允許我們自定義不同格式的表頭、行和頁腳。在創建第一個模板化組件之前,我們先來創建一個新的 Blazor Server 應用程序並添加其基本功能,以表格格式呈現一些數據。
在 Blazor Server 應用程序中創建一個 Data 文件夾,並在 Data 文件夾中添加以下兩個模型類。
Product.cs
public class Product
{
public int Id { get; set; }
public string Title { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
}
Order.cs
public class Order
{
public int Id { get; set; }
public string OrderNo { get; set; }
public DateTime OrderDate { get; set; }
public string Status { get; set; }
public decimal OrderTotal { get; set; }
}
在項目中創建一個 Services 文件夾,並在 Services 文件夾中添加如下的 IProductService
和 ProductService
。在本教程中,我僅返回一些用於生成表格的模擬數據。
IProductService.cs
public interface IProductService
{
List<Product> GetTopSellingProducts();
}
ProductService.cs
public class ProductService : IProductService
{
public List<Product> GetTopSellingProducts()
{
return new List<Product>()
{
new Product()
{
Id = 1,
Title = "Wireless Mouse",
Price = 29.99m,
Quantity = 3
},
new Product()
{
Id = 2,
Title = "HP Headphone",
Price = 79.99m,
Quantity = 4
},
new Product()
{
Id = 3,
Title = "Sony Keyboard",
Price = 119.99m,
Quantity = 5
}
};
}
}
接下來,在同一 Services 文件夾中創建 IOrderService
和 OrderService
並添加一些用於生成表格的模擬訂單數據。
IOrderService.cs
public interface IOrderService
{
List<Order> GetLatestOrders();
}
OrderService.cs
public class OrderService : IOrderService
{
public List<Order> GetLatestOrders()
{
return new List<Order>()
{
new Order()
{
Id = 1,
OrderNo = "12345",
OrderDate = DateTime.Today.AddDays(-2),
Status = "Pending",
OrderTotal = 399.99m
},
new Order()
{
Id = 2,
OrderNo = "67890",
OrderDate = DateTime.Today.AddDays(-5),
Status = "Completed",
OrderTotal = 199.99m
},
new Order()
{
Id = 3,
OrderNo = "13579",
OrderDate = DateTime.Today.AddDays(-7),
Status = "Completed",
OrderTotal = 249.99m
}
};
}
}
我們需要使用依賴注入將上述服務注入到 Blazor 組件中,為此,我們需要在 Startup.cs 文件中注冊上述服務。如果您想了解關於依賴注入的更多知識,可以閱讀我的文章 A Step by Step Guide to ASP.NET Core Dependency Injection[3]。
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<WeatherForecastService>();
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<IProductService, ProductService>();
}
接下來,在項目 Pages 文件夾中創建 Blazor 組件 Dashboard.razor 及其對應的代碼隱藏文件 Dashboard.razor.cs。如果您不熟悉 Blazor 組件及代碼隱藏文件,請閱讀我的文章 Blazor 組件入門指南。
組件的代碼隱藏文件 Dashboard.razor.cs 中同時注入了 IOrderService 和 IProductService,然后我們將使用 GetLatestOrders
和 GetTopSellingProducts
方法來填充我們的本地 Orders 和 Products 列表。
Dashboard.razor.cs
public partial class Dashboard
{
[Inject]
private IOrderService OrderService { get; set; }
[Inject]
private IProductService ProductService { get; set; }
private List<Order> Orders { get; set; }
private List<Product> Products { get; set; }
protected override void OnInitialized()
{
Orders = OrderService.GetLatestOrders();
Products = ProductService.GetTopSellingProducts();
}
}
Razor 組件視圖文件將簡單地在 Orders 和 Products 上運行 foreach 循環,並生成 HTML 表格。
@page "/dashboard"
<h1>Dashboard</h1>
<br />
<div class="row">
<div class="col">
@if (Orders != null)
{
<table class="table table-striped table-bordered">
<thead class="thead-dark">
<tr>
<th scope="col">Order</th>
<th scope="col">Date</th>
<th scope="col">Status</th>
<th scope="col">Total</th>
</tr>
</thead>
<tbody>
@foreach (var order in Orders)
{
<tr>
<td>@order.OrderNo</td>
<td>@order.OrderDate.ToShortDateString()</td>
<td>@order.Status</td>
<td>@order.OrderTotal</td>
</tr>
}
</tbody>
</table>
}
</div>
<div class="col">
@if (Products != null)
{
<h3>Top Selling Products</h3>
<table class="table table-striped table-bordered">
<thead class="thead-dark">
<tr>
<th scope="col">Title</th>
<th scope="col">Price</th>
<th scope="col">Quantity</th>
</tr>
</thead>
<tbody>
@foreach (var product in Products)
{
<tr>
<td>@product.Title</td>
<td>@product.Price</td>
<td>@product.Quantity</td>
</tr>
}
</tbody>
</table>
}
</div>
</div>
此時如果您運行項目,將在頁面上看到以下兩個表格。
截至目前,我們尚沒有創建任何模板化組件,但您會感覺到我們很快將需要一個,因為上面顯示的訂單和產品表格幾乎都具有相同的外觀和體驗,並且我們在上面的 foreach 循環中復制了大量的 HTML 來生成這兩張表格。一個好注意是,創建一個模板化組件,然后重用該組件來生成上述兩張表格,並且仍然能夠自定義它們顯示的表頭和數據行。讓我們來創建我們的第一個模板化組件,命名為 TableWidget 組件。
創建 Blazor 模板化組件
在 Shared 文件夾中新建一個 Razor 組件 TableWidget.razor,並在其中添加以下代碼:
TableWidget.razor
@typeparam TItem
<br />
<h3>@Title</h3>
<table class="table table-striped table-bordered">
<thead class="thead-dark">
<tr>
@HeaderTemplate
</tr>
</thead>
<tbody>
@foreach (var item in Items)
{
<tr>
@RowTemplate(item)
</tr>
}
</tbody>
<tfoot>
<tr>
@FooterTemplate
</tr>
</tfoot>
</table>
@code {
[Parameter]
public string Title { get; set; }
[Parameter]
public RenderFragment HeaderTemplate { get; set; }
[Parameter]
public RenderFragment<TItem> RowTemplate { get; set; }
[Parameter]
public RenderFragment FooterTemplate { get; set; }
[Parameter]
public IReadOnlyList<TItem> Items { get; set; }
}
我們的 TableWidget 組件包含以下三個模板:
[Parameter]
public RenderFragment HeaderTemplate { get; set; }
[Parameter]
public RenderFragment<TItem> RowTemplate { get; set; }
[Parameter]
public RenderFragment FooterTemplate { get; set; }
HeaderTemplate 允許用戶在表格的表頭中呈現任意 UI 模板。此模板用於在 thead 元素內渲染表格表頭的單元格。
<thead class="thead-dark">
<tr>
@HeaderTemplate
</tr>
</thead>
FooterTemplate 與 HeaderTemplate 類似,它允許用戶在表格的頁腳中呈現任意 UI 模板。此模板用於在 tfoot 元素內渲染表格頁腳的單元格。
<tfoot>
<tr>
@FooterTemplate
</tr>
</tfoot>
RowTemplate 的類型為 RanderFragment<TItem>
,它允許用戶使用任意的 .NET 類型渲染 UI 模板。該類型不是固定的,而是使用組件頂部的 @typeparam 指令聲明為一個泛型類型。
@typeparam TItem
我們還在組件中創建了一個 TItem
對象的集合,以便我們可以迭代該集合生成表格的行。
[Parameter]
public IReadOnlyList<TItem> Items { get; set; }
我們將要傳入 UI 模板中的 TItem
類型的對象會使用以下 foreach 循環進行渲染。您很快就會看到這將如何幫助我們使用相同的 TableWidget 組件同時渲染產品和訂單表格。
<tbody>
@foreach (var item in Items)
{
<tr>
@RowTemplate(item)
</tr>
}
</tbody>
使用 Blazor 模板化組件的不同方式
現在是時候來實踐一下我們的 TableWidget 組件了,我們可以通過不同的方式使用這個組件。用下面的 TableWidget 組件替換我們前面生成的 Recent Orders 表格。
<div class="col">
@if (Orders != null)
{
<TableWidget Title="Recent Orders" Items="Orders">
<HeaderTemplate>
<th scope="col">Order</th>
<th scope="col">Date</th>
<th scope="col">Status</th>
<th scope="col">Total</th>
</HeaderTemplate>
<RowTemplate>
<td>@context.OrderNo</td>
<td>@context.OrderDate.ToShortDateString()</td>
<td>@context.Status</td>
<td>@context.OrderTotal</td>
</RowTemplate>
</TableWidget>
}
</div>
在上面的代碼片段中,Items 屬性是使用我們的從服務獲取的 Orders 列表進行初始化的。然后我們選擇使用 HeaderTemplate 和 RowTemplate 來生成表格的表頭和數據行。您可能在想 context 是從哪里來的?context 是一個隱式參數,所有類型為 RenderFragment<T>
的組件參數都可以使用。我們可以使用 context 訪問我們正在處理對象的屬性。在上面的示例中,context 將向模板提供訂單信息。
如果此時您運行項目,會在頁面上看到以下兩個表格。現在,最近的訂單(Recent Orders)表格是使用我們的 TableWidget 組件生成的了。
讓我們重用 TableWidget 組件來生成熱賣產品(Top Selling Products)表格。這一次,我們傳遞了 Products 列表給它,還指定了我們自己的 Context="product",這意味着現在我們可以使用 product 取代隱式參數 context 來訪問產品的屬性。
<div class="col">
@if (Products != null)
{
<TableWidget Title="Top Selling Products" Items="Products" Context="product">
<HeaderTemplate>
<th scope="col">Title</th>
<th scope="col">Price</th>
<th scope="col">Quantity</th>
</HeaderTemplate>
<RowTemplate>
<td>@product.Title</td>
<td>@product.Price</td>
<td>@product.Quantity</td>
</RowTemplate>
</TableWidget>
}
</div>
您還可以在模板級別指定上下文(Context),如下面的示例所示,其中將 Context="product" 添加到了 RowTemplate。
<TableWidget Title="Top Selling Products" Items="Products">
<HeaderTemplate>
<th scope="col">Title</th>
<th scope="col">Price</th>
<th scope="col">Quantity</th>
</HeaderTemplate>
<RowTemplate Context="product">
<td>@product.Title</td>
<td>@product.Price</td>
<td>@product.Quantity</td>
</RowTemplate>
</TableWidget>
現在如果您運行該項目,您將看到頁面上顯示了以下兩個表格,但是我們知道這次這兩個表格是使用我們的模板化組件 TableWidget 渲染的。該示例清楚地演示了,同一個模板化組件可用於生成不同類型的 UI,並且可以根據我們的應用程序需求渲染不同類型的對象。
下面讓我們通過另外兩個例子重用一下我們的 TableWidget 組件,它們將顯示同樣的最近訂單(Recent Orders)和熱銷產品(Top Selling Products),但布局略有改變。
<div class="row">
<div class="col">
@if (Orders != null)
{
<TableWidget Title="Recent Orders" Items="Orders">
<HeaderTemplate>
<th scope="col" colspan="2">Order Details</th>
<th scope="col">Status</th>
<th scope="col">Total</th>
</HeaderTemplate>
<RowTemplate Context="order">
<td colspan="2">
<b>Order No: </b>@order.OrderNo
<br />
<b>Order Date: </b>@order.OrderDate.ToShortDateString()
</td>
<td>@order.Status</td>
<td>@order.OrderTotal</td>
</RowTemplate>
</TableWidget>
}
</div>
<div class="col">
@if (Products != null)
{
<TableWidget Title="Top Selling Products" Items="Products" TItem=”Product”>
<RowTemplate Context="product">
<td>
<h2>@product.Title</h2>
<h4><b>@product.Price.ToString("C")</b></h4>
</td>
</RowTemplate>
<FooterTemplate>
<td class="text-right"><b>Last 30 Days</b></td>
</FooterTemplate>
</TableWidget>
}
</div>
</div>
在使用泛型類型組件時,會盡可能推斷類型參數。不過,我們可以選擇使用一個特性來顯式指定類型,該特性的名稱與類型參數相同,在上面的示例中是 TItem。
此時如果您運行該項目,您將在頁面上看到使用同一個 TableWidget 模板化組件渲染的全部四個表格。
創建通用模板化組件
我們的 TableWidget 組件很好,我們已見識了重用它的多個示例,但該組件的問題是它只生成了 HTML 表格。如果我們想要創建一個更通用的組件,可以重用它來生成任何類型的 UI(比如:表格、卡片、項目符號等)。我們可以通過從模板化組件中刪除所有的標簽來輕松地創建這樣一個組件。讓我們來創建一個通用的 ListWidget 組件,來實戰練習一下這種組件。
在 Shared 文件夾中創建一個新的 ListWidget.razor 組件,並在其中添加以下代碼。這次在組件中沒有 HTML 標簽,在 foreach 循環中僅有一個 ItemTemplate。這意味着我們可以使用這個 ListWidget 組件自由地生成任意類型的列表。
ListWidget.razor
@typeparam TItem
@foreach (var item in Items)
{
@ItemTemplate(item)
}
@code {
[Parameter]
public RenderFragment<TItem> ItemTemplate { get; set; }
[Parameter]
public IReadOnlyList<TItem> Items { get; set; }
}
假如我們想要使用這個 ListWidget 組件生成 bootstrap 列表,那么我們可以使用下面的代碼段來實現這一操作。
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between align-items-center active">
Latest Products
</li>
<ListWidget Items="Products" Context="product">
<ItemTemplate>
<li class="list-group-item d-flex justify-content-between align-items-center">
@product.Title
<b>@product.Price.ToString("C")</b>
<span class="badge badge-primary badge-pill">
@product.Quantity
</span>
</li>
</ItemTemplate>
</ListWidget>
</ul>
運行該項目,您將看到以 bootstrap 列表方式生成的相同產品的列表。
現在,假設您有另一個頁面,其中需要使用 div 和 a 標簽以不同形式展示產品列表,那么您可以再次重用相同的 ListWidget 組件,這次生成如下標記:
<div class="list-group">
<a class="list-group-item d-flex justify-content-between align-items-center active">
Latest Products
</a>
<ListWidget Items="Products" Context="product" TItem="Product">
<ItemTemplate>
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1"><b>@product.Title</b></h5>
<small class="text-muted">@product.Quantity units left</small>
</div>
<p class="mb-1">@product.Price.ToString("C")</p>
</a>
</ItemTemplate>
</ListWidget>
</div>
運行該項目,您將看到類似以下內容的輸出。
總結
在本教程中,我概述了 Blazor 模板化組件,並創建了兩種類型的模板化組件。然后,我們實踐了幾個重用 TableWidget 和 ListWidget 組件來生成不同類型標記的例子。我不得不承認,模板化組件是 Blazor 開發者工具箱中的一個很好的補充,我們可以使用這些組件創建一些令人驚嘆的可重用組件。
相關閱讀:
- Blazor Server 和 WebAssembly 應用程序入門指南
- Blazor 組件入門指南
- Blazor 數據綁定開發指南
- Blazor 事件處理開發指南
- Blazor 組件之間使用 EventCallback 進行通信
- Blazor 路由及導航開發指南
- Blazor 模板化組件開發指南
- Blazor Server 應用程序中進行 HTTP 請求
- Blazor WebAssembly 應用程序中進行 HTTP 請求
- Blazor 組件庫開發指南
https://www.ezzylearning.net/tutorial/a-developers-guide-to-blazor-templated-components A Developer’s Guide To Blazor Templated Components ↩︎
https://github.com/ezzylearning/BlazorTemplatedComponentDemo 下載源碼 ↩︎
https://www.ezzylearning.net/tutorial/a-step-by-step-guide-to-asp-net-core-dependency-injection A Step by Step Guide to ASP.NET Core Dependency Injection ↩︎