DEV Community

James Eastham
James Eastham

Posted on

Front End for the .NET Developer with Blazor Web Assembly

Throughout my entire development career, I've always found web to be the best way of deploying an interface to any software I've written.

Desktop never really appealed to me, maybe that comes from growing up almost entirely in an age where everything was web-based.

However, coming from a .NET background, there are some downsides to this web-first approach.

ASP Web Forms were ok, and MVC built upon that a little bit. But nothing ever really came close to the magic of the big Javascript frameworks.

That lead me down a rabbit hole I never truly got to grips with, the magical world of Angular and React.

I've toyed with both, completed tutorials and written production apps. Still, neither of them really clicked fully (I did massively prefer React, but that's a personal preference thing).

Typescript improved upon this pain slightly, bringing strong typing and a more C#-esque way of development.

And then, along came Blazor...

Blazor

I'm not going to dive into too much detail around Blazor, there is plenty of other articles around the web regarding that (the official docs are a great place to start).

To quickly summarise though, Blazor is Microsoft's challenge to the big JS-based front end frameworks. It allows rich and interactive front end apps to be written entirely in C#. Not a line of Javascript insight.

And with that, let's dive into some code.

Getting Started

To get started with Blazor, I'm going to add a front end to the team-service I've been building throughout this series of articles. It's a learning process for me as well, so stay with me!

Running the below commands is all it takes to get started with a new Blazor app.

dotnet new -i Microsoft.AspNetCore.Blazor.Templates::3.2.0-preview1.20073.1 -o LeagueManager
dotnet new blazorwasm -o LeagueManager
Enter fullscreen mode Exit fullscreen mode

The first install's the latest Blazor Web Assembly project template (at the time of writing, it's 3.2.0 Preview 1). The second command creates a new project.

Following the two commands with the below, gets the sample app up and running.

cd .\LeagueManager\
dotnet run
Enter fullscreen mode Exit fullscreen mode

Alt Text

Out of the box, Blazor applications use Bootstrap and are created with some great examples of the key functionality (namely two-way model binding and making external requests).

First things first, let's add our own color scheme and font. I'll also remove a lot of the standard Blazor code leaving a much more basic project structure.

I'm also going to apply one of the principles I learned from React, which is to keep stateful components to an absolute minimum and make the majority of the application dumb.

For that, I'm going to add two additional folders on top of the pre-configured 'Pages' folder with two new folders:

  • Components - Dumb, layout components
  • Services - Stateful components

Initially, I considered disposing of the Pages folder as well. But keeping actual pages (with routes) in one place seemed more logical.

For this initial piece of front end of development, I need to be able to:

  • View a list of teams
  • Create a new team
  • View details about a specific team
  • Add players to a team

In my head, that gives me two pages.

  1. A list of all teams and the ability to create a new team
  2. A page to view a specific team containing a list of the team's players and corresponding data entry form for adding new players

Handling State

Before I get into much detail about the page structure, I first wanted to see how HTTP data access works in Blazor.

Turns out, the answer is extremely simple.

The answer is a huge +1 already for using Blazor as a front end framework. The code written to manage to retrieve data from an external HTTP service is almost identical to code that would run in any kind of .NET application.

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using LeagueManager.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;

namespace LeagueManager.Services
{
    public class TeamState
    {
        private string LastSearch = "";

        public IReadOnlyList<Team> TeamSearchResults { get; private set; }
        public bool SearchInProgress { get; private set; }
        public event Action OnChange;

        private readonly HttpClient _httpClient;
        private readonly ILogger<TeamState> _logger;

        public TeamState(
            HttpClient httpClient,
            ILogger<TeamState> logger)
        {
            this._httpClient = httpClient;
            this._logger = logger;
        }

        public async Task DeletePlayer(Player player)
        {
            try
            {
                this._logger.LogInformation($"{player.TeamId} - {player.Name} - {player.Position}");

                var httpContent = new StringContent(JsonSerializer.Serialize(player), Encoding.UTF8, "application/json");

                var postResult = await this._httpClient.DeleteAsync($"http://localhost:8080/team/{player.TeamId}/players");

                this._logger.LogInformation(postResult.StatusCode.ToString());

                if (postResult.IsSuccessStatusCode)
                {
                    NotifyStateChanged();
                }
                else
                {
                    throw new Exception(await postResult.Content.ReadAsStringAsync());
                }
            }
            catch (Exception ex)
            {
                this._logger.LogError(ex.Message);
                this._logger.LogError("Failure adding player");
            }
        }
        public async Task AddPlayer(Player player)
        {
            try
            {
                this._logger.LogInformation($"{player.TeamId} - {player.Name} - {player.Position}");

                var httpContent = new StringContent(JsonSerializer.Serialize(player), Encoding.UTF8, "application/json");

                var postResult = await this._httpClient.PostAsync($"http://localhost:8080/team/{player.TeamId}/players", httpContent);

                this._logger.LogInformation(postResult.StatusCode.ToString());

                if (postResult.IsSuccessStatusCode)
                {
                    NotifyStateChanged();
                }
                else
                {
                    throw new Exception(await postResult.Content.ReadAsStringAsync());
                }
            }
            catch (Exception ex)
            {
                this._logger.LogError(ex.Message);
                this._logger.LogError("Failure adding player");
            }
        }
        public async Task<Team> GetSpecific(string teamId)
        {
            this._logger.LogInformation("Running HTTP search");

            var team = await this._httpClient.GetJsonAsync<TeamSearchResponse>($"http://localhost:8080/team/{teamId}");

            return team.Team;
        }
        public async Task ReRunSearch()
        {
            await this.Search(LastSearch);
        }
        public async Task Search(string searchTerm)
        {
            try
            {
                this.LastSearch = searchTerm;

                this._logger.LogWarning("Running search");
                this.SearchInProgress = true;

                NotifyStateChanged();

                var searchResult = await this._httpClient.GetJsonAsync<TeamSearchResponse>($"http://localhost:8080/team?search={searchTerm}");

                if (string.IsNullOrEmpty(searchResult.Err))
                {
                    this.TeamSearchResults = searchResult.Teams;
                }

                this.SearchInProgress = false;

                NotifyStateChanged();
            }
            catch (Exception ex)
            {
                this._logger.LogError(ex, ex.Message);
                this._logger.LogError(ex, "Failure running search");
            }
        }
        public async Task CreateTeam(Team team)
        {
            try
            {
                this._logger.LogInformation($"{team.Name}");

                var httpContent = new StringContent(JsonSerializer.Serialize(team), Encoding.UTF8, "application/json");

                var postResult = await this._httpClient.PostAsync($"http://localhost:8080/team", httpContent);

                this._logger.LogInformation(postResult.StatusCode.ToString());

                if (postResult.IsSuccessStatusCode)
                {
                    NotifyStateChanged();
                }
                else
                {
                    throw new Exception(await postResult.Content.ReadAsStringAsync());
                }
            }
            catch (Exception ex)
            {
                this._logger.LogError(ex.Message);
                this._logger.LogError("Failure adding team");
            }
        }

        private void NotifyStateChanged() => OnChange?.Invoke();
    }
}
Enter fullscreen mode Exit fullscreen mode

A reasonably straightforward class that uses Dependency Injection to add a HttpClient and an ILogger. From there, it is mostly just a set of different methods handling the various different CRUD operations.

** Note the terrible hardcoded URL, this is a work in progress :) **

There are a couple of front-end/Blazor specific parts that I do just want to point out, however.

public IReadOnlyList<Team> TeamSearchResults { get; private set; }
public bool SearchInProgress { get; private set; }
public event Action OnChange;
Enter fullscreen mode Exit fullscreen mode

These three properties are all relevant to HTTP in Blazor.

The Blazor application runs completely in a web browser. Therefore this TeamState object is specific to the client accessing the site.

Therefore, whenever search runs we can hold a copy of the returned data in a property for easy retrieval if the search needs to happen again.

SearchInProcess holds a value indicating that the search method is running. Imagine a front end in which a loading bar appears during a search, that could be bound to this property.

The OnChange property is probably the most important of the three. It's linked directly to this line of code here:

private void NotifyStateChanged() => OnChange?.Invoke();
Enter fullscreen mode Exit fullscreen mode

So, the OnChange property can be subscribed to by an external method. NotifyStateChanged then invokes that external method.

Let's dive into a real example. Here is the code for the Team List page.

@page "/teams"

@using LeagueManager.Services
@using LeagueManager.Components.Teams

@inject TeamState state

<div id="team-search">
    <div id="new-team">
        <CreateTeam OnSave="this.CreateTeam" />
    </div>
    <div id="search-area">
        <Search OnSearch="state.Search" />
    </div>
    <div id="search-results">
        <SearchResults Teams="state.TeamSearchResults" />
    </div>
</div>

@code { 

    protected override async Task OnInitializedAsync()
    {
        state.OnChange += StateHasChanged;

        await state.Search(string.Empty);
    }

    public async Task CreateTeam(Team team)
    {
        await this.state.CreateTeam(team);

        await state.ReRunSearch();
    }
}
Enter fullscreen mode Exit fullscreen mode

An instance of the TeamState object is injected into the page, and on PageLoad (OnInitializedAsync is Blazor's PageLoad event) the state Blazor page StateHasChanged event is subscribed to the OnChange event of the state injected state class.

So, when a search runs within the TeamState object the OnChange event is triggered. In this instance, the StateHasChanged event of the Blazor page will execute. Taking wording straight from the Microsoft docs:

"StateHasChanged notifies the component that its state has changed. When applicable, calling StateHasChanged causes the component to be rerendered."

Binding any page to the OnChange event of the state will, therefore, cause the page to be re-rendered whenever our state components notify that something has changed. Cool!

Model Binding

Now that we can retrieve data from the server and give that to the front end, it'd be fantastic to actually get it to display on the page.

Displaying data

Sticking with the best front end practices I've taken from JS, my Teams.razor page handles all of the state management. It then passes the data down to components to manage the display.

A good example of this is the SearchResults component.

<div class="col">
    <div class="row py-1">
        @if (Teams == null || Teams.Count == 0)
        {
            <p>No results found.</p>
        }
        else
        {
            <table class="table">
                <thead>
                    <tr>
                        <th scope="col">#</th>
                        <th scope="col">Name</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach (Team team in Teams)
                    {
                        <tr @key="team.Id">
                            <th scope="row">
                                <a href="/teams/@team.Id">@team.Id</a>
                            </th>
                            <td>@team.Name</td>
                        </tr>
                    }
                </tbody>
            </table>
        }
    </div>
</div>

@code
{
    [Parameter]
    public IReadOnlyList<Team> Teams { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

The above snippet raises one of the key parts of Blazor functionality. That being, the Parameter attribute.

The parameter attribute indicates that a value can be passed into the component by way of an HTML element.

In this case, there is a parameter that holds a list of Team objects.

This component can certainly be classified as dumb.

It takes a list of objects and either shows 'no results found', or creates an HTML table and loops through each Team object to create the table rows.

C#. In the browser. It's a magical time to be a .NET developer.

Creating a new record

The final piece of this front end puzzle is creating new records.

My absolute favourite feature of both Angular and React is the ability to be able to initialize a new instance of an object and bind that straight to an input in the HTML.

Luckily, Blazor has taken notes from the JS big boys and offers a similar kind of function.

For that, let's have a look at the CreatePlayer component.

<EditForm Model=@newPlayer>
    <div class="col">
        <div class="input-group mb-3">
            <input type="text" class="form-control" placeholder="Name" aria-label="Search Term" @bind="newPlayer.Name">
        </div>
        <div class="input-group mb-3">
            <InputSelect @bind-Value=newPlayer.Position class="custom-select">
                <option value="GK">Goalkeeper</option>
                <option value="DEF">Defender</option>
                <option value="MID">Midfielder</option>
                <option value="ST">Striker</option>
            </InputSelect>
        </div>
        <div class="row py-1">
            <div class="ml-auto">
                <button @onclick="() => OnSave.InvokeAsync(newPlayer)" type="button" class="btn btn-danger px-5">
                    Add +
                </button>
            </div>
        </div>
    </div>
</EditForm>
@code
{
    [Parameter]
    public EventCallback<Player> OnSave { get; set; }

    [Parameter]
    public string TeamId { get; set; }

    private Player newPlayer = new Player();

    protected override Task OnInitializedAsync()
    {
        newPlayer.TeamId = this.TeamId;
        return base.OnInitializedAsync();
    }
}
Enter fullscreen mode Exit fullscreen mode

When the page is loaded, a new Player object is created. OnInitialization, the TeamId of the newly created player object is assigned from an input value.

A TeamId is a requirement for a player object, so it probably makes sense to implement some kind of null check on the TeamId property just in case. But that's a job for another day.

From there, an EditForm component is added to the HTML. An EditForm parses out to a standard HTML form. However, it enables model binding between the DOM and the C# object.

Using the @bind element, the HTML becomes reasonably straightforward. An input is bound to the player name property and a select dropdown is bound to the position property.

When the form is submitted, the OnSave event callback is invoked.

What the hell is an EventCallback?

For that to make a little more sense, let's take a quick look at a component that consumes this CreatePlayer component.

TeamMetadata.razor

@if (TeamData == null)
{
    <p>Loading...</p>
}
else
{
    <h1>@TeamData.Name</h1>
    <h2>Current Players</h2>
    <CreatePlayer OnSave="OnAddPlayer" TeamId="@TeamId" />
    <div>
        <PlayerList Players="@TeamData.Players" OnDelete="OnDeletePlayer" />
    </div>
}

@code {
    [Parameter]
    public Team TeamData { get; set; }

    [Parameter]
    public string TeamId { get; set; }

    [Parameter]
    public EventCallback<Player> OnAddPlayer { get; set; }

    [Parameter]
    public EventCallback<Player> OnDeletePlayer { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

ViewTeam.razor

@page "/teams/{TeamId}"

@using Microsoft.Extensions.Logging
@using LeagueManager.Components.Teams

@inject TeamState state

<TeamMetadata TeamData="@teamData" TeamId="@TeamId" OnAddPlayer="state.AddPlayer" OnDeletePlayer="state.DeletePlayer" />


@code {
    [Parameter]
    public string TeamId { get; set; }

    private Team teamData { get; set; }

    protected override async Task OnInitializedAsync()
    {
        state.OnChange += StateHasChanged;

        teamData = await state.GetSpecific(this.TeamId).ConfigureAwait(false);
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        teamData = await state.GetSpecific(this.TeamId).ConfigureAwait(false);
    }
}
Enter fullscreen mode Exit fullscreen mode

The two code snippets above are for the View Team page, and the corresponding TeamMetadata component within it.

Sticking with the practice of having as little components managing state as possible, the ViewTeam page holds all the logic for loading the players and the creation of a new player.

The state.AddPlayer method is passed down into the OnAddPlayer parameter of the TeamMetadata component, and from there into the OnSave parameter of the CreatePlayer component.

So when the save button is clicked in the CreatePlayer component that event bubbles all the way up to state.AddPlayer method. The newly created and data-bound Player object goes along for the ride and is sent off to our backend server for creation.

I will say it again...

C#. In the browser. It's a magical time to be a .NET developer.

In Summary

Sometimes I can waffle a little bit with these summary sections. This time, not so much.

Blazor is one of the best web technologies I have ever worked with. I'm probably biased, being a .NET developer. But I no longer need to stumble my way through the world of Javascript.

And that my friends is a magical place to be.

Top comments (0)