| 节选自《行动中的Blazor》,作者Chris Sainty 这篇文章包括 § 使用模板来定义用户界面的特定区域 § 用泛型加强模板 如果你是一个全栈式的C#和.NET网络开发人员,想了解更多关于Blazor的信息,请阅读这篇文章。 |
在manning.com结账时,在折扣代码框中输入fccsainty,可享受 《Blazor in Action》的25%折扣。
在这篇文章中,我们将把可重用性提高到新的水平。我们将学习如何利用模板和泛型来制作最终的可重用组件。为了给我们一个实际的例子,我们将用一个组件来增强一个叫做Blazing Trails的网站的主页,这个组件允许用户在网格和表格之间切换布局(图1)。
图1显示了Blazing Trails的主页和我们将在本章中建立的最后一个ViewSwitcher 组件。该组件允许用户在可用路径的网格视图和表格视图之间进行切换。
一旦我们建立了我们的ViewSwitcher 组件,我们将通过学习Razor类库(RCL)来结束本章。RCL允许我们将任何通用的组件捆绑起来,并在不同的应用程序中共享它们。这可以通过项目参考来完成,或者RCL可以通过NuGet打包和运送--就像其他的.NET库一样。
定义模板
模板是构建可重用组件时的一个强大工具。它们允许我们指定由消费者提供的标记块,然后我们可以在我们希望的地方输出这些标记。我们在前几章中构建FormSection 和FormFieldSet 组件时已经使用了一些基本的模板。在这些组件中,我们定义了一个类型为RenderFragment 、名称为ChildContent 的参数。
[Parameter] public RenderFragment ChildContent { get; set; }
这是一个特殊的惯例。用这种特殊的名称和类型来定义参数,可以让我们捕捉到在组件的开始和结束标记之间所指定的任何标记。然而,对于我们的ViewSwitcher 组件,我们将需要更高级的东西。
ViewSwitcher 组件允许用户在可用路径的卡片视图和表格视图之间进行切换。为了使这个组件尽可能地可重复使用,我们不希望硬编码网格或表格视图的标记。相反,我们想把这些定义为模板,让组件的消费者自己来定义这些区域。
让我们看一下ViewSwitcher 组件的初始标记。现在,我们将在客户端项目中的功能>首页>共享下创建这个组件。请看下面的清单。
清单1 ViewSwitcher.razor:初始代码
<div>
<div class="mb-3 text-right">
<div class="btn-group">
<button @onclick="@(() =>
[CA]_mode = ViewMode.Grid)" title="Grid View" type="button"
[CA]class="btn @(_mode == ViewMode.Grid ? "btn-secondary"
[CA]: "btn-outline-secondary")"> #A
<i class="bi bi-grid-fill"></i>
</button>
<button @onclick="@(() =>
[CA]_mode = ViewMode.Table)" title="Table View" type="button"
[CA]class="btn @(_mode == ViewMode.Table ? "btn-secondary"
[CA]: "btn-outline-secondary")"> #A
<i class="bi bi-table"></i>
</button>
</div>
</div>
@if (_mode == ViewMode.Grid)
{
@GridTemplate #B
}
else if (_mode == ViewMode.Table)
{
@TableTemplate #C
}
</div>
@code {
private ViewMode _mode = ViewMode.Grid;
[Parameter, EditorRequired]
public RenderFragment GridTemplate { get; set; }
[CA]= default!; #D
[Parameter, EditorRequired]
public RenderFragment TableTemplate { get; set; }
[CA]= default!; #E
private enum ViewMode { Grid, Table } #F
}
#A 这两个按钮允许用户在组件提供的两个视图之间进行切换。
#B 指定消费者为GridTemplate提供的标记应该输出的位置
#C 指定消费者为TableTemplate提供的标记应该在哪里输出
#D定义了GridTemplate参数
#E 定义了TableTemplate参数
#F 该枚举定义了两种视图模式,避免了使用魔法字符串。
该组件以一些标记开始,渲染了两个按钮。这些按钮允许用户在该组件提供的两种视图之间进行切换。为了做到这一点,我们将_mode 的值设置为Grid 或Table 。_mode 字段在代码块中定义,默认为Grid 。按钮还使用一个简单的表达式来应用不同的CSS类,以突出当前激活的模式。
根据哪种模式处于激活状态,该组件渲染代码块中定义的两个模板之一:GridTemplate 或TableTemplate 。模板只是一个参数,其类型为RenderFragment 。
我们还将为该组件添加一些风格设计。我们将添加一个名为ViewSwitcher.razor.scss的新文件并添加以下代码。
清单2 ViewSwitcher.razor.scss
.grid { #A
display: grid;
grid-template-columns: repeat(3, 288px);
grid-column-gap: 123px;
grid-row-gap: 75px;
}
table { #B
width: 100%;
margin-bottom: 1rem;
color: #212529;
border-collapse: collapse;
::deep th, ::deep td {
padding: .75rem;
vertical-align: middle;
}
::deep thead tr th {
border-bottom: 4px solid var(--brand);
border-top: none;
}
::deep tbody tr:nth-of-type(odd) {
background-color: rgba(0,0,0,.05);
}
}
#A 这个类定义了网格视图的样式。
#B 这个类定义了表格视图的样式。
这就是我们现在所需要的一切。让我们跳到HomePage.razor ,实现ViewSwitcher 。然后我们就可以运行这个应用程序,看看一切是什么样子。我们将用下面清单中的代码替换当前渲染路径网格的代码。
清单3 HomePage.razor:使用ViewSwitcher
<ViewSwitcher>
<GridTemplate> #A
<div class="grid">
@foreach (var trail in _trails)
{
<TrailCard Trail="trail" OnSelected="HandleTrailSelected" />
}
</div>
</GridTemplate>
<TableTemplate> #B
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Location</th>
<th>Length</th>
<th>Time</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var trail in _trails)
{
<tr>
<th scope="col">@trail.Name</th>
<td>@trail.Location</td>
<td>@(trail.Length)km</td>
<td>@trail.TimeFormatted</td>
<td class="text-right">
<button @onclick="@(() =>
[CA]HandleTrailSelected(trail))" title="View" class="btn btn-primary">
<i class="bi bi-binoculars"></i>
</button>
<button @onclick="@(() => NavManager
[CA].NavigateTo($"/edit-trail/{trail.Id}"))" title="Edit"
[CA]class="btn btn-outline-secondary">
<i class="bi bi-pencil"></i>
</button>
</td>
</tr>
}
</tbody>
</table>
</TableTemplate>
</ViewSwitcher>
#A定义了GridTemplate的标记
#B定义了TableTemplate的标记
为了指定一个特定模板的标记,我们定义与参数名称相匹配的子元素。在我们的例子中,就是GridTemplate 和TableTemplate 。我们上面为GridTemplate 和TableTemplate 定义的标记将由ViewSwitcher 输出,其中我们指定了@GridTemplate 和@TableTemplate 的表达方式。
我们现在可以运行这个应用程序,看看一切是什么样子。图2显示了两个视图的并排对比。
图2显示了由ViewSwitcher 组件提供的网格和表格视图
这下好了!我们现在有了这个组件的初始版本。接下来,我们要把泛型引入到ViewSwitcher 。
用泛型加强模板
目前,我们的组件运行良好。它允许我们为表格和网格视图定义标记,并允许用户在它们之间进行切换。然而,我认为我们可以改进一些东西。现在,当我们使用该组件时,我们必须在HomePage 中定义大量的标记。我们在网格模板中围绕一个foreach 块定义了一个类别为.grid 的div。然后对于表格模板,我们要为表格提供整个标记。
因为我们知道我们将显示一个网格或一个表格,我们可以把一些模板标记烘托到组件中。然后,当我们使用该组件时,我们只需要指定特定用途的标记和数据。为了做到这一点,我们将在我们的ViewSwitcher 组件中引入泛型。下面的清单显示了更新后的代码。
清单4 ViewSwitcher.razor:更新为使用泛型。
@typeparam TItem #A
// code omitted
@if (_mode == ViewMode.Grid)
{
<div class="grid">
@foreach (var item in Items)
{
@GridTemplate(item) #B
}
</div>
}
else if (_mode == ViewMode.Table)
{
<table>
<thead> #B
<tr> #B
@HeaderTemplate #B
</tr> #B
</thead> #B
<tbody>
@foreach (var item in Items)
{
<tr>
@RowTemplate(item) #C
</tr>
}
</tbody>
</table>
}
// code omitted
@code {
private ViewMode _mode = ViewMode.Grid;
[Parameter, EditorRequired]
public IEnumerable<TItem> Items { get; set; }
[CA]= default!; #D
[Parameter, EditorRequired]
public RenderFragment<TItem> GridTemplate { get;
[CA]set; } = default!; #C
[Parameter, EditorRequired]
public RenderFragment HeaderTemplate { get; set; }
[CA]= default!; #B
[Parameter, EditorRequired]
public RenderFragment<TItem> RowTemplate { get;
[CA]set; } = default!; #C
// code omitted
}
#A 使用tyeparam指令来指定一个类型参数。
#B 我们现在只要求在使用该组件时指定表头单元格,而不是为表头的所有标记。
#C 用一个类型参数来定义RenderFragments,允许消费者在定义模板时使用该类型的属性。
#D 该组件现在接受了一个要显示的项目的列表。
我们首先为组件引入一个类型参数。我们使用@typeparam 指令来做到这一点。一旦我们这样做了,我们就可以在代码块中定义模板参数的时候引用这个类型参数。我们现在说明,GridTemplate 和RowTemplate 将包含类型为TItem 的项目。当我们在标记部分调用这些RenderFragments ,我们可以传入一个类型为TItem 的对象。这些项目来自我们创建的新的Items 参数。我们稍后会看到这个的好处,当我们更新HomePage ,但通过用一个类型定义我们的模板参数,我们将能够在定义模板时访问该类型的属性。
让我们去更新HomePage ,以配合我们对ViewSwitcher 所做的修改。HomePage.razor 的更新代码显示在下面的清单中。
清单5 HomePage.razor。替换现有的ViewSwitcher代码
<ViewSwitcher Items="_trails"> #A
<GridTemplate> #B
<TrailCard Trail="context"
[CA]OnSelected="HandleTrailSelected" /> #C
</GridTemplate>
<HeaderTemplate> #D
<th>Name</th>
<th>Location</th>
<th>Length</th>
<th>Time</th>
<th></th>
</HeaderTemplate>
<RowTemplate>
<th scope="col">@context.Name</th> #C
<td>@context.Location</td> #C
<td>@(context.Length)km</td> #C
<td>@context.TimeFormatted</td> #C
<td class="text-right">
<button @onclick="@(() =>
[CA]HandleTrailSelected(context))" title="View"
[CA]class="btn btn-primary"> #C
<i class="bi bi-binoculars"></i>
</button>
<button @onclick="@(() =>
[CA]NavManager.NavigateTo($"/edit-trail/{context.Id}"))"
[CA]title="Edit" class="btn btn-outline-secondary"> #C
<i class="bi bi-pencil"></i>
</button>
</td>
</RowTemplate>
</ViewSwitcher>
#A 轨迹列表现在被传递到ViewSwitcher中,而不是在模板中定义foreach循环。
#B GridTemplate现在更干净了,因为我们不再需要定义网格和一个foreach循环。
#C 在使用RenderFragment的模板中,我们现在可以通过一个名为context的变量访问对象的属性。这使得我们在构建标记时有了很大的灵活性。
#D 头部模板允许我们定义表格所需的列,但没有之前的所有模板。
径的列表现在通过Items 参数传入ViewSwitcher 。这意味着我们不再需要像以前那样,担心在各种模板中定义foreach 循环。这使GridTemplate 整洁了许多。我们现在只需要为一个单独的项目定义标记。
由于GridTemplate 被定义为RenderFragment<T> ,我们可以在我们的模板中访问T 的任何属性。我们通过一个名为context 的特殊参数访问这些属性。由于TrailCard 组件需要一个Trail 的实例,我们可以直接将context 传递给Trail 参数。RowTemplate 在更大程度上显示了访问T 的属性。
我们做的另一个改变是添加了一个HeaderTemplate ,这样我们就可以定义我们的表格的列,而不需要之前所有的额外的模板标记。正如你所看到的,我们现在只需要定义各个单元格。这大大减少了我们需要编写的代码量。
这看起来很好,但我们还可以做一个小小的改进,以帮助我们的代码的可读性--context 参数。如果我们在一个组件上扫描,我们将不得不停顿一下,以了解在这种情况下上下文的含义。在我们的例子中,context 是一个Trail 。如果它只是被称为trail ,那不是很好吗?我想是的。而好消息是,我们可以给它起任何我们喜欢的名字!下面的清单显示了ViewSwitcher 上的HomePage ,并重新命名了上下文参数。
清单6 HomePage.razor。重命名上下文变量
<ViewSwitcher Items="_trails">
<GridTemplate Context="trail"> #A
<TrailCard Trail="trail"
[CA]OnSelected="HandleTrailSelected" /> #B
</GridTemplate>
<HeaderTemplate>
<th>Name</th>
<th>Location</th>
<th>Length</th>
<th>Time</th>
<th></th>
</HeaderTemplate>
<RowTemplate Context="trail"> #A
<th scope="col">@trail.Name</th>
<td>@trail.Location</td> #B
<td>@(trail.Length)km</td> #B
<td>@trail.TimeFormatted</td> #B
<td class="text-right">
<button @onclick="@(() =>
[CA]HandleTrailSelected(trail))" title="View"
[CA]class="btn btn-primary"> #B
<i class="bi bi-binoculars"></i>
</button>
<button @onclick="@(() =>
[CA]NavManager.NavigateTo($"/edit-trail/{trail.Id}"))"
[CA]title="Edit" class="btn btn-outline-secondary"> #B
<i class="bi bi-pencil"></i>
</button>
</td>
</RowTemplate>
</ViewSwitcher>
#A 上下文参数可以使用Context属性进行重命名。
#B 一旦重命名,新的名字就可以在模板中用来指代该对象。
我们可以使用模板上的Context 属性来重命名context 参数。这只有在模板被定义为RenderFragment<T> 时才可用。一旦重命名,新的名称就可以用来指代模板中显示的对象。正如你所看到的,这使得代码的可读性大大增强,更容易理解,一目了然。
我们可以再往前走一步。我们可以在组件层重命名context 参数,所有的模板将自动继承这个名字。请看下面的清单。
清单7 HomePage.razor。在组件级重命名上下文
<ViewSwitcher Items="_trails" Context="trail"> #A
<GridTemplate>
<TrailCard Trail="trail"
[CA]OnSelected="HandleTrailSelected" /> #B
</GridTemplate>
<HeaderTemplate>
<th>Name</th>
<th>Location</th>
<th>Length</th>
<th>Time</th>
<th></th>
</HeaderTemplate>
<RowTemplate>
<th scope="col">@trail.Name</th>
<td>@trail.Location</td> #B
<td>@(trail.Length)km</td> #B
<td>@trail.TimeFormatted</td> #B
<td class="text-right">
<button @onclick="@(() =>
[CA]HandleTrailSelected(trail))" title="View"
[CA]class="btn btn-primary"> #B
<i class="bi bi-binoculars"></i>
</button>
<button @onclick="@(() =>
[CA]NavManager.NavigateTo($"/edit-trail/{trail.Id}"))"
[CA]title="Edit" class="btn btn-outline-secondary"> #B
<i class="bi bi-pencil"></i>
</button>
</td>
</RowTemplate>
</ViewSwitcher>
#A 上下文参数在组件级被重命名。
#B 一旦重命名,新的名字就可以在模板中用来指代该对象。
通过在组件级重命名context 参数,我们可以从每个模板中删除单独的名字。
这篇文章就写到这里。谢谢你的阅读。
The postLeveraging Templates to Make Reusable Componentsappeared first onManning.
