.NET Tools
Essential productivity kit for .NET and game developers
Building A Blazor Farm Animal Soundboard
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.
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:
- C# Classes and Data
- Razor View and Components
- 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.
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.
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.
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.