4

Understanding Basic Blazor Components

In this chapter, we will look at the components that come with the Blazor template and start to build our own components. Knowing the different techniques used for creating Blazor websites will help us when we start building our components.

Blazor uses components for most things, so we will use the knowledge from this chapter throughout the book.

We will start this chapter with theory and end by creating a component to show some blog posts using the API we created previously in Chapter 3, Managing State – Part 1.

In this chapter, we will cover the following topics:

  • Exploring components
  • Learning Razor syntax
  • Understanding dependency injection
  • Figuring out where to put the code
  • Lifecycle events
  • Parameters
  • Writing our first component

Technical requirements

Make sure you have followed the previous chapters or use the Chapter03 folder as the starting point.

You can find the source code for this chapter’s result at https://github.com/PacktPublishing/Web-Development-with-Blazor-Second-Edition/tree/main/Chapter04.

For this chapter, we will work with the Blazor Server project, so make sure to right-click on the BlazorServer project and select Set as Startup Project.

Exploring components

In Blazor, a component is a .razor file containing a small, isolated functionality (code and markup) or it can be used as a page. A component can host other components as well. This chapter will show us how components work and how to use them.

There are three different ways we can create a component:

  • Using Razor syntax, with the code and HTML sharing the same file
  • Using a code-behind file together with a .razor file
  • Using only a code-behind file

We will go through the different options. The templates we will go through next all use the first option, .razor files, where we have a mix of code and HTML in the same file.

The components in the template are as follows:

  • Counter
  • FetchData

Counter

The counter page shows a button and a counter; if we press the button, the counter increases. We will now break the page apart, making it easier to understand.

At the top of the page is the @page directive, which makes it possible to route to the component directly, as we can see in this code:

@page "/counter"

If we start the BlazorServer project and add /counter to the end of the URL, we see that we can directly access the component by using its route. We can also make the route take parameters, but we will return to that later.

Next, let’s explore the code. To add code to the page, we use the @code statement, and within that statement, we can add ordinary C# code, as shown:

@code {
    private int currentCount = 0;
    private void IncrementCount()
    {
        currentCount++;
    }
}

In the preceding code block, we have a private currentCount variable set to 0. Then we have a method called IncrementCount(), which increments the currentCount variable by 1.

We show the current value by using the @ sign. In Razor, the @ sign indicates that it is time for some code:

<p role="status">Current count: @currentCount</p>

As we can see, Razor is very smart because it understands when the code stops and the markup continues, so there is no need to add something extra to transition from the code to the markup (more on that in the next section).

As we can see in the preceding example, we are mixing HTML tags with @currentCount , and Razor understands the difference. Next, we have a button that is the trigger for changing the value:

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

This is an HTML button with a Bootstrap class (to make it look a bit nicer). @onclick binds the button’s onclick event to the IncrementCount() method. If we were to use onclick without the @, it would refer to the JavaScript event and not work.

So, when we press the button, it will call the IncrementCount() method (depicted by 1 in Figure 4.1), the method increments the variable (depicted by 2), and due to changing the variable, the UI will automatically be updated (depicted by 3), as shown in Figure 4.1:

Figure 4.1 – The flow of the counter component

Figure 4.1: The flow of the counter component

The counter component is implemented similarly for both Blazor WebAssembly and Blazor Server. The FetchData component has two different implementations simply because the Blazor Server project can access the server data directly and Blazor WebAssembly needs to access it through a web API.

We use the same approach with our API to get a feel for how we can leverage Dependency Injection (DI) and connect to a database directly when we use Blazor Server.

FetchData

The next component we will take a look at is the FetchData component. It’s located in the Pages/FetchData.razor folder.

The main implementation of the FetchData component looks similar in both Blazor WebAssembly and Blazor Server. The top rows of the files and the way it gets data differ in the two versions. For Blazor Server, it looks like this:

@page "/fetchdata"
@using BlazorServer.Data
@inject WeatherForecastService ForecastService

It defines a route, adds a namespace, and injects a service. We can find the service in the Data folder in the BlazorServer project.

The service is a class that creates some random forecast data; the code looks like this:

public class WeatherForecastService
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };
    public Task<WeatherForecast[]> GetForecastAsync(DateOnly startDate)
        {
            return Task.FromResult(Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        }).ToArray());
    }
}

As we can see, it generates summaries and randomizes temperatures.

In the code section of the FetchData component, we will find the code that calls the service:

private WeatherForecast[] forecasts;
protected override async Task OnInitializedAsync()
{
    forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
}

The code will get the data from the service and populate an array of WeatherForecast called forecasts.

In the BlazorWebAssembly.Client project, things look a bit different. First of all, the top rows of the file look like this:

@page "/fetchdata"
@using BlazorWebAssembly.Shared
@inject HttpClient Http

The code defines a route using a page directive, adds @using reference to our shared library namespace, and injects HttpClient instead of the service. HttpClient is used to get the data from the server, which is a more realistic real-world scenario.

HttpClient is defined in the Program.cs file and has the same base address as the BlazorWebAssembly.Server project, since the server project is hosting the client project.

Getting the data looks like this:

private WeatherForecast[] forecasts;
protected override async Task OnInitializedAsync()
{
    forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
}

The code will get the data and populate an array of WeatherForecast called forecasts. But instead of getting the data from the service, we are making a call to the "WeatherForecast" URL. We can find the web API in the BlazorWebAssembly.Server project.

Notice that we are using the same WeatherForecast class on both the server and the client. This is one of the things I really like with Blazor.

The controller (Controllers/WeatherForcastController.cs) looks like this (with many similarities to the service):

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };
    private readonly ILogger<WeatherForecastController> logger;
    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        this.logger = logger;
    }
    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        var rng = new Random();
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

It looks the same as the service but is implemented as a web API. As the data looks the same in both versions, getting the data (in both cases) will populate an array with weather forecast data.

In Pages/FetchData.razor, the code for showing the weather data looks like this in both Blazor WebAssembly and Blazor Server:

<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from a service.</p>
@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()
                    </td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

As we can see, by using the Razor syntax, we are seamlessly mixing code with HTML. The code checks whether there is any data – if yes, it will render the table; if not, it will show a loading message. We have full control over the HTML, and Blazor will not add anything to the generated HTML.

There are component libraries that can make this process a bit simpler, which we will look at in the next chapter, Chapter 5, Creating Advanced Blazor Components.

Now that we know how the sample template is implemented, it is time to dive deeper into the Razor syntax.

Learning Razor syntax

One of the things I like about the Razor syntax is that it is easy to mix code and HTML tags. By having the code close to the markup, it is, in my opinion, easier to follow and understand. The syntax is very fluid; the razor parser understands when the code stops and markup begins, which means we don’t need to think about it that much. It is also not a new language; instead, we can leverage our existing C# and HTML knowledge to create our components. This section will be a lot of theory to help us understand the Razor syntax.

To transition from HTML to code (C#), we use the @ symbol. There are a handful of ways we can add code to our file:

  • Razor code blocks
  • Implicit Razor expressions
  • Explicit Razor expressions
  • Expression encoding
  • Directives

Razor code blocks

We have already seen some code blocks. A code block looks like this:

@code {
    //your code here
}

If we wish, we can skip the code keyword like so:

@{
    //your code here
}

Inside those curly braces, we can mix HTML and code like this:

@{
    void RenderName(string name)
    {
        <p>Name: <strong>@name</strong></p>
    }
    RenderName("Steve Sanderson");
    RenderName("Daniel Roth");
}

Notice how the RenderName() method transitions from code into the paragraph tags and back to code; this is an implicit transition.

If we want to output text without having an HTML tag, we can use the text tag instead of using the paragraph tags, as shown in the following example:

<text>Name: <strong>@name</strong></text>

This would render the same result as the previous code but without the paragraph tags, and the text tag won’t be rendered.

Implicit Razor expressions

Implicit Razor expressions are when we add code inside HTML tags.

We have already seen this in the FetchData example:

<td>@forecast.Summary</td>

We start with a <td> tag, then use the @ symbol to switch to C#, and switch back to HTML with the end tag. We can use the await keyword together with a method call, but other than that, implicit Razor expressions cannot contain any spaces.

We cannot call a generic method using implicit expressions since <> would be interpreted as HTML. Hence, to solve this issue, we can use explicit expressions.

Explicit Razor expressions

We can use explicit Razor expressions if we want to use spaces in the code. Write the code with the @ symbol followed by parentheses ( ). So, it would look like this: @().

In this sample, we subtract 7 days from the current date:

<td>@(DateTime.Now - TimeSpan.FromDays(7))</td>

We can also use explicit Razor expressions to concatenate text; for example, we can concatenate text and code like this:

<td>Temp@(forecast.TemperatureC)</td>

The output would then be <td>Temp42</td>.

Using explicit expressions, we can easily call generic methods by using this syntax:

<td>@(MyGenericMethod<string>())</td>

The Razor engine knows whether we are using code or not. It also makes sure to encode strings to HTML when outputting it to the browser, called expression encoding.

Expression encoding

If we have HTML as a string, it will be escaped by default. Take this code, for example:

@("<span>Hello World</span>")

The rendered HTML would look like this:

&lt;span&gt;Hello World&lt;/span&gt;

To output the actual HTML from a string (something we want to do later on), you can use this syntax:

@((MarkupString)"<span>Hello World</span>")

Using MarkupString, the output will be HTML, showing the HTML tag span. In some cases, one line of code isn’t enough; then, we can use code blocks.

Directives

There are a bunch of directives that change the way a component gets parsed or can enable functionality. These are reserved keywords that follow the @ symbol. We will go through the most common and useful ones.

I find that it is pretty nice to have the layout and the code inside of the same .razor file.

Note that we can use code-behind to write our code to get a bit more separation between the code and layout. Later in this chapter, we will look at how to use code-behind instead of Razor syntax for everything. For now, the following examples will look at how we would do the same directives using both Razor syntax and code-behind.

Adding an attribute

To add an attribute to our page, we can use the attribute directive:

@attribute [Authorize]

If we were using a code-behind file, we would use the following syntax instead:

[Authorize]

Adding an interface

To implement an interface (IDisposable in this case), we would use the following code:

@implements IDisposable

Then we would implement the methods the interface needs in a @code{} section.

To do the same in a code-behind scenario, we would add the interface after the class name, as shown in the following example:

public partial class SomeClass : IDisposable {}

Inheriting

To inherit another class, we should use the following code:

@inherits TypeNameOfClassToInheritFrom

To do the same in a code-behind scenario, we would add the class we want to inherit from after the class name:

public class SomeClass : TypeNameOfClassToInheritFrom {}

Generics

We can define our component as a generic component.

Generics allow us to define the data type, so the component works with any data type.

To define a component as a generic component, we add the @typeparam directive; then, we can use the type in the code of the component like this:

@typeparam TItem
@code
{
      [Parameter]
      public List<TItem> Data { get; set; }
}

Generics are super-powerful when creating reusable components, and we will return to generics in Chapter 6, Building Forms with Validation.

Changing the layout

If we want to have a specific layout for a page (not the default one specified in the App.razor file), we can use the @layout directive:

@layout AnotherLayoutFile

This way, our component will use the specified layout file (this only works for components with the @page directive).

Setting a namespace

By default, the component’s namespace will be the name of the default namespace of our project, plus the folder structure. If we want our component to be in a specific namespace, we can use the following:

@namespace Another.NameSpace

Setting a route

We have already touched on the @page directive. If we want our component to be directly accessed using a URL, we can use the @page directive:

@page "/theurl"

The URL can contain parameters, subfolders, and much more, which we will return to later in this chapter.

Adding a using statement

To add a namespace to our component, we can use the @using directive:

@using System.IO

If there are namespaces that we use in several of our components, then we can add them to the _Imports.razor file instead. This way, they will be available in all the components we create.

Now we know more about how Razor syntax works. Don’t worry; we will have plenty of time to practice it. There is one more directive that I haven’t covered in this section, and that is inject. We have seen it a couple of times already, but to cover all the bases, we first need to understand what DI is and how it works, which we will see in the next section.

Understanding dependency injection

DI is a software pattern and a technique to implement Inversion of Control (IoC).

IoC is a generic term that means we can indicate that the class needs a class instance instead of letting our classes instantiate an object. We can say that our class wants either a specific class or a specific interface. The creation of the class is somewhere else, and it is up to IoC what class it will create.

When it comes to DI, it is a form of IoC when an object (class instance) is passed through constructors, parameters, or service lookups.

Here is a great resource if you want to dive deeper into DI in .NET: https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection.

In Blazor, we can configure DI by providing a way to instantiate an object. In Blazor, this is a key architecture pattern that we should use. We have already seen a couple of references to it, for example, in Startup.cs:

services.AddSingleton<WeatherForecastService>();

Here, we say that if any class wants WeatherForecastService, the application should instantiate an object of the WeatherForecastService type. In this case, we don’t use an interface; instead, we could create an interface and configure it like this:

services.AddSingleton<IWeatherForecastService, WeatherForecastService>();

In this case, if a class asks for an instance of IWeatherForecastService, the app will instantiate a WeatherForecastService object and return it. We did this in the previous chapter, Chapter 3, Managing State – Part 1. We created an IBlogApi interface that returned an instance of BlogApiJsonDirectAccess; when we implement the WebAssembly version, DI will return another class instead.

There are many advantages to using DI. Our dependencies are loosely coupled, so we don’t instantiate another class in our class. Instead, we ask for an instance, which makes it easier to write tests and change implementations depending on platforms.

Any external dependencies will be much more apparent since we must pass them into the class. We also can set the way we should instantiate the object in a central place. We configure the DI in Program.cs.

We can configure the creation of objects in different ways, such as the following:

  • Singleton
  • Scoped
  • Transient

Singleton

When we use a singleton, the object will be the same for all site users. The object will only be created once.

To configure a singleton service, use the following:

services.AddSingleton<IWeatherForecastService, WeatherForecastService>();

We should use a singleton when we want to share our object with all the users of our site, but beware that the state is shared, so do not store any data connected to one particular user or user preference because it will affect all the users.

Scoped

When we use scoped, a new object will be created once for each connection, and since Blazor Server needs a connection to work, it will be the same object as long as the user has a connection. WebAssembly does not have the concept of scoped, since there is no connection, so all the code runs inside the user’s web browser. If we use scoped, it will work the same way as a singleton for Blazor WebAssembly, since we only have one user and everything running inside the browser. The recommendation is still to use scoped if the idea is to scope a service to the current user. This makes it easier to move code between Blazor Server and Blazor WebAssembly and gives a bit more context on how the service is supposed to be used.

To configure a scoped service, use the following:

services.AddScoped<IWeatherForecastService, WeatherForecastService>();

We should use scoped if we have data that belongs to the user. We can keep the user’s state by using scoped objects. More on that in Chapter 11, Managing State – Part 2.

Transient

When we use transient, a new object will be created every time we ask for it.

To configure a transient service, use the following:

services.AddTransient<IWeatherForecastService, WeatherForecastService>();

We should use transient if we don’t need to keep any state, and we don’t mind the object being created every time we ask for it.

Now that we know how to configure a service, we need to start using the service by injecting it.

Injecting the service

There are three ways to inject a service.

We have already seen the first method in the FetchData component code. We can use the @inject directive in the Razor file:

@inject WeatherForecastService ForecastService

This will make sure we have access to WeatherForecastService in our component.

The second way is to create a property by adding the Inject attribute if we are using code-behind:

[Inject]
public WeatherForecastService ForecastService { get; set; }

The third way is if we want to inject a service into another service, then we need to inject the services using the constructor:

public class MyService
{
    public MyService(WeatherForecastService
      weatherForecastService)
    {
    }
}

Now we know how DI works and why we should use it.

In this chapter, we have mentioned code-behind a couple of times. In the next section, we will look at how we can use code-behind with Razor files and skip the Razor files altogether.

Figuring out where to put the code

We have seen examples of writing code directly in the Razor file. I prefer doing that unless the code gets too complicated. I always lean in favor of readability.

There are four ways we can write our components:

  • In the Razor file
  • In a partial class
  • Inheriting a class
  • Only code

Let’s go through each item on this list in more detail.

In the Razor file

If we are writing a file that is not that complex, it would be nice not to switch files when writing components. As we already covered in this chapter, we can use the @code directive to add code directly to our Razor file.

If we want to move the code to a code-behind file, then it is only the directives that we need to change. For the rest of the code, we can just move to the code-behind class. When I started with Blazor, writing code and markup in the same file felt strange, but I suggest you try it out when developing your web apps.

At work, we started using code-behind but switched to writing code in the .razor file instead, and we haven’t looked back since.

But many developers prefer code-behind, separating code from the layout. For that, we can use a partial class.

In a partial class

We can create a partial class with the same filename as the Razor file and just add .cs.

If you have downloaded the source code (or you can check the code on GitHub) for Chapter 3, Managing State – Part 1, you can look at FetchDataWithCodeBehind.razor.cs in the BlazorServer project. I have moved all the code to the code-behind file; the result when compiling this will be the same as if we kept the code in the Razor file. It is just a matter of preference.

The code-behind looks like this:

public partial class FetchDataWithCodeBehind
{
    private WeatherForecast[]? forecasts;
    [Inject]
    public WeatherForecastService? ForecastService { get; set; }
    protected override async Task OnInitializedAsync()
    {
        if (ForecastService != null)
        {
            forecasts = await ForecastService.GetForecastAsync(DateOnly.FromDateTime(DateTime.Now));
        }
    }
}

As we can see, instead of using @inject, we are using [Inject] like this:

[Inject]
public WeatherForecastService ForecastService { get; set; }

Other than that, I have just copied the code from the Razor file.

This is not the only way to use a code-behind file; we can also inherit from a code-behind file.

Inheriting a class

We can also create a completely different class (the common pattern is to call it the same thing as the Razor file and add Model at the end) and inherit it in our Razor file. For that to work, we need to inherit from ComponentBase. In the case of a partial class, the class already inherits from ComponentBase, since the Razor file does that.

Fields must be protected or public (not private) for the page to access the fields. I recommend using the partial class if we don’t need to inherit from our base class.

This is a snippet of the code-behind class declaration:

public class FetchDataWithInheritsModel:ComponentBase

We’ll need to inherit from ComponentBase or from a class that inherits from ComponentBase.

In the Razor file, we will use the @inherits directive:

@inherits FetchDataWithInheritsModel

The Razor file will now inherit from our code-behind class (this was the first available way to create code-behind classes).

Both the partial and inherit options are simple ways of moving the code to a code-behind file. But another option is to entirely skip the Razor file and use only code.

Only code

Visual Studio will use source generators to convert the Razor code into C#. We will dig deeper into source generators in Chapter 17, Examining Source Generators. The Razor file will generate code at compile time. We can skip the Razor step if we want to and write our layout completely in code.

This file (CounterWithoutRazor.cs) is available on GitHub.

The counter sample would look like this:

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
namespace BlazorServer.Pages
{
    [Route("/CounterWithoutRazor")]
    public class CounterWithoutRazor: ComponentBase
    {
        protected override void BuildRenderTree
          (RenderTreeBuilder builder)
        {
            builder.AddMarkupContent(0, "<h1>Counter</h1>

");
            builder.OpenElement(1, "p");
            builder.AddContent(2, "Current count: ");
            builder.AddContent(3,currentCount);
            builder.CloseElement();
            builder.AddMarkupContent(4, "

");
            builder.OpenElement(5, "button");
            builder.AddAttribute(6, "class","btn btn-primary");
            builder.AddAttribute(7,"onclick", EventCallback.Factory.Create<MouseEventArgs>(this,IncrementCount));
            builder.AddContent(8, "Click me");
            builder.CloseElement();
        }
        private int currentCount = 0;
        private void IncrementCount()
        {
            currentCount++;
        }
    }
}

The Razor file will first be converted in to something roughly the same as the previous code, and then the code is compiled. It adds the elements one by one, which, in the end, will render the HTML.

The numbers in the code are how Blazor keeps track of each element in the render tree. Some prefer to write the code as in the previous code block rather than using the Razor syntax; there are even efforts in the community to simplify the process of manually writing the BuildRenderTree() function.

Some of Microsoft’s built-in components are built in this way.

I recommend never writing this manually, but I’ve kept it in the book because it shows how Razor files get compiled. Now that we know how to use code-behind let’s look at the life cycle events of Blazor and when they get executed.

Lifecycle events

We can use a couple of lifecycle events to run our code. In this section, we will go through them and see when we should use them. Most life cycle events have two versions – synchronous and asynchronous.

OnInitialized and OnInitializedAsync

The first time the component is loaded, OnInitialized() is called, then OnInitializedAsync(). This is a great method to load any data, as the UI has not yet been rendered. If we are doing long-running tasks (such as getting data from a database), we should put that code in the OnInitializedAsync() method.

These methods will only run once. If we want to update the UI when a parameter changes, see OnParametersSet() and OnParametersSetAsync().

OnParametersSet and OnParametersSetAsync

OnParametersSet() and OnParametersSetAsync() are called when the component is initialized (after OnInitialized() and OnInitializedAsync()) and whenever we change the value of a parameter.

If we, for example, load data in the OnInitialized() method but it does use a parameter, the data won’t be reloaded if the parameter is changed, since OnInitialized() will only run once. We need to trigger a reload of the data in OnParametersSet() or OnParametersSetAsync(), or move the loading to that method.

OnAfterRender and OnAfterRenderAsync

After the component renders, the OnAfterRender() and OnAfterRenderAsync() methods are called. When the methods are being called, all the elements are rendered, so if we want/need to call any JavaScript code, we have to do that from these methods (we will get an error if we try to make a JavaScript interop from any of the other lifecycle event methods). We also have access to a firstRender parameter, so we can only run our code on the first render.

ShouldRender

ShouldRender() is called when our component is re-rendered; if it returns false, the component will not be rendered again. It will always render once; it is only when it is re-rendered that the method runs.

ShouldRender() does not have an asynchronous option.

Now we know when the different lifecycle events happen and in what order. A component can also have parameters, and that way, we can reuse them but with different data.

Parameters

A parameter makes it possible to send a value to a component. To add a parameter to a component, we use the [Parameter] attribute on the public property:

@code {
    [Parameter]
    public int MyParameter { get; set; }
}

We can also do the same using a code-behind file. We can add a parameter using the @page directive by specifying it in the route:

@page "/parameterdemo/{MyParameter}"

In this case, we have to have a parameter specified with the same name as the name inside the curly braces. To set the parameter in the @page directive, we go to /parameterdemo/THEVALUE.

There are cases where we want to specify another type instead of a string (string is the default). We can add the data type after the parameter name like this:

@page "/parameterdemo/{MyParameter:int}"

This will match the route only if the data type is an integer. We can also pass parameters using cascading parameters.

Cascading parameters

If we want to pass a value to multiple components, we can use a cascading parameter.

Instead of using [Parameter], we can use [CascadingParameter] like this:

[CascadingParameter]
public int MyParameter { get; set; }

To pass a value to the component, we surround it with a CascadingValue component like this:

<CascadingValue Value="MyProperty">
    <ComponentWithCascadingParameter/>
</CascadingValue> 
@code {
    public string MyProperty { get; set; } = "Test Value";
}

CascadingValue is the value we pass to the component, and CascadingParameter is the property that receives the value.

As we can see, we don’t pass any parameter values to the ComponentWithCascadingParameter component; the cascading value will match the parameter with the same data type. If we have multiple parameters of the same type, we can specify the name of the parameter in the component with the cascading parameter like this:

[CascadingParameter(Name = "MyCascadingParameter")]

We can also do so for the component that passes CascadingValue, like this:

<CascadingValue Value="MyProperty" Name="MyCascadingParameter">
    <ComponentWithCascadingParameter/>
</CascadingValue>

If we know that the value won’t change, we can specify that by using the IsFixed property:

<CascadingValue Value="MyProperty" Name="MyCascadingParameter" IsFixed="True">
    <ComponentWithCascadingParameter/>
</CascadingValue>

This way, Blazor won’t look for changes. The cascading values/parameters cannot be updated upward but are updated only downward. This means that to update a cascading value, we need to implement it in another way; updating it from inside the component won’t change any components that are higher in the hierarchy.

In Chapter 5, Creating Advanced Blazor Components, we will look at events, which are one way to solve the problem of updating a cascading value.

Phew! This has been an information-heavy chapter, but now we know the basics of Blazor components. Now it is time to build one!

Writing our first component

The first component we will build shows all the blog posts on a site. To be fair, we haven’t written any blog posts yet, but we will temporarily solve that so we can start doing something fun.

In Chapter 3, Managing State – Part 1, we created a JSON repository and an API (or interface); now, it is time to use them.

Components with or without a page directive can be shared across different projects. By the end of this book, we will have built a blog in both Blazor Server and Blazor WebAssembly that share code.

There is a whole chapter on sharing (Chapter 9, Sharing Code and Resources) but let’s start now.

Creating a components library

The first thing we need to do is to create a new project and then add our components to that project.

To create our first component, follow these instructions:

  1. Right-click on the MyBlog solution and select Add | New Project.
  2. Find the template Razor Class Library and click Next.
  3. Name the project Components and click Next.
  4. Select .NET 7.0 (Standard Term Support) and click Create.
  5. We now have a project called Components, where we can add all the components we want to share. Remove the Component1.razor and ExampleJsInterop.cs that are created by default.
  6. In the Components project, add a project reference to Data.Models.
  7. In the BlazorServer project, in the Pages folder, delete all .razor files.
  8. In the BlazorServer project, delete the Shared folder completely.
  9. Now, we will move some of the files from the BlazorWebAssembly.Client project. These are the files we can share between the projects.

    That would be the files in the Pages folder and Shared folder. In the BlazorWebAssembly.Client project, cut the Pages and Shared folders, and paste them into the Components project.

  1. Since FetchData has a bit of a different implementation, let’s delete that one as well.

    In the Components project, delete Pages/FetchData.razor.

  1. In the Components project, open _Imports.razor and add the following lines:
    @using System.Net.Http
    @using Microsoft.AspNetCore.Authorization
    @using Microsoft.AspNetCore.Components.Forms
    @using Microsoft.AspNetCore.Components.Routing
    @using Microsoft.AspNetCore.Components.Web
    @using Microsoft.AspNetCore.Components.Web.Virtualization
    @using Microsoft.JSInterop
    @using Components.Pages
    @using Components.Shared
    

We have a new project, moved components, and cleaned up the BlazorServer and BlazorWebAssembly projects.

Next, we will put the components to use.

Using our components library

We have a nice library, and can start using the components right now, but we also want to trigger the routes. We want our site to get the Index component if we navigate to "/".

To do that, we need to follow a couple of steps:

  1. In the BlazorServer project, add a project reference to the Components project.
  2. Open App.razor and add the following property to the Router component:
    <Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="new[] { typeof(Components.Pages.Index).Assembly}">
    

    We are telling Blazor to look for Razor components inside the assembly where the app component lives.

    This means the BlazorServer assembly in this case.

    We also want Blazor to look for components in the assembly where the Index component is located, which is the components assembly.

  1. Open _Imports.razor and add the following:
    @using Components.Pages
    @using Components.Shared
    
  2. Remove the @using BlazorServer.Shared since that namespace doesn’t exist now we have moved our files.

    Now we need to do the same thing for our WebAssembly project.

  1. In the BlazorWebAsesembly.Client project, add a project reference to the Components project.
  2. Open App.razor and add the following property to the Router component.
    <Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="new[] { typeof(Components.Pages.Index).Assembly}">
    

    Just as with Blazor Server, we are telling Blazor that it should look for Razor components inside the assembly where the app component lives.

    This means the BlazorWebAssembly.Client assembly in this case.

    We also want Blazor to look for components in the assembly where the Index component is located, which is the components assembly.

  1. Open _Imports.razor and add the following:
    @using Components.Pages
    @using Components.Shared
    
  2. Remove the @using BlazorWebAssembly.Client.Shared since that namespace doesn’t exist after we moved our files.

Great! we have all our components in a separate library and are sharing the components between the BlazorServer and the BlazorWebAssembly projects.

Creating our own component

Now it’s time to start adding our own component!

OK, this is not completely true because we will reuse Index.razor. Let’s start by creating a component that lists our blog posts:

  1. In the Components project, open Pages/Index.razor.
  2. Replace the contents of that file with the following code:
    @page "/"
    @using Data.Models.Interfaces
    @using Data.Models
    @inject IBlogApi _api
    @code{
    }
    

    If we start from the top, we can see a page directive. It will ensure that the component is shown when the route is “/”. Then, we have three @using directives, bringing in the namespaces so we can use them in the Razor file.

    Then we inject our API (using DI) and name the instance _api.

  1. Add a variable that holds all our posts. In the code section, add the following:
    protected List<BlogPost> posts = new List<BlogPost>();
    
  2. Now we need to load the data.

    To load posts, add the following in the code section:

    protected override async Task OnInitializedAsync()
    {
        posts = await _api.GetBlogPostsAsync(10, 0);
        await base.OnInitializedAsync();
    }
    
  1. Now, when the page loads, the posts will be loaded as well: 10 posts and page 0 (the first page).
  2. Under the @inject row, add the following code:
    <ul>
        @foreach (var p in posts)
        {
            <li>@p.Title</li>
        }
    </ul>
    

We add an Unordered List (UL); inside that, we loop over blog posts and show the title.

Now we can run the application by pressing Ctrl + F5 (Debug | Start Without Debugging). Make sure you have the BlazorServer selected as the startup project.

Since we don’t have any blog posts, this would take us to an empty page. Luckily there is a folder in the repo called Example data.; if you download that, put those files in the Data folder, and reload the web, you should see a couple of posts.

Great job, we have created our first component!

There are a few noteworthy things; the Components project knows nothing about the JSON repository implementation and only knows about the IBlogApi interface.

The Index component asks for an instance of IBlogApi, and the BlazorServer project knows it should return an instance of BlogApiJsonDirectAccess. This is one of the things I love about Blazor; we can create components that only consume an interface and know nothing about the implementation.

We will come back to this when we implement a web API for WebAssembly in Chapter 7, Creating an API.

Summary

In this chapter, we learned a lot about Razor syntax – something we will use throughout the book. We learned about DI, directives, and parameters and, of course, created our first component. This knowledge will help us understand how to create and reuse components.

In the next chapter, we will look at more advanced component scenarios.

Join our community on Discord 

Join our community’s Discord space for discussions with the author and other readers: 

https://packt.link/WebDevBlazor2e

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

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