Blazor Walkthrough: Act II - rochellew/csci_1260_spring2025 GitHub Wiki

Digital Tome of Magical Flora & Fauna - Chapter II

🧙 "Oh hello, intrepid developer! I see you have met my brother the BLUE WIZARD and already started on this project! It's wonderful to have you here.

In this section of the tutorial, you will learn to create a JSON file and populate a new view with the data. Are you ready? If so, let's get started!

Good luck!

NPC art by William H. Rochelle

Task 1: Creating a JSON File for Creatures

In TomeBlazorProject, you should have a folder called wwwroot. This is intended to contain your .css files, images, and other resources. We are going to add a .json file here that contains some creatures.

Assuming you have followed the instructions in the previous guide, this should be a relatively simple process.

  1. Create a new folder in /wwwroot called data.
  2. Create a new file in /wwwroot/data called creatures.json.
  3. In this file, you will add at least five creatures; though, feel free to add more if you'd like.
    • Use the empty JSON below as a template for creating a list of JSON objects.
[
  {
    "Name": "",
    "ImageUrl": "",
    "Description": "",
    "Rarity": "",
    "Habitat": "",
    "IsHostile": true
  },
  {
    "Name": "",
    "ImageUrl": "",
    "Description": "",
    "Rarity": "",
    "Habitat": "",
    "IsHostile": true
  },
  {
    "Name": "",
    "ImageUrl": "",
    "Description": "",
    "Rarity": "",
    "Habitat": "",
    "IsHostile": true
  },
  {
    "Name": "",
    "ImageUrl": "",
    "Description": "",
    "Rarity": "",
    "Habitat": "",
    "IsHostile": true
  },
  {
    "Name": "",
    "ImageUrl": "",
    "Description": "",
    "Rarity": "",
    "Habitat": "",
    "IsHostile": true
  }
]

🧙 Wizard Wisdom: You will need to change the value of IsHostile to true or false depending on what kind of creature you create. Also, recall that in class on Monday, we said that enum values needed to be stored in JSON as their numerical counterparts -- this is not necessarily true, as we'll see later in the tutorial. Use the name of the Rarity enum (e.g., "Common", "Mythical", etc.) rather than a number.

Task 2: Updating OrganismService

Now that we have some data (a list of at least 5 JSON creatures) in creatures.json, we want to modify OrganismService to be able to return a .NET List<Creature> to a new Razor page.

Navigate to OrganismService.cs.

  1. We need to do some setup to make this work properly. Add the following attributes to the OrganismService class.
    • A private readonly HttpClient object named _httpClient.
      • private readonly HttpClient _httpClient;
    • A private readonly JsonSerializerOptions object named _options.
    • A private readonly NavigationManager object named _nav.

🧙 Wizard Wisdom: The code for creating a private field is shown for the _httpClient declaration. Use this as a guide for the other two. The reason we're adding these in is twofold: one, we want the serializer to be able to read the values of the Rarity enum rather than have to use the numbers (hence the JsonSerializerOptions) and two, we want to read file data from /wwwroot (hence the HttpClient and NavigationManager).

  1. Create a constructor with the following criteria.
    • Has the public modifier
    • Is named the same as the class (should be OrganismService)
    • Has two parameters:
      • An HttpClient object named httpClient (notice the lack of underscore differentiates this one from the private attribute you just made)
      • A NavigationManager object named nav (notice the lack of underscore differentiates this one from the private attribute you just made)

The body of the constructor is where we will determine what happens every time a new OrganismService object is created.

  1. In the constructor body, set the value of the private fields to the parameter variables.

    • _httpClient = httpClient;
    • _nav = nav;
  2. Copy the following code into the constructor body to set up the JsonSerializerOptions object.

_options = new JsonSerializerOptions
{
    PropertyNameCaseInsensitive = true
};
_options.Converters.Add(new JsonStringEnumConverter());

Now that we have the constructor set up, along with the capability of reading our JSON file, let's add a method to read the data in from creatures.json and return a List<Creature> to a new Razor page (we'll make that later).

  1. Create a new method called GetCreaturesFromJson that meets the following criteria.

    • Has the public modifier
    • Is async
    • Returns a Task<List<Creature>>
    • Has no parameters
  2. Copy the following code into GetCreaturesFromJson in order to provide the functionality we want.

var url = new Uri(new Uri(_nav.BaseUri), "data/creatures.json");
var stream = await _httpClient.GetStreamAsync(url);
List<Creature> creatures = JsonSerializer.Deserialize<List<Creature>>(stream, _options)!;
return creatures ?? new List<Creature>();

🧙 Wizard Wisdom: While I provided much of the code for this change to OrganismService, I would encourage you to take a look at it and try to figure out what's going on. Additionally, you should still have the previous two methods we made last time: GetSampleCreature and GetSamplePlant.

Task 3: Modifying Program.cs

Remember that we registered OrganismService in our Program.cs file so it can be used in our application. We want to change that registration as well as register one more service to make sure everything will work properly when we create our Razor page. Open Program.cs in TomeBlazorProject.

The first thing we want to do is register a new scoped service so the HttpClient will work properly when reading our JSON data. Assuming you named the variables properly in the previous steps, this should be a simple copy/paste job.

  1. Copy the following code and paste it above the line where OrganismService is registered.
builder.Services.AddScoped(sp =>
{
    var navManager = sp.GetRequiredService<NavigationManager>();
    return new HttpClient { BaseAddress = new Uri(navManager.BaseUri) };
});
  1. Change the lifetime of OrganismService from Singleton to Scoped
    • You can do this by changing the method from AddScoped to AddSingleton and nothing else needs to change.

Task 4: Adding a new Razor page for Creatures and CSS Styling

Now that we have modified the service, and ensured that everything will work properly behind the scenes, let's add a Razor page to see if all that work paid off! Find the Components folder in the TomeBlazorProject root directory to get started.

  1. Add a new Razor component in /Components/Pages called Creatures.razor.
  2. Replace the default contents of Creatures.razor with the following code.
@page "/creatures"
@using TomeLibraryProject
@inject TomeBlazorProject.Services.OrganismService OrganismService

<h1>Magical Creatures</h1>

@if (creatures == null)
{
    <p>Loading creatures...</p>
}
else if (!creatures.Any())
{
    <p>No creatures found.</p>
}
else
{
    @foreach (var creature in creatures)
    {
        <div class="creature-card" @onclick="@(() => ToggleExpanded(creature))">
            <div class="card-header">
                <img src="@creature.ImageUrl" alt="@creature.Name" />
                <div>
                    <h3>@creature.Name</h3>
                    <p class="rarity">@creature.Rarity</p>
                </div>
            </div>

            @if (expandedCreature == creature)
            {
                <div class="card-details">
                    <p><strong>Description:</strong> @creature.Description</p>
                    <p><strong>Habitat:</strong> @creature.Habitat</p>
                    <p><strong>Hostile?</strong> @(creature.IsHostile ? "Yes" : "No")</p>
                </div>
            }
        </div>
    }
}

@code {
    private List<Creature>? creatures;
    private Creature? expandedCreature;

    protected override async Task OnInitializedAsync()
    {
        creatures = await OrganismService.GetCreaturesFromJson();
    }

    void ToggleExpanded(Creature creature)
    {
        expandedCreature = expandedCreature == creature ? null : creature;
    }
}
  1. Add the following (do not replace anything) to app.cs to style this new page.
.creature-card {
    background: #fff;
    border-radius: 12px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
    margin: 1rem auto;
    padding: 1rem;
    max-width: 600px;
    transition: box-shadow 0.2s ease;
    cursor: pointer;
}

.creature-card:hover {
    box-shadow: 0 6px 18px rgba(0,0,0,0.15);
}

.card-header {
    display: flex;
    align-items: center;
    gap: 1rem;
}

.card-header img {
    width: 100px;
    height: auto;
    border-radius: 8px;
}

.rarity {
    font-weight: bold;
    color: #6c5ce7;
    margin-top: 0.25rem;
}

.card-details {
    margin-top: 1rem;
    border-top: 1px solid #eee;
    padding-top: 1rem;
}

At this point, you should be good to run the application. You will likely still see the home page from the last tutorial. Add /creatures to the end of the URL in your web browser to see the Creatures page you just made. Click around on the different creature cards to make sure everything works properly.

Task 5: Finishing Touches

Here's where you can flex your web design skills! Add the following to the HTML and CSS of the application to finish this tutorial up.

  1. Create a navbar component that all of the pages share that includes a link to the Creatures page.
    • I would recommend adding NavbarComponent somewhere in the Components folder, then including it in MainLayout.razor before the @body line.
  2. Create some CSS in app.css to style the navbar properly.
  3. Modify the Razor and/or CSS to differentiate between different levels of Rarity visually (e.g., the Common label is blue, while the Mythical label is purple).

Below is a screenshot for what your webpage should look like, as well as an example navbar.

Web page by William H. Rochelle

Conclusion

You did it! Congratulations on finishing up the second tutorial. The next steps will be the inclusion of lambda and LINQ as well as some debugging tips. See you next time.

❤️

⚠️ **GitHub.com Fallback** ⚠️