如何利用模板来制作可重复使用的组件

158 阅读6分钟

Description: https://images.manning.com/360/480/resize/book/f/0f647f3-e049-4e1c-a292-1645c06c7c08/Sainty-Blazor-MEAP.png

节选自《行动中的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库一样。

定义模板

模板是构建可重用组件时的一个强大工具。它们允许我们指定由消费者提供的标记块,然后我们可以在我们希望的地方输出这些标记。我们在前几章中构建FormSectionFormFieldSet 组件时已经使用了一些基本的模板。在这些组件中,我们定义了一个类型为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 的值设置为GridTable_mode 字段在代码块中定义,默认为Grid 。按钮还使用一个简单的表达式来应用不同的CSS类,以突出当前激活的模式。

根据哪种模式处于激活状态,该组件渲染代码块中定义的两个模板之一:GridTemplateTableTemplate 。模板只是一个参数,其类型为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的标记

为了指定一个特定模板的标记,我们定义与参数名称相匹配的子元素。在我们的例子中,就是GridTemplateTableTemplate 。我们上面为GridTemplateTableTemplate 定义的标记将由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 指令来做到这一点。一旦我们这样做了,我们就可以在代码块中定义模板参数的时候引用这个类型参数。我们现在说明,GridTemplateRowTemplate 将包含类型为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.