Working with Blazor’s component model

Microsoft Blazor wallpaper

Welcome to Working with Blazor’s component model” post! In this new post I’ll build a simple project in Blazor and explain the basic Blazor components and interactions. Also, the source code of the project I’m going to create in this post is available on GitHub.

Here the posts I wrote about Blazor that help you to learn this new technology better and faster:

What is a Blazor component?

First, the fundamental building blocks of Blazor applications are components, almost everything you do will directly or indirectly work with them. In order to build great applications, you must know how to harness their power.

So, components define a piece of UI, that can be something as small as a button or as large as an entire page, components can also contain other components. They encapsulate any data that that piece of UI requires to function. They allow a piece of UI that you can reuse across an application or even shared across multiple applications.

Then, you can pass data into a component using parameters. Parameters define the public API of a component. The syntax for passing data into a component using parameters is like defining attributes on a standard HTML element, with a key/value pair. The key being the parameter name and the value being the data you wish to pass to the component.

Also, the data a component holds is more commonly referred to as its state. Methods on a component define its logic. These methods manipulate and control that state via the handling of events.

Also, we can style the components via traditional global styling; however, it is more common to use scoped styles. Scoped styles allow the component to define its own CSS classes without fear of collision with other styles in the application. It’s even possible to use CSS pre-processors such as Sass with scoped styling.

The new component for drawer in our application - Working with Blazor’s component model
The new component for drawer in our application

Now, we’ll be adding a cool new slide out drawer feature to Blazor Trails. The drawer will slide out from the right-hand side of the page when the user clicks on a button we’ll add to the TrailCard component. The drawer will display more detailed information about the selected trail. When the user clicks the close button on the drawer it will cleanly slide back out of view.

Structuring Components

As you will find with almost every part of Blazor, there are multiple ways of doing things. The Blazor team have been very deliberate with making the framework unopinionated. So, developers can build applications the way that works best for them.

It is possible to define a component in a single .razor file that contains both its markup and logic. Also, it is possible to separate a component into a .razor file which defines the markup and a C# class which defines the logic.

Single file

When using a single file approach all mark-up and logic for a component is defined in a single file. The primary advantage of this approach is that it allows you to work with everything in one place. This can really help with productivity as you don’t need to keep swapping back and forth between multiple files.

Single file is the default when creating new components.

HomePage code

@page "/"
@inject HttpClient Http
 
@if (_trails == null)
{
    <p>Loading trails...</p>
}
else
{
    <div class="row row-cols-1 row-cols-md-2">
 
        @foreach (var trail in _trails)
        {
            <div class="col mb-4">
                <TrailCard Trail="trail" />
            </div>
 
        }
 
    </div>
}

@code {
    private IEnumerable<Trail> _trails;
 
    protected override async Task OnInitializedAsync()
    {
        try
        {
            _trails = await Http.GetFromJsonAsync<IEnumerable<Trail>>("trails/trail-data.json");
        }
        catch (HttpRequestException ex)
        {
            Console.WriteLine($"There was a problem loading trail data: {ex.Message}");
        }
    }
}

Code explained

The code should look familiar, this is the Blazor Trails home page component. The entire component is defined in a single .razor file with the markup coming first then the logic coming second, defined in the code block.

So, having everything in a single file, it allows me to work faster as I don’t have to switch files. But there is another benefit which I find useful, monitoring component size.

When building applications, it’s easy to create very large components which are doing lots of things. However, just like when creating regular C# classes, you should try to keep your components focused, with a single purpose.

One way I use to gauge this is the size of my component files. When I find I’m constantly scrolling up and down a file adding markup and logic, it’s an indication that my component may be doing too much and I should be thinking about splitting it out into additional components with more focused responsibility. This isn’t a clear-cut method however, there are times when a component may be quite large but still only have one responsibility, but it at least makes me think about it.

One argument I often hear against this method is that mark-up and logic should be separated because otherwise we’re mixing concerns. I disagree with this view. The logic in a component should be logic which operates over the mark-up and drives the function of the component. Business logic has no place in components.

If you take this view then the logic and mark-up are intertwined, they are tightly coupled, one can’t exist without the other. In which case separating them seems to fall into the same category as organizing an applications files by type rather than feature, and this is inefficient and hinders productivity.

Visual Studio and Razor

There is one drawback of this approach at the moment, and that is the functionality of the Razor editor in Visual Studio. There are several key refactoring abilities which don’t work in razor files, namely quick actions and refactorings. Renaming of variables can be a bit sketchy and only work some of the time. This is due to the fact the Razor editor in Visual Studio is not fully support Blazor yet.

However, the tooling teams are currently completely rebuilding the Razor tooling experience from the ground up to include all the rich functionality that developers have come to expect when working with regular C# class files. Once this work is complete, the experience working in Razor files should be on par with that of C# class files.

Partial class

Another approach is to split the markup and logic of a component into two separate files. The markup of the component is kept in the .razor file, the logic is added to a C# class. In earlier versions of Blazor it was only possible to apply this approach using inheritance as there was no support for the partial keyword, this is no longer the case.

Let’s take a look at the home page component refactored to use this approach.

@page "/"
 
@if (_trails == null)
{
    <p>Loading trails...</p>
}
else
{
    <div class="row row-cols-1 row-cols-md-2">
 
        @foreach (var trail in _trails)
        {
            <div class="col mb-4">
                <TrailCard Trail="trail" />
            </div>
 
        }
 
    </div>
}

Now the logic for the component.

using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
 
namespace BlazorTrails.Features.Home
{
    public partial class HomePage : ComponentBase
    {
        private IEnumerable<Trail> _trails;
 
        [Inject] public HttpClient Http { get; set; }
 
        protected override async Task OnInitializedAsync()
        {
            try
            {
                _trails = await Http.GetFromJsonAsync<IEnumerable<Trail>>("trails/trail-data.json");
            }
            catch (HttpRequestException ex)
            {
                Console.WriteLine($"There was a problem loading trail data: {ex.Message}");
            }
        }
    }
}

As you can see, using this technique you can make the two elements of the component completely separate. You should also note the naming of the files, HomePage.razor and HomePage.razor.cs. If you’re using Visual Studio to build your applications, following this naming convention will produce a nested effect.

 Naming a partial class the same as the markup portion of the component will produce a nested effect when using Visual Studio - Working with Blazor’s component model
 Naming a partial class the same as the markup portion of the component will produce a nested effect when using Visual Studio

Pros of separating UI and logic

This makes it easier to work with the component. It also keeps the number of files being displayed in the IDE to a minimum as you can simply hide any partial classes you’re not currently interested in.

The major benefit of separating the markup and logic of the component is the development experience. As I mentioned when talking about the single file approach, the razor editor is not as fully featured as when working with regular C# class files. By separating out the logic to a C# class file, developers can access all the editor features.

Cons of separating UI and logic

The drawback of this approach is that you now have two files to manage when you’re working with a component. This can end up with lots of switching back and forth as you will need to be in the logic file when adding methods or other members to the component. Then you will need to be in the mark-up file to add any UI, hook up event handlers, etc.

Largely which approach you choose for building your applications is a personal choice based on which method you find most productive.

Component lifecycle methods

Just as in other component-based frameworks, components in Blazor have a lifecycle.

Blazor component lifecycle methods - Working with Blazor’s component model
Blazor component lifecycle methods

Depending on what an application is doing, it may need to perform actions at certain points during this lifecycle. For example, load initial data for the component to display when it is first created, or update the UI when a parameter has a certain value from the parent. Blazor supports this by giving us access to the component lifecycle at specific points.

  1. Initialized component: OnInitialized/OnInitializedAsync
  2. Parameter set: OnParametersSet/OnParametersSetAsync
  3. After Render: OnAfterRender/OnAfterRenderAsync

The lifecycle methods are provided by the ComponentBase class which all components inherit from. Each method has a synchronous and asynchronous version. The synchronous version is always called before the asynchronous version.

<h1>Componet Lifecycle</h1>
<p>Check the browser console for details...</p>
 
@code {
 
    public override async Task SetParametersAsync(ParameterView parameters)
    {
        Console.WriteLine("SetParametersAsync - Begin");
        await base.SetParametersAsync(parameters);
        Console.WriteLine("SetParametersAsync - End");
    }
 
    protected override void OnInitialized()
    {
        Console.WriteLine("OnInitialized");
       }
 
    protected override async Task OnInitializedAsync()
    {
        Console.WriteLine("OnInitializedAsync");
       }
 
    protected override void OnParametersSet()
    {
        Console.WriteLine("OnParametersSet");
    }
 
    protected override async Task OnParametersSetAsync()
    {
        Console.WriteLine("OnParametersSetAsync");
    }
 
    protected override void OnAfterRender(bool firstRender)
    {
        Console.WriteLine($"OnAfterRender (First render: {firstRender})");   
    }
 
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        Console.WriteLine($"OnAfterRenderAsync (First render: {firstRender})");     }
}

The first render

During the first render, all the component’s lifecycle methods will be called – during subsequent renders only a subset of the methods will run.

The process starts with SetParametersAsync being called. This is the only lifecycle method which requires us to call the base method, if we don’t then the component will fail to load. This is because the base method does two essential things:

  • Sets the values for any parameters the component defines – This happens both the first time the component is rendered and whenever parameters could have changed
  • Calls the correct lifecycle methods depending if the component is running for the first time or not

If we removed the call to the base method the output in the browser console would look like this.

Render explained

During a first render, the component hasn’t been initialized. This means that OnInitialized and OnInitializedAsync will be called first – it is also the only time they will run. This pair of methods are the only ones which run once in a component’s lifetime. You can think of these as constructors for your component. It makes them a great place to make API calls, for example, to get the initial data the component will display.

Once the OnInitialized methods have run, OnParametersSet and OnParametersSetAsync are called. These methods allow developers to perform actions whenever a components parameters change. In the case of a first render, the components parameters have been set to their initial values.

The final methods to run are OnAfterRender and OnAfterRenderAsync. These methods both take a Boolean value indicating if this is the first time the component has been rendered. On the initial render, the value of firstRender will be set to true for every render after, it will be false.

void OnAfterRender(bool firstRender)
Task OnAfterRenderAsync(bool firstRender)

This is useful as it allows one-time operations to be performed when a component is first rendered, but not on subsequent renders. The primary use of the OnAfterRender methods are to perform JavaScript interop and other DOM related operations such as setting the focus on an element.

The lifecycle with async

One key point about the render we just covered was that it ran synchronously. In the Lifecycle component there are no awaited calls in any of the async lifecycle methods, meaning each method ran in sequence. However, when async calls are added then things look a bit different. To demonstrate this let’s update the OnInitializedAsync method in the Lifecycle.razor component to make an async call.

protected override async Task OnInitializedAsync()
{
    Console.WriteLine("OnInitializedAsync - Begin");
    await Task.Delay(300);
    Console.WriteLine("OnInitializedAsync - End");
}

While Blazor was awaiting the async call, the component was rendered. You can see it was then rendered a second time after the OnParametersSet methods, as before. This is because Blazor checks to see if an awaitable task is returned from OnInitializedAsync, if there is it calls StateHasChanged to render the component with the results of any of the asynchronous code which has been run so far, while awaiting the completion of the task. This behavior is also true for async calls made in OnParametersSetAsync.

Dispose – The extra lifecycle method

There is another lifecycle method which we can use but this one is optional and it’s not built-in to the ComponentBase class, Dispose. This method is used for the same purposes in Blazor as in other C# applications, to clean up resources. This method is essential when creating components which subscribe to events, as failing to unsubscribe from events before a component is destroyed will cause a memory leak.

In order to access this method a component must implement the IDisposable interface.

@implements IDisposable
 
<h1>Componet Lifecycle</h1>
<p>Check the browser console for details...</p>
 
@code {
 
    // Other methods ommitted for brevity
 
    public void Dispose()
    {
        Console.WriteLine($"Dispose - Begin");
        Console.WriteLine($"Dispose - End");
    }
}

To see the effect of this new lifecycle method we need to navigate away from the component, this will remove it from the DOM and invoke the Dispose method.

Blazor understands the IDisposable interface, when it detects its presence on a component it will call the Dispose method at the correct point when destroying the component instance.

As of .NET 5, Blazor also supports the IAsyncDisposable interface. This allows disposal of resources asynchronously. This is useful when using JavaScript interop. But for now, note that IDisposable and IAsyncDisposable can’t both be implemented on the same component.

Communicating between parent and child components

A great analogy for components is Lego blocks. Each Lego block is a self-contained unit, but the real fun comes when you plug the blocks together in order to build something bigger and better. This is the same for components, they can be useful on their own, but they are more powerful when used together. In order to do this in any meaningful way, components need to be able to communicate with each other, passing data, and firing and handling events.

In Blazor, we achieve this using component parameters. Component parameters are declared on a child component which forms that components API. A parent component can then pass data to the child using that API. But component parameters can also be used to define events on the child that the parent can handle. This allows data to be passed from the child back up to the parent.

We’ll add a view button to the TrailCard which when clicked, will slide open a drawer on the right of the application. This drawer will display more detailed information about the selected trail. For this to work we will need to have three different components communicate and pass data.

The HomePage component will coordinate the operation. It will handle any OnSelected events from the TrailCard component. When an OnSelected event is raised, the HomePage component will record the selected trail and pass it into the TrailDetails component. Inside the TrailDetails component, whenever the trail value changes, it will be the trigger for the drawer to active and slide into view.

Passing values from a parent to a child

In order to build our new drawer, we need to create a component which takes in a trail and then displays its information. We will use a component parameter to create its API.

The TrailDetails component

The TrailDetails component will display the selected trail which is passed in via a component parameter.

<div class="drawer-wrapper @(_isOpen ? "slide" : "")">
    <div class="drawer-mask"></div>
    <div class="drawer">
        @if (Trail != null)
        {
            <div class="drawer-content">
                <div class="trail-image">
                    <img src="@Trail.Image" />
                </div>
                <div class="trail-details">
                    <h3>@Trail.Name</h3>
                    <h6 class="mb-3 text-muted"><span class="oi oi-map-marker"></span> @Trail.Location</h6>
                    <div class="mt-4">
                        <span class="mr-5"><span class="oi oi-clock mr-2"></span> @Trail.Time</span>
                        <span><span class="oi oi-infinity mr-2"></span> @Trail.Length km</span>
                    </div>
                    <p class="mt-4">@Trail.Description</p>
                </div>
            </div>
            <div class="drawer-controls">
                <button class="btn btn-secondary" @onclick="@(() => _isOpen = false)">Close</button>
            </div>
        }
    </div>
</div>
 
@code {
    private bool _isOpen;
 
    [Parameter] public Trail? Trail { get; set; }
 
    protected override void OnParametersSet()
    {
        if (Trail != null)
        {
            _isOpen = true;
        }
    }
}

Define a component parameter

A component parameter is defined as a public property which is decorated with the Parameter attribute. Blazor uses this attribute to find component parameters during the execution of the SetParametersAsync lifecycle method we looked at earlier in the chapter. During this lifecycle method, the reflection sets the parameter values.

We’re using the OnParametersSet lifecycle method to trigger the drawer sliding into view. As we learned earlier, this lifecycle method is run every time the components parameters change. This makes it perfect for our scenario as we can use it to trigger opening the drawer.

Drawer implementation

Opening and closing the drawer is done using CSS. When a new trail is passed in, the isOpen field is set to true, this triggers the logic at the top of the component to render the slide CSS class.

<div class="drawer-wrapper @(_isOpen ? "slide" : "")">

In the app.css file (this is found in wwwroot > css folder) we need to add the styles to the bottom of the file.

.drawer-mask {
    visibility: hidden;
    position: fixed;
    overflow: hidden;
    top: 0;
    right: 0;
    left: 0;
    bottom: 0;
    z-index: 99;
    background-color: #000000;
    opacity: 0;
    transition: opacity 0.3s ease, visibility 0.3s ease;
}
 
.drawer-wrapper.slide > .drawer-mask {
    opacity: .5;
    visibility: visible;
}
 
.drawer {
    display: flex;
    flex-direction: column;
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    width: 35em;
    overflow-y: auto;
    overflow-x: hidden;
    background-color: white;
    border-left: 0.063em solid gray;
    z-index: 100;
    transform: translateX(110%);
    transition: transform 0.3s ease, width 0.3s ease;
}
 
.drawer-wrapper.slide > .drawer {
    transform: translateX(0);
}
 
.drawer-content {
    display: flex;
    flex: 1;
    flex-direction: column;
}
 
.trail-details {
    padding: 20px;
}
 
.drawer-controls {
    padding: 20px;
    background-color: #ffffff;
}

The two key parts of the styles above are the transform: translateX properties on the .drawer and .drawer-wrapper.slide > .drawer classes. Without these properties, the drawer would sit in its open position, in full view. Figure 3.12 shows the effect of the properties on the drawer.

The transform property on the .drawer class repositions the drawer off the right-hand side of the screen by 110% of its width. The transform property on the .drawer-wrapper.slide > .drawer class repositions it back to its default, bringing it into view.

Updating the HomePage component

To pass the trail into the TrailDetails component, we use attributes when defining the component in the parent. The parent for us is the HomePage component. Listing 3.10 shows the HomePage component updated with the TrailDetails component.

@page "/"
@inject HttpClient Http
 
@if (_trails == null)
{
    <p>Loading trails...</p>
}
else
{
    <TrailDetails Trail="_selectedTrail" />
    <div class="row row-cols-1 row-cols-md-2">
 
        @foreach (var trail in _trails)
        {
            <div class="col mb-4">
                <TrailCard Trail="trail" />
            </div>
 
        }
 
    </div>
}
 
@code {
    private IEnumerable<Trail> _trails;
    private Trail? _selectedTrail;
 
    protected override async Task OnInitializedAsync()
    {
        try
        {
            _trails = await Http.GetFromJsonAsync<IEnumerable<Trail>>("trails/trail-data.json");
        }
        catch (HttpRequestException ex)
        {
            Console.WriteLine($"There was a problem loading trail data: {ex.Message}");
        }
    }
}

In the HomePage component we have defined a field called _selectedParameter which will store the selected trail. We then pass this into the TrailDetails component using an attribute style syntax.

<TrailDetails Trail="_selectedTrail" />

The attribute name matches the component parameter we defined on the TrailDetails component. It is important that the case also matches otherwise Blazor will consider it a regular HTML attribute and ignore it. If you’re using an IDE such as Visual Studio for Windows or Mac, or JetBrains Rider you will receive IntelliSense to help you do this. Visual Studio Code also has IntelliSense support for Blazor via the C# extension.

Passing data from a child to a parent

We have successfully used a component parameter to define the API of the TrailDetails component, but we can’t see the fruit of our labor yet. In order to see something happen on screen we need to be able to select a trail to display. To do this we need to be able to pass that information up from the TrailCard component to the HomePage component.

To do this we are going to use component parameters to define an event on the TrailCard. This event will pass the selected trail, the HomePage component can then handle this event and pass the trail to the TrailDetails component to display.

<div class="card" style="width: 18rem;">
    <img src="@Trail.Image" class="card-img-top" alt="@Trail.Name">
    <div class="card-body">
        <h5 class="card-title">@Trail.Name</h5>
        <h6 class="card-subtitle mb-3 text-muted"><span class="oi oi-map-marker"></span> @Trail.Location</h6>
        <div class="d-flex justify-content-between">
            <span><span class="oi oi-clock mr-2"></span> @Trail.Time</span>
            <span><span class="oi oi-infinity mr-2"></span> @Trail.Length km</span>
        </div>
        <button class="btn btn-primary mt-3" @onclick="@(() => OnSelected?.Invoke(Trail))">View</button>
    </div>
</div>
 
@code {
    [Parameter] public Trail Trail { get; set; }
    [Parameter] public Action<Trail> OnSelected { get; set; }
}

We define the event as a delegate of type Action<Trail>. This allows us to pass the trail which this TrailCard is displaying back to the parent component. This happens when the View button is clicked. We handle the buttons click event using Blazor’s @onclick event.

With the TrailCard updated all that’s left to do is handle the event in the HomePage component. First, we need to add a method to the code block which will be called whenever an event is raised.

private void HandleTrailSelected(Trail trail)
{
    _selectedTrail = trail;
    StateHasChanged();
}

Code explained

This method accepts the selected trail, assigns it to the _selectedTrail field. However, in order to see anything happen we must call StateHasChanged – this is to let Blazor know that we need the UI to update. The reason we must do this manually, is that Blazor can’t know the intent of our code. It has no idea that our custom event should trigger a re-render of the UI.

There are some cases where this manual control over re-renders is preferred, however, in most cases this is just an extra line of code which must be added to achieve the desired effect. There is another way. We can use a different type to define our event on the TrailCard called EventCallback. By using this type for our event, Blazor will automatically call StateHasChanged on the component which handles the event, removing the need to manually call it.

To take advantage of this we can update the component parameter on TrailCard and update how the event is invoked.

<div class="card" style="width: 18rem;">
    <img src="@Trail.Image" class="card-img-top" alt="@Trail.Name">
    <div class="card-body">
        <h5 class="card-title">@Trail.Name</h5>
        <h6 class="card-subtitle mb-3 text-muted"><span class="oi oi-map-marker"></span> @Trail.Location</h6>
        <div class="d-flex justify-content-between">
            <span><span class="oi oi-clock mr-2"></span> @Trail.Time</span>
            <span><span class="oi oi-infinity mr-2"></span> @Trail.Length km</span>
        </div>
        <button class="btn btn-primary mt-3" @onclick="@(async () => await OnSelected.InvokeAsync(Trail))">View</button>
    </div>
</div>
 
@code {
    [Parameter] public Trail Trail { get; set; }
    [Parameter] public EventCallback<Trail> OnSelected { get; set; }
}

Then, we can simply remove the StateHasChanged call from our handler in the HomePage component:

private void HandleTrailSelected(Trail trail)
{
    _selectedTrail = trail;
}

The final update is to assign the HandleTrailSelected method to the OnSelected event. We do this the same way we did to pass the selected trail into the TrailDetails component, using attributes.

<TrailCard Trail="trail" OnSelected="HandleTrailSelected" />

If all has gone to plan, then clicking the view button should trigger the drawer and display the trail. Clicking the close button at the bottom of the drawer will close it and allow a new trail to be selected.

Styling components

The styling is an important element to building any application and a powerful tool in delivering great UX. Look at the drawer we just built, the ability for it to slide in and out of the viewport was achieved using CSS, not C#.

There are two approaches to styling component-based applications such as Blazor:

  • Global styling
  • Scoped styling

As you would expect, global styles are classes which are declared on the global scope and can apply to any element which uses that class name or meets the selector for that class. Scoped styles are the opposite, a stylesheet is created for a specific component and any classes defined in it are made unique to that component using a unique identifier produced during the build process.

No matter which of these approaches you take to style your application, it is possible to combine it with CSS preprocessors. CSS preprocessors like SASS, allow CSS to be written in a more modular and maintainable way – taking advantage of features such as variables and functions.

Global styling

Global styling is the default method when building applications. This is how we have been styling Blazing Trails up to this point. To apply global styling, one or more stylesheets are added to the host page which, by default, is index.html in Blazor WebAssembly and _Host.cshtml in Blazor Server. The styles defined in those stylesheets are then available throughout the application.

Global styles are fantastic for creating a consistent look and feel across an application. For example, if all buttons needed to be blue with certain font size and rounded corners. This can be defined once in a global style and would apply to all buttons in the app:

button {
    font-size: 1rem;
    background-color: blue;
    border-radius: .25rem
}

This makes global styles an incredibly powerful tool because if we wanted to change how the buttons, or any aspect of the applications design looked, we can change the styles in one place, and the application is immediately updated.

Why a global CSS?

This global scope of styles can also cause some issues when developing larger applications. For example, if we wanted a certain button to be green with square corners rather than the global blue style above, we would need to add another style to the stylesheet. We would then need to apply the style to the particular button. That doesn’t seem too bad, but think of this happening many times over, you end up with a stylesheet which is full of one-off styles or niche styles. You could say this is down to bad design or lack of maintenance, which would be fair, but it still doesn’t stop it happening.

Making changes to global styles can also be cumbersome. Constantly scrolling up and down a stylesheet with 100’s of lines of style classes can become tedious. Especially, when changes need to be made in multiple places.

There are mitigations for this of course, using a CSS preprocessor like SCSS allows the global styles to be broken up and kept next to the component they are for in the project structure. This makes working with them much easier and more efficient. There is also another option which has come about with the rise of SPA frameworks, scoped CSS.

Scoped styling

Scoped styling works by allowing a developer to create styles which only effect a certain component in the application – this is done by creating a stylesheet with the same name as the component. During the build process, Blazor generates unique IDs for each component and then the styles for that component are rescoped using each ID.

To get a feel for this, let’s rework the styles for the TrailDetails component we just built to use scoped CSS. To do this, we first need to create a new stylesheet called TrailDetails.razor.css, then take all the styles we added to app.css for the TrailDetails component and move them to this file.

It is important that we name the file this way otherwise Blazor won’t pick it up and associate its styles with the TrailDetails component. If you’re using Visual Studio, a nice effect of this naming convention is the file nesting in Solution Explorer.

Example of scoped styling

When using scoped CSS, there will be a lot of stylesheets dotted around the application. Adding each and every one of them to the host page would be tedious and difficult to maintain. So, what Blazor does as part of the build process is bundle all the styles from the various stylesheets into a single stylesheet. This means we just need to reference that one stylesheet in our host page. The file has a naming convention of [ProjectName].styles.css. As our project is called BlazingTrails.Web, the file will be called, BlazingTrails.Web.styles.css. Listing 3.13 demonstrates where to reference the file.

<!DOCTYPE html>
<html>
 
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>BlazingTrails.Web</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="BlazingTrails.Web.styles.css" rel="stylesheet" />
</head>

If we run the project and select a trail to open the drawer, we can use the browsers dev tools to look at the HTML and styles produced.

Inspecting the HTML of the application in a browser shows a unique ID applied to each HTML element belonging to the TrailDetails component
Inspecting the HTML of the application in a browser shows a unique ID applied to each HTML element belonging to the TrailDetails component

Each HTML element belonging to the TrailDetails component now has a unique attribute applied to it. This attribute follows the format of b-[uniqueID]. If we then select an element to inspect its styles.

Each selector for the styles in the TrailDetails.razor.css file has been rewritten to use the unique ID Blazor generated for the component. Doing this is what scopes the style to that component and stops the style effecting another element in another component.

Global styles can still have an effect

If you use scoped styles and nothing else in your application, then what I’m about to say isn’t an issue. However, if you have some global styles and some scoped styles then you may still run into issues.

To give an example, let’s say we had the following CSS class called .drawer in our global CSS file, in addition to the one we have in the TrailDetails components scoped stylesheet:

.drawer {
    border: 5px solid lawngreen;
}

As you can see, using scoped styles doesn’t make components immune from the standard behavior of CSS. This is something to think about when deciding on how to style your components, mixing global and scoped styles could make things more complicated.

Using CSS preprocessors

Whether you choose to use global styles, scoped styles, or a mix of both, you can still leverage the power of CSS preprocessors. Preprocessors work in a similar way as TypeScript does for JavaScript, as a superset language. They provide access to a richer feature set than CSS provides alone.

There are many options out there when it comes to CSS preprocessors, but the main players are:

They all provide similar feature sets which offer the following enhancements over regular CSS, just with different syntaxes.

  • Mixins – reusable groups of styles
  • Variables – works the same way as variables in C#
  • Nesting – the ability to define the scoped of a style by writing it within another
  • Import – allows us to organize large CSS files into smaller more focused files. Then import common aspects such as variables.

Choosing a preprocessor largely comes down to which syntax you prefer. My favorite preprocessor is SCSS (https://sass-lang.com/). It has a syntax very similar to regular CSS which makes everything easy to read. Also, it has been around for a very long time so there’s lots of documentation and blog posts out there to help if you get stuck.

Integrating a CSS preprocessor

I’m going to show you how to integrate SCSS into a Blazor app, specifically when using scoped CSS. There are two ways we can integrate SCSS into our application, using JavaScript tools or not using .NET tools.

If you don’t want to use any JavaScript tools in your application, then I would suggest of the following options.

  • WebCompiler from Mads Kristensen (https://marketplace.visualstudio.com/items?itemName=MadsKristensen.WebCompiler). This hasn’t had any meaningful updates for a couple of years, but it does still appear to work.
  • WebCompiler by excubo-ag (https://github.com/excubo-ag/WebCompiler). This is forked from Mads WebCompiler and is looking like a promising project. It uses a dotnet CLI tool to perform the compilation of SCSS files and ties in with MSBuild. However, it currently only supports SCSS. Meaning if you are using a different preprocessor such as LESS or Stylus you are out of luck. Configuration is a bit difficult and there is limited documentation.

The option I prefer, and I’m going to show you, is to use a mix of NPM and MSBuild. This does require having an up to date version of NodeJS installed which can be downloaded at https://nodejs.org/. The version I’m using is 14.15.0 and is the latest LTS version.

Integrate tools

We’re going to use a tool called dart-sass (https://sass-lang.com/dart-sass) which we can install as an NPM package called sass (https://www.npmjs.com/package/sass). We’re then going to use MSBuild to call this tool during the build process, specifically at the start of the build process. This is important as we need to compile our SCSS files to CSS before Blazor’s compiler runs so it can pick up the compiled CSS files and bundle them into the single [ProjectName].style.css file we talked about earlier.

This new SCSS version of the TrailDetails styles only has one slight modification, it’s using the nesting feature from SCSS. It will allow us to confirm that the compilation steps worked and that the SCSS generates CSS.

Conclusion

Finally, we did it! We understand how working with Blazor’s component model and it works. If you have any problem, you have the source code on GitHub. If you have any question, please use the forum.

Happy coding!