DEV Community

Ondrej Polesny
Ondrej Polesny

Posted on • Updated on

How to Build an Interactive SPA with Blazor

Are you a .NET developer? Are you also a little bit jealous of the beautiful, responsive SPAs that JS folks create nowadays? Envy no more, Blazor is here to help you.

I used to do a lot of .NET coding in the past, so when JavaScript became the rising star a few years ago, I was sad that Microsoft did not really have an answer to that. Over time, I found my way to Vue.js, but things like npm, webpack, bundling, deploying and others were quite a big bite for me from the start. Earlier this year, I stumbled upon Blazor—a framework that enables developers to write .NET code compiled using Mono runs on Webassembly. That means it can run in all major present-day browsers. Plus, with Blazor, I was able to stay in Visual Studio (my comfort zone) and use the very same design patterns as with MVC. So did it deliver on its promises?

In this article, I will show you how to build a client-side Blazor app, add some functionality, use NuGet package, and communicate with JavaScript.

What Is the Project?

In the city where I live, Brno, we quite recently got residential parking zones and a lot of drivers, mainly foreigners, are confused when looking for a parking spot. Everyone is just afraid of the blue lines on roads. Therefore, I aimed to build a simple app that would check where the visitor is standing (using mobile phone GPS) and explain the rules for that particular spot in two sentences. Possibly navigate to the nearest parking machine. Simple, fast, responsive.

Data Model

When you're building a site of any size larger than your dog's homepage, you need to structure and store data and content somewhere.

Map markers

At the start, I needed to store the data of:

  • Parking zone
    • name of zone
    • coordinates of a few hundreds of points (red markers in the map) that mark each zone
    • restrictions
      • Visitor restriction
        • duration - when the rules are applied
        • description - what are the rules
      • Resident restriction
        • duration - when the rules are applied
        • neighbors - residents may be allowed to park in nearby areas
  • Parking machines
    • coordinates

Overall, we're looking at 4 models (Zone, Visitor restriction, Resident restriction, and Parking machines).

I tend to use a headless CMS whenever I can as it does not require any installation, runs in the cloud, delivers data via CDN, and features a REST API or better—an SDK for the platform I use. Building the content types and desired hierarchy is not a problem in any mature headless CMS such as Kontent, Contentful, or ContentStack.

Starting off the Blazor Template

The easiest way to start with Blazor is to let dotnet clone a template for you. I aimed for the client-side version of Blazor, but there are templates for server-side (communicating with browsers using SignalR) and server and client combined projects as well. To install Blazor templates, run the following command:

dotnet new -i Microsoft.AspNetCore.Blazor.Templates::3.1.0-preview3.19555.2

And to clone the template (client-side), use:

dotnet new blazorwasm

This gives you the base project structure:

  • Pages
  • Shared
  • wwwroot
  • css
    • index.html
  • Startup.cs

The boilerplate already uses basic stylesheets that make the site look nice. If you need to use yours (like I did — my additional stylesheets are here), put them into css folder and reference them using a <link> element in wwwroot/index.html. That file is the main wrapper around every page Blazor renders. Just make sure not to delete the <app> element :-). That's where the magic happens.

Blazor client-side app

You should see this screen when you run the project. If you don't, then:

  • check that you are running the latest version of Visual Studio 2019 Preview, have the latest version of .NET Core SDK and the latest version of Blazor templates (look here).
  • the problem may be connected with linking (especially when you see a non-descriptive error in the output window). In that case, either switch it off (look here) or add a linker config (look here or check out sample config file).

Creating a Map Using Blazor Page and Component

Now that the site is running, let's add a page with the map component. Add a new file Map.razor in the Pages folder.

Pages always contain a path on which they are accessible.

@page "/map"

The rest of the file is HTML markup of the page.

<h1>Map</h1>
<div>Location: @Longitude, @Latitude</div>
<Map />

You see I am already using razor syntax and rendering property values. Properties and methods can be either defined right in the markup using @code{ //implementation } block (that's what MS recommends) or separated into a "code behind" file. I prefer the latter as it makes the code more readable (especially if you plan more than two properties and one method). However, remember that the inline code has a preference over the separated file should there be any conflicts.

Add a new file Map.razor.cs. Visual Studio will place the file underneath the page with the same name. The new class needs to inherit from ComponentBase and define all used properties on the page. In my case, that is Longitude and Latitude.

public class MapBase : ComponentBase
{
  public decimal Latitude { get; set; }
  public decimal Longitude { get; set; }
}

Then you need to tell the page that there is a code behind:

@inherits MapBase

Adding a Map Component

The next piece is the Map component. Create a folder Components in the root of the project and a new file Map.razor. In my case, this file contains just HTML markup.

<div class="mapContainer">
  <div id="m"></div>
</div>

Adding Map JavaScripts to the Website

The map also needs a JavaScript that initializes it. The script is available online, so I can either reference it directly or copy it to wwwroot/js folder and reference a local copy. I chose the first option, therefore I need to add the following lines to the wwwroot/index.html:

<script type="text/javascript" src="https://api.mapy.cz/loader.js"></script>
<script type="text/javascript">Loader.load();</script>

I can't add the scripts to the component directly as Blazor does not allow it.

Next, I need to configure and initialize the map. This is a simple JS code defining where is the initial center of the map and how detailed the map should be. The best place to put the file is within wwwroot/js folder and reference it from wwwroot/index.html, just like the other scripts.

<script type="text/javascript" src="./js/map.js"></script>

Find the full file content here.

Invoking JavaScript from Blazor

The function for initialization needs to be called by Blazor at the moment of component rendering. The catch here is that Blazor renders the components multiple times during their lifecycle. The reason for that is while visitors interact with my site and change some data sets, the components need to react on those changes. They need to re-render. But for the map, I just need to execute the initialization script once. The boolean parameter firstRender in the OnAfterRenderAsync function override will enable you to do just that.

The communication between Blazor and JavaScript is possible through JSRuntime. It's a simple service you can inject into any component directly.

[Inject]
protected IJSRuntime JSRuntime { get; set; }

And, to execute a JavaScript function, you need to specify its name and provide data for its parameters (if it has any). If the function returns data, JSRuntime can bind it to the respective .NET data type automatically. In my case, the mapInit does not return anything, so I'm using object.

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        await JSRuntime.InvokeAsync<object>("mapInit");
    }
}

Identifying GPS Position Using NuGet Package

The map initialization function contains fixed coordinates for centering the map. That's not ideal. The site visitors would have to know where they are standing (or use another app to find out) and click on that spot on the map. What I can do instead is ask their browser for GPS coordinates. And I could implement it myself, but why reinvent the wheel when we can use NuGet?

I found a package AspNetMonsters.Blazor.Geolocation that implements a LocationService. This service can be injected into pages and components, and it handles the initial geolocation query automatically. But first, you need to register the service in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<LocationService>();
}

I am registering the service as a singleton so Blazor would provide the same instance to all requests. However, you may also use:

  • AddTransient - new instance is created every time
  • AddScoped - instance is scoped to the current request

There is also one JavaScript file that needs to be included in the body. Find it here and add it to /wwwroot/js/Location.js. Reference it the same way as before:

<script type="text/javascript" src="./js/Location.js"></script>

This is how the service is injected into the Map page:

[Inject]
protected LocationService LocationService { get; set; }

protected override async Task OnAfterRenderAsync(bool firstRender)
{
   if (firstRender)
   {
       await JSRuntime.InvokeAsync<object>("mapInit");

       var currentLocation = await LocationService.GetLocationAsync();
       await JSRuntime.InvokeAsync<object>("mapCenter", currentLocation.Latitude, currentLocation.Longitude);
   }
}

Invoking Blazor from JavaScript and Adding Services

But before I add that into the Map page, let's take a look at the last item on the list—invoking Blazor code from JavaScript. This covers the use case when a visitor clicks on a spot on the map. After that, the map should center to that point (handled by JavaScript) and invoke Blazor with the new coordinates. Blazor will check the new position against all parking zones and re-render respective components if necessary (parking allowed/forbidden).

DotNet.invokeMethodAsync('DontParkHere', 'SetLocation', coords.y.toString(10), coords.x.toString(10));

This code will invoke method SetLocation within DontParkHere namespace with two parameters—two GPS coordinates. Note that the method needs to be public, static, and decorated with [JSInvokable] attribute.

But if the method is static, how do we get the new coordinates into the Map component, execute the checks, and update the front end?

Let's first create a service in Services/MapService.cs. This service will hold an action delegate to a method in the Map page that we want to invoke whenever new coordinates arrive.

public class MapService
{
    static Action<Location> _callback;

    public void WatchLocation(Action<Location> watchCallback)
    {
        _callback = watchCallback;
    }
}

The Location type comes from the previously added NuGet package.

Now, we need to add the static method invokable from JavaScript.

[JSInvokable]
public static void SetLocation(string latitude, string longitude)
{
    var location = new Location
    {
        Latitude = Convert.ToDecimal(latitude),
        Longitude = Convert.ToDecimal(longitude),
        Accuracy = 1
    };

    _callback.Invoke(location);
}

And register the service in Startup.cs like we did earlier with the Location service:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<LocationService>();
    services.AddSingleton<MapService>();
}

Finally, I can update the Map page. I ensure the new service is injected:

[Inject]
protected MapService MapService { get; set; }

And add a method that will process checks every time new coordinates arrive:

protected void SetLocation(Location location)
{
    Console.WriteLine($"Check this position: {location.Latitude} {location.Longitude}");
    Latitude = location.Latitude;
    Longitude = location.Longitude;
    StateHasChanged();
}

You see that I need to call StateHasChanged to let the page know that it needs re-rendering as I changed the underlying data set. During the rendering process, I assign the MapService's delegate to this function:

protected override async Task OnAfterRenderAsync(bool firstRender)
{
   if (firstRender)
   {
       MapService.WatchLocation(this.SetLocation);

       await JSRuntime.InvokeAsync<object>("mapInit");

       var currentLocation = await LocationService.GetLocationAsync();
       await JSRuntime.InvokeAsync<object>("mapCenter", currentLocation.Latitude, currentLocation.Longitude);
   }
 }

After these changes, when you run the project and access URL /map your browser should first ask you for sharing your location (this request may be automatically dismissed as the local IIS Express does not use SSL by default). Simultaneously, you should see the map rendering and with every click on the map, the page should show you updated coordinates.

Summary

It did not take long to build a functional SPA on Blazor. It took me a while to get my workspace up to date and a day or two to understand how Blazor works. Then the time spent is comparable to any other .NET project.

I personally like the fact I can use C# everywhere and stick to the patterns I know from the past. And even if you need to use a JS component or combine Blazor with JS frameworks, it's possible through the JS Interop. The downsides that I see now are low performance on the first load and complicated debugging. Overall, I'm very interested to see Blazor mature over time, and I expect it to disrupt the JS frameworks reign.

Check out the whole implementation on GitHub.

Other articles in the series:

Top comments (0)