© Michele Aponte 2020
M. AponteBuilding Single Page Applications in .NET Core 3 https://doi.org/10.1007/978-1-4842-5747-0_4

4. Build Your Reusable Blazor Library

Michele Aponte1 
(1)
Torre del Greco (NA), Italy
 

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-1

The 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-2

Extracting 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-3

Extracting the List Component That Renders the Table Rows

In Chapter 3, 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-4

Extracting 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-5

Extracting 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-6

Using 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-7

Defining 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-8

Defining 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-9

Using 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-10

The 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-11

Using 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-12

The 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-13

The 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-14

The 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-15

The 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.104.120