Building A Blazor Farm Animal Soundboard

Khalid Abuhakmeh

How’s it going, fellow Blazorinos? Here at JetBrains, we’ve been diving face-first into Blazor, improving the development experience for Blazor lovers.

For those unfamiliar with Blazor, but still familiar with front-end web development, you can think of Blazor as a front-end framework similar to VueJs or React. Blazor allows developers to build interactive user interfaces (UI) using C#, Razor, and SignalR. Introduced in ASP.NET Core 3, the architecture of Blazor leverages shareable C# code, which can run on the server and client. Developers can also expect to ship self-contained, fully running in a web browser, Blazor apps with WebAssembly.

In this beginner tutorial, we’ll be building a Farm Animal Soundboard. A soundboard is an app that lets the user push a button and play the associated sound. We’ll walk through some major elements of building a Blazor experience: Razor pages, Components, and JavaScript interoperability.

⚠️ Prerequisites

We recommend installing the latest ReSharper EAP or Rider EAP to get C# 9 support, as we’ll be using some C# 9 features, such as record types.

Note that I’ll be using Rider for this tutorial, but this all works with ReSharper as well.

To follow this blog post, try using the latest .NET 5 SDK. The .NET team has been hard at work, enhancing the Blazor experience in .NET 5, and we should take full advantage of that. This sample should work on previous versions of Blazor found in .NET Core 3.1, but I haven’t tested that.

We will also need images and audio of our favorite farm animals. Luckily, the sample project already has those assets ready to utilize. We have nine animals, along with their accompanying sounds. Adventurous folks can also choose to change the theme of this demo to whatever they would like.

🐄 What We’re Building

Let’s take a quick look at what we’re building and breakdown our application before we get started.

Blazor Farm Animal Soundboard

We can see that we are using cards to display an animal’s image and allow our users to play an audio sound.

We can think of the elements in our UI in three major parts:

  1. C# Classes and Data
  2. Razor View and Components
  3. JavaScript Interoperability

We’ll start from the beginning of our list, where most C# devs will be comfortable, and then work our way to the “hardest” part.

🚦 Getting Started

In Visual Studio and ReSharper, use the Blazor App template, and then pick Blazor Server App. When using Rider, create a new Blazor Server App (under ASP.NET Core Web App). We can call the solution Farm. Once we have our solution, we can run the project to see that everything is working.

Rider's Start New Solution Blazor App

Let’s start modifying the Blazor template.

📚 The C# Classes And Data

We can store static data in C# classes. Taking this additional step will ensure that our Razor views stay compact and readable. Since we’ll be dealing with Animals, let’s create a static class that will store each new addition to our farm. Under the Data folder, create a C# file named Animals. Add the following C# code:

public static class Animals
{
    public static IEnumerable<AnimalInfo> All => new[]
    {
        new AnimalInfo("Cat", "The barn yard cat is a staple of many farms."),
        new AnimalInfo("Chicken", "Providing fresh eggs and constant clucking."),
        new AnimalInfo("Cow", "Cow's are the source of milk and beef."),
        new AnimalInfo("Dog", "Every farmer needs a trusty dog to keep watch."),
        new AnimalInfo("Donkey", "The trusty animal can make hard labor easier."),
        new AnimalInfo("Horse", "Help farmers cover long distances faster. YeeHaw!"),
        new AnimalInfo("Pig", "These messy animals are fun to have around."),
        new AnimalInfo("Rooster", "Helping farmers wake up early everywhere."),
        new AnimalInfo("Sheep", "A great source of wool for those cold winters.")
    };

    public sealed record AnimalInfo(string Name, string Description)
    {
        public string ImageUrl => $"/img/{Name.ToLowerInvariant()}.png";
        public string WavUrl => $"/audio/{Name.ToLowerInvariant()}.wav";
    }
}

The code uses the new C# 9 record type to store information about our farm animals. We also have two helper properties that will produce an ImageUrl and WavUrl. Our images and audio paths are stored conventionally and use the Name property to resolve each resource’s complete location. Let’s move onto something more interesting, the Razor implementation.

🪒 Razor View and Components

Blazor utilizes Razor as its rendering engine. For .NET developers coming from ASP.NET MVC or Razor pages, this syntax will be familiar. In our Index.razor file, we’ll be building our animal grid. We’ll preemptively design our Index page, thinking about how we may want to instantiate each card.

@page "/"
@using Farm.Data

<h1>
    <i class="oi oi-home" aria-hidden="true"></i>
    Old McKhalid's Farm Animals
</h1>

<div class="container-fluid">
    <div class="row equal">
        @foreach (var animal in Animals.All)
            {
                <Animal
                    Name="@animal.Name"
                    ImageUrl="@animal.ImageUrl"
                    WavUrl="@animal.WavUrl">
                    @animal.Description
                </Animal>
            }
        </div>
</div>

We first notice the conciseness of our Razor view. There are 21 lines in total, and six of those lines are a formatting choice around the Animal component. We are also referencing the namespace containing our C# data records along with the static class and its collection Animals.All.

The core of the soundboard lies in our reuseable Animal component. We can pass our properties into parameter placeholders for Name, ImageUrl, WavUrl. Along with Parameters, we also allow for child content within the Animal tag.

To create the Animal component, let’s add a new Blazor Component named Animal.razor under the Shared directory.

Create Blazor Component In Rider

Let’s look at the entire implementation of our Animal component, then break down the essential parts.

@inject IJSRuntime Js
@implements IDisposable

<div class="col-3 d-flex pb-3">
    <div class="card" style="width: 18rem;">
        <img class="card-img-top" src="@ImageUrl" alt="Card image cap">
        <div class="card-body">
            <h5 class="card-title">@Name</h5>
            <p class="card-text">
                @ChildContent
            </p>
            @if (IsPlaying)
            {
                <a class="btn btn-primary btn-block" @onclick="StopAudio">
                    <i class="oi oi-media-stop" aria-hidden="true"></i>
                    Stop Sound
                </a>
            }
            else
            {
                <a class="btn btn-primary btn-block" @onclick="PlayAudio">
                    <i class="oi oi-media-play" aria-hidden="true"></i>
                    Play Sound
                </a>
            }
            <audio @ref="Audio">
                <source src="@WavUrl" type="audio/wav">
                Your browser does not support the audio element.
            </audio>
        </div>
    </div>
</div>

@code {

    bool IsPlaying { get; set; }

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

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

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

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    private DotNetObjectReference<Animal> animal;
    private ElementReference Audio { get; set; }

    private async Task PlayAudio()
    {
        await Js.InvokeVoidAsync("playAudio", Audio);
        IsPlaying = true;
    }

    private async Task StopAudio()
    {
        await Js.InvokeVoidAsync("stopAudio", Audio);
        IsPlaying = false;
    }

    [JSInvokable]
    public async Task OnEnd()
    {
        IsPlaying = false;
        StateHasChanged();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            animal = DotNetObjectReference.Create(this);
            await Js.InvokeVoidAsync("initAudio", Audio, animal);
        }

        await base.OnAfterRenderAsync(firstRender);
    }

    public void Dispose()
    {
        animal?.Dispose();
    }
}

Razor components are a hybrid of HTML, Razor, and C#. For folks coming from the front-end development world, this should be reminiscent of Vue and React development.

At the top of our Animal file, we are injecting an IJSRuntime dependency, which will allow us to interact with client-side JavaScript and DOM elements. We’ll see the Js variable used later in our @code block.

Moving through the HTML, we can see the use of the @ symbol. Throughout the markup, we are placing our parameters, allowing Blazor to render their values. In the middle of our HTML block, we see a Razor if/else block. Blazor will perform state management as our IsPlaying property changes values, switching which HTML element our client renders accordingly. Finally, we have our audio HTML tag, which we decorate with the @ref attribute. The @ref keyword allows Blazor to hold a reference to any DOM element and pass it to our JavaScript implementations.

The @code block is likely the most unfamiliar part of the Razor file to new Blazor developers. We can think of the @code section as our class definition, which is a reminder that each .razor file is also a C# class. We define private members in the code block, the parameters we saw earlier in our Index.razor file, and interactivity methods.

The ParameterAttribute allows the properties they decorate to be assigned values by the component consumer. This attribute is critical for anyone building reusable components.

Other notable “Blazorisms” include the classes RenderFragment and ElementReference. The RenderFragment type allows us to accept child content when using our component. The markup located within the Animal tag is considered the child content.

<Animal
Name="@animal.Name"
ImageUrl="@animal.ImageUrl"
WavUrl="@animal.WavUrl">
    THIS IS CHILD CONTENT!
</Animal>

We can use the @ref attribute with the ElementReference type. The type allows us to hold a DOM reference to an HTML element; we’ll use it to pass our audio tag to our JavaScript to play and stop our animals’ sound.

<audio @ref="Audio">
    <source src="@WavUrl" type="audio/wav">
    Your browser does not support the audio element.
</audio>

In our case, the @ref attribute maps directly to our private Audio property.

ElementReference Audio { get; set; }

Let’s look at our PlayAudio and StopAudio methods. We bind the methods to our component’s button elements utilizing the @onclick binding. The Razor binding should not be confused with HTML’s onclick attribute.

@if (IsPlaying)
{
    <a class="btn btn-primary btn-block" @onclick="StopAudio">
        <i class="oi oi-media-stop" aria-hidden="true"></i>
        Stop Sound
    </a>
}
else
{
    <a class="btn btn-primary btn-block" @onclick="PlayAudio">
        <i class="oi oi-media-play" aria-hidden="true"></i>
        Play Sound
    </a>
}

Let’s take a look at the methods themselves and the utilization of IJSRuntime. These methods interact with our audio DOM element, so we utilize the Js property to invoke our JavaScript functions. These methods are also in charge of managing the state of IsPlaying, toggling the value from true and false.

private async Task PlayAudio()
{
    await Js.InvokeVoidAsync("playAudio", Audio);
    IsPlaying = true;
}

private async Task StopAudio()
{
    await Js.InvokeVoidAsync("stopAudio", Audio);
    IsPlaying = false;
}

Another essential attribute when dealing with JavaScript interoperability is the JsInvokeAttribute. The feature allows our client-side JavaScript to call a .NET method using SignalR. We create this bridge using the DotNetObjectReference class. We need to Create the reference in our OnAfterRenderAsync method because our component is accessible.

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        animal = DotNetObjectReference.Create(this);
        await Js.InvokeVoidAsync("initAudio", Audio, animal);
    }

    await base.OnAfterRenderAsync(firstRender);
}

Looking at this component, we can see the significant elements of what it takes to build a reusable Razor component. From injected services, Blazor specific types, and JavaScript interoperability calls.

Let’s look at the JavaScript that our component will be calling.

😱 Did Someone Say JavaScript?!

Choosing Blazor as a front-end framework means writing less JavaScript, but it doesn’t mean that we’ll be writing no JavaScript. Blazor’s interoperability with JavaScript is a strength, and we should embrace the fact that we’ll be writing some script to make our UI experience’s function. For folks looking to avoid JavaScript altogether, I’m sorry to say that it’s likely not possible.

Luckily, in the context of this demo, the JavaScript is very minimal. Let’s create a new JavaScript file at /wwwroot/js/site.js and paste the following functions into the file.

function initAudio(element, reference){
    element.addEventListener("ended", async e => {
        await reference.invokeMethodAsync("OnEnd");
    });
}

function playAudio(element) {
    stopAudio(element);
    element.play();
}

function stopAudio(element) {
    element.pause();
    element.currentTime = 0;
}

Phew! That was painless. As we can see in the JavaScript, we are interacting with our reference elements found in our component. This is the real magic of Blazor, allowing for seamless server and client interactions with very little code.

Next, we’ll need to reference this script in our _Host.cshtml file, located in the Pages directory. Right about the reference to the blazor.server.js, we can add our new script file.

<script src="/js/site.js"></script>
<script src="_framework/blazor.server.js"></script>

We must reference our file before the Blazor script, as our script needs to be loaded to have correctly functioning Animal components.

Running Our App

All the pieces are now in place, and we should be able to run our farm soundboard.

Blazor Farm Animal Soundboard

Let me be the first to say the obvious, “the cow says moo”. We’ll notice the play button switching state as we play audio and when the audio reaches its end.

We did it! We have a functioning Blazor farm soundboard.

Conclusion

Blazor delivers on the promise of interactive web experiences, helping folks working with ASP.NET and C# bridge the front-end gap. It’s not as scary for folks new to Blazor as it first looks, and I hope this post convinces you to try it out.

The Blazor documentation does a great job explaining the fundamentals. I also found Ed Charbenau’s Blazor, A Beginners Guide a great starting point for anyone interested in the topic. Understanding the boundaries between .NET and the front-end will be the most unclear for folks. Understanding the framework provided by Blazor helps breakdown the elements of a Blazor app and helps keep the project from turning into an overwhelming task.

Anyone interested in seeing the final version of this project can fork it on GitHub. I hope you enjoyed this post, and please leave a comment below if you enjoyed it.

Subscribe

Subscribe to .NET Tools updates