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

An excerpt from Blazor in Action by Chris Sainty

This article covers

§   Using templates to define specific regions of UI

§   Enhancing templates with generics

Read it if you’re a full-stack C# and .NET web developer who wants to learn more about Blazor.


Take 25% off Blazor in Action by entering fccsainty into the discount code box at checkout at manning.com.


In this article, we’re going to take reusability to the next level. We’ll learn how to leverage templates and generics to make the ultimate reusable components. To give us a practical example, we’ll be enhancing the home page of a website called Blazing Trails with a component that allows the user to toggle the layout between a grid and a table (figure 1).


Figure 1 Shows the home page of Blazing Trails with the final ViewSwitcher component we’ll be building during this chapter. The component allows the user to toggle between a grid view and a table view of the available trails.


Once we’ve built our ViewSwitcher component, we’ll finish the chapter by learning about Razor class libraries (RCL). RCLs allow us to bundle up any common components and share them across applications. This can be done via a project reference, or RCLs can be packed and shipped via NuGet—just like any other .NET library.

Defining templates

Templates are a powerful tool when building reusable components. They allow us to specify chunks of markup to be provided by the consumer, which we can then output wherever we wish. We have already used some basic templating when we built the FormSection and FormFieldSet components in the previous chapters. In those components, we defined a parameter with a type of RenderFragment and a name of ChildContent.

 
 [Parameter] public RenderFragment ChildContent { get; set; }
  

This is a special convention. Defining a parameter with this specific name and type allows us to capture whatever markup has been specified between the start and end tags of the component. However, for our ViewSwitcher component, we’re going to need something a little more advanced.

The ViewSwitcher component allows the user to toggle between a card view and a table view of the available trails. To make this component as reusable as possible, we don’t want to hardcode the markup for either the grid or the table view. Instead, we want to define these as templates that allow the consumer of the component to define these areas for themselves.

Let’s look at the initial markup for the ViewSwitcher component. For now, we will create this component in the Client project under Features > Home > Shared. See the following listing.

Listing 1 ViewSwitcher.razor: Initial code

 
 <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 The two buttons allow the user to toggle between the two views offered by the component.

#B Specifies where the markup provided by the consumer for the GridTemplate should be output

#C Specifies where the markup provided by the consumer for the TableTemplate should be output

#D Defines the GridTemplate parameter

#E Defines the TableTemplate parameter

#F The enum defines the two view modes and avoids using magic strings.

The component starts with some markup that renders two buttons. These buttons allow the user to toggle between the two views offered by the component. To do this, we’re setting the value of _mode to either Grid or Table. The _mode field is defined in the code block and defaulted to Grid. The buttons also use a simple expression to apply different CSS classes to highlight which of the modes is currently active.

Depending on which mode is active, the component renders one of two templates defined in the code block, GridTemplate or TableTemplate. A template is just a parameter with a type of RenderFragment.

We’re also going to add some styling for the component. We’ll add a new file called ViewSwitcher.razor.scss and add the following code.

Listing 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 This class defines the styling for the grid view.

#B This class defines the styling for the table view.

That is all we need for now. Let’s jump over to HomePage.razor and implement ViewSwitcher. Then we can run the app and see what everything looks like. We’re going to replace the current code that renders the grid of trails with the code shown in the following listing.

Listing 3 HomePage.razor: Using 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 Defines the markup for the GridTemplate

#B Defines the markup for the TableTemplate

To specify the markup for a particular template, we define child elements that match the name of the parameter. In our case, that is GridTemplate and TableTemplate. The markup we’ve defined above for the GridTemplate and TableTemplate will be output by ViewSwitcher where we specified the @GridTemplate and @TableTemplate expressions.

We can now run the app and see what everything looks like. Figure 2 shows a side-by-side comparison of the two views.


Figure 2 Shows the grid and table views offered by the ViewSwitcher component


That’s great! We now have the initial version of the component in place. Next, we’re going to introduce generics to ViewSwitcher.

Enhancing templates with generics

Currently, our component is working well. It allows us to define the markup for the table and grid views and for the user to toggle between them. However, I think we can improve things a bit. Right now, we must define a lot of markup in the HomePage when we’re using the component. We’re defining a div with a class of .grid around a foreach block in the grid template. Then for the table template, we’re providing the entire markup for the table.

As we know we’re going to be displaying a grid or a table, we can bake some of the boilerplate markup into the component. Then when we use the component, we only have to specify the markup and data specific to that usage. To do this, we will introduce generics into our ViewSwitcher component. The following listing shows the updated code.

Listing 4 ViewSwitcher.razor: Updated to use generics

 
 @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 A type parameter is specified using the typeparam directive.

#B We now only require the header cells to be specified when using the component, rather than all the markup for the head of the table.

#C Defining RenderFragments with a type parameter allows the consumer to use properties of that type when defining a template.

#D The component now accepts a list of items to be displayed.

We start by introducing a type parameter to the component. We do this using the @typeparam directive. Once we do this, we can reference the type parameter when defining our template parameters in the code block. We’re now stating that the GridTemplate and RowTemplate will contain items of type TItem. When we invoke these RenderFragments in the markup section, we can pass in an object of type TItem. These items are coming from the new Items parameter we’ve created. We’ll see the benefit of this in more detail in a second, when we update the HomePage, but by defining our template parameters with a type, we’ll be able to access properties of that type when defining the template.

Let’s go and update the HomePage to work with the changes we’ve made to ViewSwitcher. The updated code for HomePage.razor is shown in the following listing.

Listing 5 HomePage.razor: Replace existing ViewSwitcher code

 
 <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 The list of trails is now passed into the ViewSwitcher rather than having to define foreach loops in the templates.

#B The GridTemplate is now cleaner, as we no longer need to define the grid and a foreach loop.

#C In the template that uses RenderFragment<T>, we can now access properties of the object through a variable called context. This allows loads of flexibility when building our markup.

#D The header template allows us to define the columns our table needs, but without all the boilerplate we had before.

The list of trails is now passed into the ViewSwitcher via the Items parameter. This means we no longer need to worry about defining foreach loops in various templates, like before. This has tidied up the GridTemplate a lot. We only need to define the markup for an individual item now.

As the GridTemplate is defined as RenderFragment<T>, we can access any properties of T in our template. We access these via a special parameter called context. As the TrailCard component needs an instance of a Trail, we can just pass context to the Trail parameter. The RowTemplate shows accessing properties of T to an even greater extent.

The other change we made was to add in a HeaderTemplate so we could define the columns of our table without all the extra boilerplate markup we had before. As you can see, we only need to define the individual cells now. This reduces the amount of code we need to write considerably.

This is looking great, but there is one other small improvement we can make to help the readability of our code—the context parameter. If we were scanning over a component, we would have to pause for a second to understand what context meant in this scenario. In our case, context is a Trail. Wouldn’t it be great if it were just called trail instead? I think so. And the great news is, we can name it whatever we like! The following listing shows the ViewSwitcher on the HomePage with a renamed context parameter.

Listing 6 HomePage.razor: Rename context variable

 
 <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 The context parameter can be renamed using the Context attribute.

#B Once renamed, the new name can be used within the template to refer to the object.

We can rename the context parameter using the Context attribute on a template. This is available only when the template is defined as RenderFragment<T>. Once renamed, the new name can be used to refer to the object being displayed in the template. As you can see, this has made the code far more readable and easier to understand at a glance.

We can take this one step further. We can rename the context parameter at the component level, and all the templates will automatically inherit the name. See the following listing.

Listing 7 HomePage.razor: Rename context at the component level

 
 <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 The context parameter is renamed at the component level.

#B Once renamed, the new name can be used within the template to refer to the object.

By renaming the context parameter at the component level, we can remove the individual names from each template.

That’s all for this article. Thanks for reading.