Building a Pokedex with Blazor and Azure
Blazor is a web framework that lets you create single page web applications using C# instead of JavaScript or TypeScript. Blazor is rapidly gaining momentum among C# and .NET developers since it lets them leverage their existing skills to build rich web applications. In this post, we’re going to build a Pokedex app with Blazor and deploy it as a static site using Azure blob storage.
You can take a look at the finished app here. You can also view the complete code here.
- Prerequisites
- Create the Project
- Fix the Sidebar
- Implement the Pokemon List Page
- Implement Paging
- Implement Pokemon Detail Page
- Deploy to Azure
Prerequisites
To develop the Pokedex app by following this post, you’ll need Visual Studio 2019 or later installed on your machine.
If you also want to deploy your app to Azure by following this post, you’ll also need:
- An Azure Subscription
- Azure PowerShell installed on your computer
- AzCopy on your computer (Make sure it’s added to your system path)
Create the Project
Before we continue, let me mention that Blazor has two hosting models. We are going to use the Blazor Web Assembly (Blazor WASM) model because it is much more like a modern single page application built using React or Angular. Open up a PowerShell prompt and run the following commands to create a Blazor WASM app from the default template:
dotnet new sln -n Pokedex dotnet new blazorwasm -n Pokedex dotnet sln add Pokedex/Pokedex.csproj
Now, let’s open up the solution Pokedex.sln in Visual Studio. We’re going to get rid of the following files which we don’t need:
- Pages\Counter.razor
- Pages\FetchData.razor
- Shared\SurveyPrompt.razor
Go ahead and run the app by running the following command in your PowerShell prompt:
dotnet watch run
The watch part of the command above allows for hot reload, which means that whenever you save the code in your editor, the app will automatically build and run again. After you run the above command, the app should automatically open up in your browser.
Fix the Sidebar
Before we get started with the serious stuff, let’s get some cosmetic issues out of the way. If you look at the app in your browser, you’ll see a purple side-menu. A classic Pokedex must be red, not purple, so let’s fix that! Open Shared\MainLayout.razor.css in Visual Studio and search for the following code:
.sidebar { background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); }
Replace the above block of code with the following:
.sidebar { background-color: darkred; }
If you look at the app now, you’ll see that the background color of the sidebar is red, but that we still have unnecessary menu items in the sidebar like Counter and FetchData. Before we remove those, let’s add a script to pull in a pokeball icon which we will use for one of our sidebar menu items. Open up wwwroot\index.html and paste the following script tag just before the </body> tag.
<script src="https://code.iconify.design/1/1.0.7/iconify.min.js"></script>
Now let’s open Shared\NavMenu.razor and search for the following code which defines the sidebar menu items:
<li class="nav-item px-3"> <NavLink class="nav-link" href="" Match="NavLinkMatch.All"> <span class="oi oi-home" aria-hidden="true"></span> Home </NavLink> </li> <li class="nav-item px-3"> <NavLink class="nav-link" href="counter"> <span class="oi oi-plus" aria-hidden="true"></span> Counter </NavLink> </li> <li class="nav-item px-3"> <NavLink class="nav-link" href="fetchdata"> <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data </NavLink> </li>
Replace this section with the following code which just defines one menu item called Pokemon with a pokeball icon:
<li class="nav-item px-3"> <NavLink class="nav-link" href="" Match="NavLinkMatch.All"> <span class="iconify" data-icon="gg:pokemon" data-inline="false"></span>Pokemon </NavLink> </li>
Before we move on to the next part, let’s remove the header in main section which contains the About link. We’ll do this so that we have the maximum amount of space to display the Pokemon. Remove the following code from Shared\MainLayout.razor:
<div class="top-row px-4"> <a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a> </div>
That’s it! We’re done with the sidebar and ready to move on to implementing the Pokemon list page.
Implement the Pokemon List Page
Our next task is to implement a page which will list Pokemon with their name, number and picture. To do that we need Pokemon data! Fortunately for us, a free Pokemon API already exists and it can provide us with all the data we need. What’s even better is that someone already wrote a C# client SDK for that API which we can pull in as a NuGet package. Run the following line in the Package Manager Console in Visual Studio to install the package:
Install-Package PokeApiNet
There’s a class in the PokeApiNet package called PokeApiClient, which we’ll be using in multiple pages in this project. The PokeApiClient class implements automatic caching, so we want to use the same instance of this class everywhere in the application. Fortunately, we can use the .NET Core dependency injection system in Blazor. We’ll register PokeApiClient as a singleton with the dependency injection container, and inject it into whichever components need it.
To register PokeApiClient as a singleton, open the program.cs file and add the following line within the Main method:
builder.Services.AddSingleton(new PokeApiClient());
Modern front-end web frameworks are based on components, and Blazor is no different. We are implementing a page that lists Pokemon, but each of the Pokemon listed will have their own name, number and picture. So it makes sense to separate the individual Pokemon into its own component. We’re going to style the individual Pokemon component as a card, so we’ll call the component PokemonCard.
In Visual Studio, right-click the Shared folder, hover over Add, click Razor Component, enter PokemonCard as the name and click Add. Replace the contents of the file with the following:
@using PokeApiNet <!-- If the Pokemon is null, return a blank card--> @if (Pokemon == null) { <div class="card" style="border:none"></div> } else { <div class="card" style="cursor:pointer"> <img class="card-img-top" src="@Pokemon.Sprites.FrontDefault" style="height:96px;width:96px;margin-left: auto; margin-right: auto;display: block;"> <div class="card-body"> <h5 class="card-title" style="margin-left: auto; margin-right: auto; display: block;"> <b>@Pokemon.Id</b> @Pokemon.Name </h5> </div> </div> } @code { //This is the model used for data binding in the markup above //The Parameter attribute indicates that this property is passed to the component during creation [Parameter] public Pokemon Pokemon { get; set; } }
This Razor component takes a parameter of type Pokemon during creation. This Pokemon object serves as the data model for the component. The Razor markup is used to generate the final HTML by binding to the Pokemon data model. You can see above that if our Pokemon model is null, a blank card is returned. Otherwise a card is returned which contains the Pokemon’s name and number in the title. The Pokemon’s image URL is used as the image for the card.
We’re going to use the PokemonCard component from the Pokemon list page, so let’s go ahead and create that component now. First, delete the Pages\index.razor file as we don’t need that anymore. In Visual Studio, right-click the Pages folder, hover over Add, click Razor Component, enter PokemonList as the name and click Add. This will add a file called PokemonList.razor to the Pages folder.
The PokemonCard component was relatively simple so we kept the code and the Razor markup in the same file. However the PokemonList component will have more C# code, so we will separate it into a code-behind file. To do this, right-click the Pages folder in Visual Studio, hover over Add, click on Class, enter the name as PokemonList and then click Add. This will add a PokemonList.cs file under the Pages folder.
Replace the contents of the PokemonList.cs file with the following code:
using Microsoft.AspNetCore.Components; using PokeApiNet; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Pokedex.Pages { public partial class PokemonList { //This injects the PokeClient from the dependency injection container [Inject] PokeApiClient PokeClient { get; set; } //Main data model property for the Razor component private List<Pokemon> Pokemon = new List<Pokemon>(); const int ItemsPerPage = 12; protected override async Task OnInitializedAsync() { await LoadPage(); } //Helper method to retrieve an individual pokemon from the list of pokemon private Pokemon GetPokemon(int index) { if (Pokemon.Count > index) return Pokemon[index]; else return null; } private async Task LoadPage() { //Get the list of pokemon resources var pageResponse = await PokeClient.GetNamedResourcePageAsync<PokeApiNet.Pokemon>(ItemsPerPage, 0); //Create a list of tasks for calling getting the details of each pokemon from the list above var tasks = pageResponse.Results .Select(p => PokeClient.GetResourceAsync<PokeApiNet.Pokemon>(p.Name)); //Await all the tasks and set the data model for the page Pokemon = (await Task.WhenAll(tasks)).ToList(); } } }
In the above class, we inject the PokeClient class which we had registered in the dependency injection container. The class also has a member called Pokemon which is a list of Pokemon. This contains the data we are going to display on the page. The class also overrides the OnInitializedAsync method which executes when the page is initialized. Within this method we call a helper method called LoadPage which uses the PokeApiClient to first get the list of Pokemon to display and then gets the details of each of the Pokemon in the list. The second set of API calls to get the details of the Pokemon is necessary to get the image URL for the Pokemon.
Now we just need to write the Razor markup for the PokemonList component. The markup will be pretty simple since we already defined the PokemonCard component to help us out. Replace the contents of the PokemonList.razor file with the following Razor markup:
@page "/" @page "/pokemon" @if (Pokemon.Count == 0) { <div class="spinner-border text-danger" role="status"> </div> } else { @for (var i = 0; i < Pokemon.Count; i = i + 3) { <div class="card-deck"> <PokemonCard Pokemon=@GetPokemon(i) /> <PokemonCard Pokemon=@GetPokemon(i+1) /> <PokemonCard Pokemon=@GetPokemon(i+2) /> </div> } }
The above markup will render a loading spinner if the Pokemon list has not been populated yet. Otherwise it will render up to four rows of three Pokemon.
If you look at the app in your browser now, you should see a list of 12 Pokemon with their pictures displayed. However, we still have no way to see the next 12 Pokemon. There are 932 Pokemon out there, so how are we going to catch them all if we can only see 12? Let’s look at implementing paging next.
Implement Paging
To implement paging on the Pokemon list page, we need to add some more members to PokemonList class. Add the following code to the PokemonList class in PokemonList.cs:
// this value gets overwritten after the first API call int TotalPokemon = 932; //Determines whether loading spinner for next page is displayed bool LoadingNextPage = false; //Determines whether loading spinner for previous page is displayed bool LoadingPreviousPage = false; int CurrentPage = 1; //Evaluates whether we're on the last page of Pokemon bool OnLastPage() => (CurrentPage * ItemsPerPage) >= TotalPokemon; private async Task LoadNextPage() { LoadingNextPage = true; CurrentPage++; await LoadPage(); LoadingNextPage = false; } private async Task LoadPreviousPage() { LoadingPreviousPage = true; CurrentPage--; await LoadPage(); LoadingPreviousPage = false; }
Above, we’ve added class members to track the current page number, whether the next page is loading, whether the previous page is loading and the total number of Pokemon in all pages. We also added two helper methods LoadNextPage and LoadPreviousPage which will be triggered from the previous and next page buttons on the UI.
We need to make one more modification to the class in ProgramList.cs. We need to modify the LoadPage method slightly to incorporate the new class members we added. Replace the current code for the LoadPage method with the following:
private async Task LoadPage() { //Get the list of pokemon resources var pageResponse = (await PokeClient.GetNamedResourcePageAsync<PokeApiNet.Pokemon>( ItemsPerPage, (CurrentPage - 1) * ItemsPerPage)); TotalPokemon = pageResponse.Count; //Create a list of tasks for calling getting the details of each pokemon from the list above var tasks = pageResponse.Results .Select(p => PokeClient.GetResourceAsync<PokeApiNet.Pokemon>(p.Name)); //Await all the tasks and set the data model for the page Pokemon = (await Task.WhenAll(tasks)).ToList(); }
Now we just need to add Previous and Next buttons on the Pokemon list page so the user can navigate between pages. Add the following Razor markup just after the page directives (@page) in the PokemonList.cs file:
<div class="row"> <div class="offset-4 col-2"> @if (CurrentPage > 1) { <button class="btn btn-danger " @onclick="LoadPreviousPage" disabled="@LoadingPreviousPage"> @if (LoadingPreviousPage) { <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> } Previous </button> } </div> <div class="offset-1 col-2"> @if (!OnLastPage()) { <button class="btn btn-danger" type="button" @onclick="LoadNextPage" disabled="@LoadingNextPage"> @if (LoadingNextPage) { <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> } Next </button> } </div> </div>
In addition to adding the Previous and Next buttons, the above code hides the Previous button if we are on the first page and hides the Next button if we are on the last page. It will also cause the page to display a spinner while the previous or next page is loading.
If you look at your app running in the browser, you should now see a Next button to navigate to the next page. Once you navigate to the second page, a Previous button will also appear and will let you navigate back to the first page. While our Pokemon list page is complete, it doesn’t give us much detail about the Pokemon. It doesn’t tell us the attack or defense statistics of the Pokemon, nor does it tell us what moves the Pokemon can use. Next, we’re going to implement a completely separate page to give us more details about the Pokemon.
Implement the Pokemon Details Page
Let’s create a PokemonDetail component which will represent our Pokemon details page. In Visual Studio, right-click the Pages folder, hover over Add, click Razor Component, enter PokemonDetails as the name and click Add. This will add a file called PokemonDetails.razor to the Pages folder.
Let’s also add a code-behind file for the PokemonDetails component since we’ll need a decent amount of C# code to back this page. To do this, right-click the Pages folder in Visual Studio, hover over Add, click on Class, enter the name as PokemonDetails and then click Add. This will add a PokemonDetails.cs file under the Pages folder. Replace the contents of the PokemonDetails.cs file with the following code:
using Microsoft.AspNetCore.Components; using PokeApiNet; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Pokedex.Pages { public partial class PokemonDetails { //PokemonName is passed as a parameter to this component when it is created [Parameter] public string PokemonName { get; set; } //This injects the PokeClient from the dependency injection container [Inject] PokeApiClient PokeClient { get; set; } //This injects the NavigationManager class which allows us to navigate to a different page [Inject] NavigationManager NavigationManager { get; set; } //This serves as the main data model for the page private Pokemon Pokemon; //This is also used as data for the page and contains a set of descriptive paragraphs. private HashSet<string> PokemonDescriptions = new HashSet<string>(); protected override async Task OnInitializedAsync() { //Create a task for retrieving the basic Pokemon details var pokemonTask = PokeClient.GetResourceAsync<Pokemon>(PokemonName); //Retrieve additional details about the Pokemon from the API including descriptions var pokemonSpecies = await PokeClient.GetResourceAsync<PokemonSpecies>(PokemonName); Pokemon = await pokemonTask; //We filter out just the english descriptions //Also we replace a special character appearing in the descriptions with a space. pokemonSpecies.FlavorTextEntries.Where(f => f.Language.Name == "en") .ToList().ForEach(f => PokemonDescriptions.Add(f.FlavorText.Replace("\f", " "))); } //This method is used to navigate back to the Pokemon list page after clicking a button void GoBackToListPage() { NavigationManager.NavigateTo("pokemon"); } } }
The code above is not that different from the code-behind file for the PokemonList component. However there are couple of new elements. One is the NavigationManager class we injected into the PokemonDetail class above. The NavigationManager class is a helpful utility for navigating between pages in Blazor. In this case, we’re going to be using it to navigate from the Pokemon details page back to the Pokemon list page. In the OnInitializedAsync method we’re also calling a different method on the PokeApiClient to retrieve the description text of the Pokemon. Also, note that we are taking the PokemonName as a parameter for the page.
Now let’s enter the Razor markup for the PokemonDetails component. Replace the contents of the PokemonDetails.razor file with the following code:
<!--The page directive here specifies that it takes the PokemonName as a parameter--> @page "/pokemon/{PokemonName}" <!--The onclick attribute here specifies the Method to run in the code-behind file--> <button class="btn btn-danger" @onclick="GoBackToListPage">Back</button> <!--If the data hasn't been loaded yet, we just show a loading spinner--> @if (Pokemon == null) { <div class="spinner-border text-danger" role="status"> </div> } else { <h1>@Pokemon.Name</h1> <!-- display the front and back images of the Pokemon--> <div class="row"> <img src="@Pokemon.Sprites.FrontDefault"> <img src="@Pokemon.Sprites.BackDefault"> </div> <div class="row"> <div class="col-6"> <!--List the types of the Pokemon--> <strong>Types: </strong> @foreach (var type in Pokemon.Types) { <span class="badge rounded-pill bg-warning">@type.Type.Name</span> } <!--List the stats of the Pokemon--> <strong style="display:block">Stats:</strong> <table class="table"> @foreach (var stat in Pokemon.Stats) { <tr> <td>@stat.Stat.Name</td> <td>@stat.BaseStat</td> </tr> } </table> <!--List the moves of the Pokemon--> <strong style="display:block">Moves:</strong> <ul class="list-group"> @foreach (var move in Pokemon.Moves) { <li class="list-group-item">@move.Move.Name</li> } </ul> </div> <div class="col-6"> <!--Display the description of the Pokemon--> <strong>Description:</strong> @foreach (var description in PokemonDescriptions) { <p>@description</p> } </div> </div> }
Most of the code above is relatively straightforward and simply creates HTML based on the data contained in the Pokemon and PokemonDescriptions members of the PokemonDetails class. However, there are a few things worth calling out. One is that in the @page directive, we specify that we take the parameter PokemonName in the URL. Another is that we use the @onclick attribute on the Back button element to specify the C# method to run in the code-behind file when the button is clicked.
We’ve implemented the Pokemon details page, but we don’t yet have any way to navigate to it. We would like to navigate to the details page for a particular Pokemon by clicking on it on the Pokemon list page. However, we don’t need to make any changes to the Pokemon list page directly. Instead we can implement the navigation by making a few small changes on the PokemonCard component.
First, let’s inject the NavigationManager into the PokemonCard component by adding the following code at the top of the PokemonCard.razor file:
@inject NavigationManager NavigationManager
Next, let’s add a method to the @code section of the PokemonCard.razor page which will be used to navigate to the Pokemon details page:
void NavigateToDetails() { NavigationManager.NavigateTo($"pokemon/{Pokemon.Name}"); }
The last thing we need to do is to add the @onclick attribute to the div tag which represents the card. The div element looks like this:
<div class="card" style="cursor:pointer">
Add the @onclick attribute so that it now looks like this:
<div class="card" @onclick="NavigateToDetails" style="cursor:pointer">
We are now able to navigate to the Pokemon details page from the Pokemon list page by clicking the Pokemon we’re interested in. However, there is still one more problem to deal with. Navigate to the second page on the Pokemon list page, and then click on a Pokemon to go to the details page for it. If you then click the Back button, it takes you back to the first page of the Pokemon list instead of the second. Basically we have lost track of the last page we were on. We can fix this by storing our current page number on a singleton state service.
Right-click on the project in Visual Studio, hover over Add, click Class, enter StateService as the name and click Add. Replace the contents of the StateService.cs file with the following:
namespace Pokedex { public class StateService { public int CurrentPage { get; set; } = 1; } }
The StateService class above will be used to keep track of the page number we last visited on the Pokemon list page. We want to inject the same instance of this class into any component that needs to reference the current page number. In order to do this, we must register it with the dependency injection container. We can do this by adding the following code to the Main method in the Program.cs file:
builder.Services.AddSingleton(new StateService());
Now let’s inject the StateService into the PokemonList component by adding the following code to the PokemonList class in PokemonList.cs:
[Inject] StateService StateService { get; set; }
We can then get rid of the CurrentPage property in the PokemonList class and replace all references of it with StateService.CurrentPage in the PokemonList.cs and PokemonList.razor files. This will fix our problem and we will now be able to return to the correct page number of the Pokemon list page. We’re now finished with implementing the application. The next step is to deploy it to Azure.
Deploy to Azure
Our application is purely client-side. We don’t need a server to render our content, we just need it to serve up static files. Thus, we will use an Azure storage static website to host our Blazor app. This is a very cost-effective option because we just pay for storage and for the user downloading the static files to their browser.
Before continuing, make sure you have Azure Powershell installed on your computer and the AzCopy utility downloaded and added to your system path.
In order to host our app as an Azure static website, we need an Azure storage account. We’ll use Azure PowerShell to create one. First, open a PowerShell prompt and enter the following to authenticate with Azure:
Connect-AzAccount
This will open up a browser and prompt you for your Azure credentials. Once you’ve logged in from the browser, return to your PowerShell prompt and run the following script to create a storage account and enable static website feature.
$resourceGroup = "pokedex" $location = "eastus" $storageAccountName = 'pokedex' + ((Get-Random -Maximum 1000000).ToString()) # Create Resource Group New-AzResourceGroup -Name $resourceGroup -Location $location # Create Storage Account $storageAccount = New-AzStorageAccount -ResourceGroupName $resourceGroup ` -Name $storageAccountName ` -Location $location ` -SkuName Standard_LRS ` -Kind StorageV2 $ctx = $storageAccount.Context #Enable Static Website for Storage Account Enable-AzStorageStaticWebsite -Context $ctx -IndexDocument index.html
Don’t close the PowerShell prompt just yet. Set the working directory of the Powershell prompt to the folder containing your csproj file like this:
cd <PathToFolderWithCsprojFile>
Run the following script in your PowerShell prompt which will publish your app and upload it to Azure blob storage so it can be served as a static website.
dotnet publish -c Release -o \bin\Release\net5.0\wwwroot $sasToken = New-AzStorageContainerSASToken -Name '$web' -Permission rwdl -Context $ctx azcopy copy '.\bin\Release\net5.0\publish\wwwroot\*' ('https://'+$storageAccountName+'.blob.core.windows.net/$web'+$sasToken) --recursive $storageAccount.PrimaryEndpoints.Web
The last part of the script will output the URL of your deployed app. Copy it into your browser and you’re good to go!