Making components generic
A generic class in C# is a class that’s defined with a placeholder type, allowing it to operate with any data type. This flexibility enables the creation of a single class that can adapt its behavior to a variety of data types, enhancing code reusability and efficiency. Generic components in Blazor applications are a similar concept. These components are highly reusable across different contexts and data types. They abstract away specific details, allowing high adaptability to various data or functionalities with minimal changes. This approach significantly reduces code duplication. With that flexibility, you can achieve even higher delivery velocity. The most common scenario where you’ll see generic components shine is repetitive data display, especially grids.
Let’s create a generic Grid
component that can render objects of any type by using the provided row template.
Getting ready
Before you start implementing the generic grid, do the following:
- Create a
Recipe08
directory – this will be your working directory - Copy the
Chapter01
/Data
directory, which contains theSamples
andTicketViewModel
objects required for this recipe, next to the working directory
How to do it...
Follow these steps to build and use your generic component:
- Create a
Grid
component. At the top of the file, declare it as generic with the@
typeparam
attribute:@typeparam T
- In the
@code
block of theGrid
component, declare parameters for data source and table area customization. The source and row template must be generic:@code { [Parameter, EditorRequired] public IList<T> Data { get; set; } [Parameter, EditorRequired] public RenderFragment Header { get; set; } [Parameter, EditorRequired] public RenderFragment<T> Row { get; set; } }
- For the
Grid
markup, add a standard HTML table with theHeader
content rendered where the table header is. For the table body, iterate overData
and render theRow
template for each element:<table class="grid"> <thead> @Header </thead> <tbody> @foreach (var item in Data) @Row(item) </tbody> </table>
- Create a routable
Offer
component that renders inInteractiveWebAssembly
mode and uses theSamples
assembly so thatSamples
can be referenced later:@page "/ch01r08" @using BlazorCookbook.App.Client.Chapters.Chapter01.Data @rendermode InteractiveWebAssembly
- In the
@code
block ofOffer
, implement anAdd()
placeholder method that writes a simple action confirmation toConsole
:public void Add(TicketViewModel ticket) => Console.WriteLine($"Ticket {ticket.Id} added!");
- In the markup of the
Offer
component, use theGrid
component and pass inSamples.Tickets
as the data source forGrid
:<Grid Data="@Samples.Tickets"> @* you will add areas here *@ </Grid>
- Implement the required
Header
area inside theGrid
instance in theOffer
markup:<Header> <tr> <td>Ticket code</td> <td>Tariff</td> <td>Price</td> <td></td> </tr> </Header>
- Inside the
Grid
instance, in theOffer
markup, implement the requiredRow
template so that elements of theTicketViewModel
type can be rendered:<Row> <tr> <td>@context.Id</td> <td>@context.Tariff</td> <td>@context.Price</td> <td @onclick="() => Add(context)"> Add to Cart </td> </tr> </Row>
How it works...
We started this recipe by implementing the foundation for creating a generic component. In step 1, we created a Grid
component and added the @typeparam
attribute at the top. We also specified the name for the parameter type placeholder – much like you would in backend development. We chose to call it T
. Blazor recognized @typeparam
and now allows us to operate on T
inside the component. The IDE will also apply all validations that generic modules require. In step 2, we implemented the @code
block of the Grid
component by adding a Data
parameter that will hold elements to render and two RenderFragment
parameters, enabling Grid
customization. You can learn more about RenderFragment
in the Creating components with customizable content section. Notably, the Data
collection isn’t the only generic object. The Row
parameter, which contains a row template, is also generic, which means it will expect a data object of type T
for initialization. In step 3, we implemented the Grid
markup. We rendered the Header
value inside the <thead>
tags, where the table header normally goes; for the table body, we used a foreach
loop to iterate over the Data
collection and rendered the Row
template for each element.
In step 4, we created a routable Offer
component to test our grid. As we expected interactivity, we declared that Offer
rendered in InteractiveWebAssembly
mode. We also leveraged the Samples
object, so we exposed the required assembly with the @using
directive. In step 5, we implemented an Add()
placeholder method within the @code
block of the Offer
component to test the Grid
component’s interactivity. In step 6, we started implementing the Offer
markup. We embedded the Grid
component and passed the Samples.Tickets
array as the value of the Data
parameter. In step 7, we declared the content of Header
, which in our case is a set of columns representing TicketViewModel
properties and an additional column where we placed action buttons. The real rendering magic happened in step 8. As the Row
template expects a TicketViewModel
object, we can access TicketViewModel
properties in the markup with a @context
directive and place them in table columns matching the Header
declaration.
There’s more...
The power of the generic component lies in its agnosticism to the data type you’ll use. It simply knows how to construct a template, and where to place customizable content. It’s up to you to define markup to present data properties.
You might find yourself in need of nesting multiple generic components. To do so, you’ll have to define all required RenderFragment
parameters. However, a key challenge here is going to be distinguishing each generic context. In that case, you must assign custom names to the context of each generic component using the Context
parameter. This parameter is inherited automatically, streamlining the process and enhancing the readability of your code.
Even though our example doesn’t require nesting, we can still leverage the Context
naming feature to enhance the code’s readability:
<Grid Data="@Data.Tickets" Context="ticket"> ... <Row> <tr> <td>@ticket.Id</td> ... * </tr> </Row> </Grid>
Remember that the more intuitive your code is, the easier it is to navigate and update, especially when you’re working in team environments or returning to the code after some time.