Some components will be specific to your application, and others will not be. Think about the table of two CRUD operations that you created for the article-manager application in the previous chapter, or a Bootstrap modal for your front-end; also, some form component can be specific or not, such as an advanced select control or date picker.
In this chapter, I show you how to extract components from the project to a reusable library so you can potentially use them in different projects. Following this approach, your next project will start from a collection of your own ready-to-use component libraries.
You can extract a component from a project to put it in a library, or you can create a component directly in a library and use it in a project. In the first case, you probably need to generalize the component; in the second case, you need to design it outside of the specific use, profiting from the parametrization and principles learned in the previous chapter. You can also choose to create a component library to simplify the front-end. In a large project, this helps you to divide the job among different members of a team and to create a more maintainable project structure.
Many large companies and independent developers are creating generic components to add to the Blazor ecosystem. You can choose their libraries, creating a dependency on them, or you can create your own libraries. There are pros and cons in both cases, but if you know how to build a library, then you can decide whether to create one or pick a ready-made one.
While extracting a component from a library, you will see some advanced features of the Blazor framework that are available for both the Server and WebAssembly version. Some of these features are useful but could complicate your codebase. The rule is always the same: follow the single responsibility principle and try to create value for your project and customer.
Creating a Component Library
The first step is to create a component library. The .NET CLI provides a template to create a Razor class library, which is perfect for us: launch the command dotnet new razorclasslib -o frontendlib in the root folder of the article-manager project. The -o option specifies the output of the command and creates a folder named frontendlib with the project inside it. Now we can go inside the frontendlib folder and add the library to the project with the dotnet add reference ../frontendlib command.
The razorclasslib template creates a sample component, with an example of the JavaScript interoperability with Blazor, and a wwwroot folder that contains static files. You do not need these files, so delete all of them except for the wwwroot folder and the _Imports.razor file.
Let’s begin with the List component to generalize the entity list visualization. Our goal is to reuse the interface that lists one entity (for example, the article category) for each entity of your application. If you analyze the code of ArticleCategoryList, you can see that there is a simple HTML table with a fixed-column definition and a loop on an array of ArticleCategoryListItem. For the columns, you could use a parameter like a simple array of strings that contains the column headers; for the array, you can use a .NET object or a .NET generic. Let’s start with a .NET object.
Create two folders,
Components and
Models
, to contain the component files and the model classes to support them. In the
Models folder, create a class to collect all the parameters for the List component, which simplifies the use of the component and its evolution with the creation of a unique parameter. In Listing
4-1, you can see an example of this class, named
ItemListModel, that contains a string with the name of the entity, a collection of headers, and an array of objects.
public class ItemListModel
{
public string ItemName { get; set; }
public string[] Headers { get; set; }
public object[] Items { get; set; }
}
Listing 4-1The List Component Model Class Definition
At this point, you can create a new component in the
Components folder, called
ItemList.razor, in which you will copy the
ArticleCategoryList code and define a parameter of type
ItemListModel in place of the category array. Now, you need to edit the markup as in Listing
4-2 to create the table headers based on the
ItemListModel headers, assuming that the collection is ordered based on the visualization preferences.
<table>
<thead>
<tr>
<th></th>
@foreach (var header in Model.Headers)
{
<th>@header</th>
}
<th></th>
</tr>
</thead>
Listing 4-2Extracting the List Component That Renders the Table Headers
Regarding the row, you can use .NET Reflection to inspect the object type and retrieve the properties, from which you can extract the values (Listing
4-3).
<tbody>
@foreach (var item in Model.Items)
{
<tr>
<td><button class="btn btn-warning" @onclick="e => OnEditClick.InvokeAsync(item)">Edit</button></td>
@foreach(var property in item.GetType().GetProperties())
{
<td>@property.GetValue(item)</td>
}
<td><button class="btn btn-danger" @onclick="e => ShowConfirm(item)">Delete</button></td>
</tr>
}
</tbody>
</table>
Listing 4-3Extracting the List Component That Renders the Table Rows
In Chapter , I added the code to manage the user confirmation during the delete operation, by using the Bootstrap modal and taking advantage of the Blazor JavaScript interoperability functionality to open and close the modal with the jQuery functions. You can do the same in the component library: adding a JavaScript file in the wwwroot folder of the library and naming it, for example, frontendlib.js. You can copy the showConfirmDelete and hideConfirmDelete functions from the index.html file (the library compilation adds this file in the DLL). You can reference this file by appending the path _content/<DLL name>/<filename> in the index.html script. In this case, the reference is <script src="_content/frontendlib/frontendlib.js"> </script>.
This new component permits you to delete the article and article categories components on the front-end, and it allows you to create the list visualization for any entities of your application (Listing
4-4).
@inherits ArticleCategoriesBase
@page "/articlecategories"
<h2>Article Categories</h2>
<div class="mt-3">
@if(categoryModel.Item == null)
{
<ItemList
Model="categoriesModel"
OnAddClick="AddCategory"
OnEditClick="EditCategory"
OnDeleteClick="DeleteCategory">
</ItemList>
}
else { ... }
</div>
Listing 4-4Extracting the ArticleCategories Page That Shows the Use of the New ItemList Component
Creating a Templated Component
There are a few occasions when using parameters can be too complex to generalize the content of a component. Moreover, you may need to show a piece of markup specified by the parent component to provide maximum flexibility for the user of your library. Blazor offers the ability to project markup into a component
, creating parameters of RenderFragment type. Components that use parameters of RenderFragment type, are called templated components, allowing the use of one or more templates in them.
This ability is the perfect way to create a container component, where the specific markup is always the same. Check out the application details components called Article.razor and ArticleCategory.razor. Both of these components use different fields inside the EditForm, but DataAnnotationValidator, ValidationSummary, and the submit and cancel buttons are the same. You could create a model and use .NET Reflection to generate the fields like in the List component, but in my experience, the autogenerated details forms work fine for the user of the library only in simple cases. A templated component provides significant flexibility, and Blazor provides a simple way to implement them.
Let’s create an
ItemDetails.razor file in the
Components folder of the components library and use the code in Listing
4-5. The parameter
FieldsTemplate receives the markup that Blazor places at the
@FieldTemplate position. You are not limited to one parameter of type
RenderFragment, so you can make more parts of your component replaceable with custom markup using the father component.
<EditForm Model="@Model.Item" OnValidSubmit="@(e => OnSaveClick.InvokeAsync(Model.Item))">
<DataAnnotationsValidator />
<ValidationSummary />
@FieldsTemplate
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-warning" @onclick="OnCancelClick">Cancel</button>
</EditForm>
@code {
[Parameter]
public RenderFragment FieldsTemplate { get; set; }
[Parameter]
public ItemDetailsModel Model { get; set; }
...
}
Listing 4-5Extracting the Details Component that uses a template definition
In Listing
4-6, you can see how to use the component. Between the opening and closing
ItemDetails tags, you can create a new element with the name of the parameter, in this case
<FieldsTemplate>. You can put whatever you want in this parameter. Blazor projects the content of this element into the component
ItemDetails. If you have more than one
RenderFragment parameter, you can create more elements with the respective names in the
ItemDetails elements.
<ItemDetails
ItemType="ArticleCategoryItem"
Model="categoryModel"
OnSaveClick="SaveCategory"
OnCancelClick="ShowList">
<FieldsTemplate>
<!-- place here your markup -->
</FieldsTemplate>
</ItemDetails>
Listing 4-6Using the Details Component
This is a fantastic feature that allows you to go more in-depth with the generalization of a component. But Blazor can do more.
Creating a Generic Component
If the content of a project needs to access some data of a component, you can use the generic version
of RenderFragment and pass to it an instance of the generic type. In our case, we need to pass the model of the details form to the RenderFragment, so we create a specific type called ItemDetailsModel, and then we can use it as the generic type for the RenderFragment.
However, we cannot use the type
Object for the item, like we did for the item array of the List component, because the binding of the form elements requires us to know the item fields. For example, if we have to bind the field
Name of the
Category with an
InputText component, we must have access to the field, and an
Object does not allow this. Moreover, in the component, we do not know that the object is a category because it must work with any entity of the project. The best way to solve this problem in the .NET Framework is to use a generic type in the definition, which means creating a generic
ItemDetailsModel (Listing
4-7).
public class ItemDetailsModel<TItem>
{
public string ItemName { get; set; }
public TItem Item { get; set; }
}
Listing 4-7Defining the Generic Item Details Model
We can, therefore, kill two birds with one stone and take advantage of another peculiar characteristic of the Blazor components: the generic components. Still, thanks to the
@typeparam directive, we can create an
ItemType and use it as a generic type everywhere in the component and then, in the
ItemDetailsModel and
RenderFragment too, obtain the maximum possible generalization (Listing
4-8).
@typeparam ItemType
<EditForm Model="@Model.Item" OnValidSubmit="@(e => OnSaveClick.InvokeAsync(Model.Item))">
<DataAnnotationsValidator />
<ValidationSummary />
@FieldsTemplate(Model.Item)
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-warning" @onclick="OnCancelClick">Cancel</button>
</EditForm>
@code {
[Parameter]
public RenderFragment<ItemType> FieldsTemplate { get; set; }
[Parameter]
public ItemDetailsModel<ItemType> Model { get; set; }
...
}
Listing 4-8Defining the Item Details Component with a Generic Type
When you use a generic component, you must specify the concrete type, using the name of the generic type name as a parameter. In this case, we called the generic type
ItemType (
@typeparam ItemType), so, for example, in the
ArticleCategory component, we use the
ItemDetails component with the
ItemType parameter set to
ArticleCategoryItem (Listing
4-9).
<ItemDetails
ItemType="ArticleCategoryItem"
Model="categoryModel"
OnSaveClick="SaveCategory"
OnCancelClick="ShowList">
<FieldsTemplate Context="Category">
<div class="form-group">
<label for="name">Name: </label>
<InputText id="name" @bind-Value="Category.Name" class="form-control" />
<ValidationMessage For="@(() => Category.Name)" />
</div>
<div class="form-group">
<label for="description">Description: </label>
<InputTextArea id="description" @bind-Value="Category.Description" class="form-control" />
</div>
</FieldsTemplate>
</ItemDetails>
Listing 4-9Using the Item Details Component in the ArticleCategories Page
We can access the RenderFragment context by specifying the Context parameter, as shown in Listing 4-9, where we set the Context of the FieldsTemplate to Category. So, the word Category represents the instance of the item passed to the RenderFragment (@FieldsTemplate(Model.Item)).
Using a specific context makes the code clearer, but it is not mandatory: you could use the reserved word context. For example, in Listing 4-9, you can omit Context="Category" and use @bind-Value="context.Name" in the InputText component. In the code provided with the book, I use both approaches as possible examples of use.
Creating Custom Input Components
Another good idea to simplify and make your code more maintainable is to customize the collection of the input components. Taking a look at the article category and article details forms, you will note that there is a lot of repeated code, such as the bootstrap layout structure and the parameters passed to the Blazor form components. If you need to change the layout or the way you display a single field, you must change all this code. Using a custom input component, you can create your UI components library and reuse it in all your projects.
An input component inherits from the
InputBase class, which accepts a generic argument to specify the type of value managed. In many cases, the value managed is a string, like for the
InputText and
InputTextArea. In Listing
4-10, you can see the markup and the code to generalize the use of an
InputText. You can create a component named
FieldInputText and show the label for the input only if the user provides the value.
@inherits InputBase<string>
<div class="form-group">
@if (!string.IsNullOrWhiteSpace(Label))
{
<label for="@Id">@Label: </label>
}
<InputText id="@Id" @bind-Value="@CurrentValue" class="form-control" />
<ValidationMessage For="@Validation" />
</div>
@code
{
[Parameter] public string Id { get; set; }
[Parameter] public string Label { get; set; }
[Parameter] public Expression<Func<string>> Validation { get; set; }
protected override bool TryParseValueFromString(string value, out string result, out string validationErrorMessage)
{
result = value;
validationErrorMessage = null;
return true;
}
}
Listing 4-10The Custom Input Text Component Definition
The
Input base abstract class requires us to implement the
TryParseValueFromString method
because, in case our input manages a value of a type different from the string, we must provide the correct conversion from the string value. The current value is available in the
@CurrentValue property of the base class, which is the same type of the generic for the class (in our case a string). You can do the same work for the
InputTextArea and use it and the
InputText component to simplify the article category
page (Listing
4-11).
<ItemDetails ...>
<FieldsTemplate Context="Category">
<FieldInputText
Id="name" Label="Name"
@bind-Value="Category.Name"
Validation="@(() => Category.Name)" />
<FieldInputTextArea
Id="description" Label="Description"
@bind-Value="Category.Description"
Validation="@(() => Category.Description)" />
</FieldsTemplate>
</ItemDetails>
Listing 4-11Using the Custom Input Components
If the value is always a string and the component parameters are always the same (
Id,
Label, and
Validation), we can create a base class that inherits from the
InputBase to collect the parameters and implement the conversion method. We can name this class
FieldInputBase and use it to simplify the specific component code (Listing
4-12).
public abstract class FieldInputBase : InputBase<string>
{
[Parameter] public string Id { get; set; }
[Parameter] public string Label { get; set; }
[Parameter] public Expression<Func<string>> Validation { get; set; }
protected override bool TryParseValueFromString(string value, out string result, out string validationErrorMessage)
{
result = value;
validationErrorMessage = null;
return true;
}
}
Listing 4-12The Base Class Definition for the Custom Input Text Components
Thanks to this class, in many cases we only need to create the specific markup, as shown in Listing
4-13.
@inherits FieldInputBase
<div class="form-group">
@if (!string.IsNullOrWhiteSpace(Label))
{
<label for="@Id">@Label: </label>
}
<InputTextArea id="@Id" @bind-Value="@CurrentValue" class="form-control" />
<ValidationMessage For="@Validation" />
</div>
Listing 4-13The Input Text Component Definition Simplified by the FieldInputBase Class
The Blazor form components have some limitations, like the ability to work with a string value only. Generally, this is not a problem, but sometimes it is required that you convert the current string to a specific value. This is the case of the InputSelect, where the value of the selection must be a string. We are using the InputSelect for the category of an article, and, to solve the problem, we used a string value on the front-end and converted it to an integer on the back-end.
With a custom component, you can also solve this problem thanks to the generic implementation of the base class
InputBase. In Listing
4-12, we are using a string for the generic parameter, but we can require the generic type of each component, including the
InputSelect (Listing
4-14).
public class FieldInputBase<T> : InputBase<T>
{
...
protected override bool TryParseValueFromString(string value, out T result, out string validationErrorMessage)
{
Type paramType = typeof(T);
switch (paramType.FullName)
{
case "System.String":
result = (T)(object)value; break;
case "System.Int32":
result = (T)(object)int.Parse(value); break;
default:
throw new NotSupportedException($"FieldInputBase does not support the type {paramType}");
}
validationErrorMessage = null;
return true;
}
}
Listing 4-14The Generic Implementation of the FieldInputBase
The code gets a little complicated because we need to use .NET Reflection to understand the current type and correctly convert the value in the TryParseValueFromString method. We used a switch to allow the addition of other cases, like Boolean, Guid, and enumeration.
With this change, your
FieldSelectInput
needs only an additional parameter for the selected items; the rest is handled by the base class (Listing
4-15).
@inherits FieldInputBase<int>
...
<InputSelect id="@Id" @bind-Value="@CurrentValueAsString" class="form-control">
@foreach(var item in SelectItems)
{
<option value="@item.Value">@item.Label</option>
}
...
@code {
[Parameter] public InputSelectItem[] SelectItems { get; set; }
}
Listing 4-15The Field Select Component Implementation
Note that the bind-Value uses CurrentValueAsString (defined in the InputBase class) instead of CurrentValue: the InputSelect needs a string, not an integer. Without this change, Blazor treats the integer like a string, and all the internal comparisons when the value changes do not work.
Summary
Creating a library of components greatly simplifies the code of your project, allows you to divide the work between components, and reuse what you have done in other projects. However, it requires you to analyze the requirements to better generalize the components, without going overboard with generalization.
In this chapter, you saw how to use the power of the .NET Framework in a single-page application using .NET Reflection and the generic types. You can make something similar in JavaScript, supported by powerful tools like TypeScript, but in the .NET Framework you have a strict typing system that makes these techniques less prone to errors.
When starting your project, spend a lot of time to make your components reusable and collect them into a library. If you don’t go overboard with generalizations, you will save a lot of time when maintaining your project by investing a little more in the beginning.