Skip to content
Home » Blazor – My Portfolio – Part 5 – Data and Data Binding

Blazor – My Portfolio – Part 5 – Data and Data Binding

Spread the love

In the previous part of the series we styled our components. At this point, however, all the data in the app is hard-coded. This needs to be fixed. In this part we’ll be talking about data and data binding. Well, not just talking.

You can grab all the code and assets from Github.

Where to Get the Data from?

Most modern apps make use of data. The data may come from many different sources. Some apps need data from the Internet. If this is the case, you may want to build a Web API project and communicate with it from your client project. The data then usually comes from a database.

Our app is pretty simple, though. We’re not going to create a server project. All our data will be stored in a file – just like the sample data for the weather forecast (FetchData) component that we got out of the box when our project was created. You will find the file in the wwwroot folder, in the sample-data subfolder. Its name is weather.json. Go ahead and open it. The data is in JSON format, which is a popular format to carry data over the web. Each object is placed in a separate pair of curly braces. All the objects are placed inside square brackets, which means it’s an array of objects:

[
  {
    "date": "2022-01-06",
    "temperatureC": 1,
    "summary": "Freezing"
  },
  {
    "date": "2022-01-07",
    "temperatureC": 14,
    "summary": "Bracing"
  },
  {
    "date": "2022-01-08",
    "temperatureC": -13,
    "summary": "Freezing"
  },
  {
    "date": "2022-01-09",
    "temperatureC": -16,
    "summary": "Balmy"
  },
  {
    "date": "2022-01-10",
    "temperatureC": -2,
    "summary": "Chilly"
  }
]

This data is then consumed in the FetchData component. Our approach is going to be similar. We’re eventually going to delete the sample-data folder and the FetchData component because they’re not part of our project, but let’s leave them for now for reference.

Our data will be stored in a data.json file inside a data folder in wwwroot, so let’s create the folder and the file. In order to add a JSON file, in the Add New Item dialog under Data (A) select JSON File (B), name the file data.json (C) and hit the Add button (D).

At this point your wwwroot folder should look like so:

Now we can start adding the actual data. We’ll add only a little portion of it now and then use it in our app. If everything works fine, we’ll add all the data we need. The JSON file is going to contain all the projects, categories, and technologies that I want to show off in my portfolio.

JSON Data

In the data.json file paste the following JSON code:

{
  "categories": [
    {
      "id": 1,
      "name": "Web Projects",
      "icon": "bi bi-globe"
    },
    {
      "id": 2,
      "name": "Games",
      "icon": "bi bi-controller"
    }
  ],
  "techs": [
    {
      "id": 1,
      "name": "C#",
      "icon": "devicon-csharp-plain"
    },
    {
      "id": 2,
      "name": "Unity",
      "icon": "devicon-unity-original"
    },
    {
      "id": 3,
      "name": "Python",
      "icon": "devicon-python-plain"
    }
  ],  
  "projects": [
    {
      "id": 1,
      "name": "Forest Monsters",
      "description": "A 2D game made with Unity with C# scripting. Your task is to save the enchanted forest from a bad sorcerer. You have to kill lots of monsters on your way. This is a typical platformer game. You jump from platform to platform, collect items, shoot monsters, avoid bombs and poison, move toward the door to the next level. Some of the platforms can move, which makes the game more difficult.",
      "imageUrl": "images/Forest Monsters.png",
      "category": 2,
      "techs": [1, 2],
      "links": [              
        {
          "destination": "https://github.com/prospero-apps/forest-monsters",
          "displayText": "Github",
          "icon": "devicon-github-original"
        },
        {
          "destination": "https://youtu.be/i7xyMbhdg_8",
          "displayText": "YouTube",
          "icon": "bi bi-youtube"
        },
        {
          "destination": "https://prosperocoder.com/posts/unity/forest-monsters-a-2d-platformer-game-made-with-unity",
          "displayText": "Prospero Coder blog",
          "icon": "bi bi-book"
        },
        {
          "destination": "https://prosperocoder.itch.io/forest-monsters",
          "displayText": "Download",
          "icon": "bi bi-download"
        }
      ]
    },
    {
      "id": 2,
      "name": "Slugrace",
      "description": "A 2D game made with Python and Kivy. It can be played by up to four players. Each player places a bet on one of four racing slugs and they either win or lose money. The game is over when there's only one player left with any money, but you can set a different ending condition in the settings screen too, like after a given number of races or after a set period of time.",
      "imageUrl": "images/Slugrace.png",
      "category": 2,
      "techs": [ 3 ],
      "links": [
        {
          "destination": "https://github.com/prospero-apps/slugrace",
          "displayText": "Github",
          "icon": "devicon-github-original"
        },
        {
          "destination": "https://youtu.be/z24eIFsbEl8",
          "displayText": "YouTube",
          "icon": "bi bi-youtube"
        },
        {
          "destination": "https://prosperocoder.com/blog/posts/kivy",
          "displayText": "Prospero Coder blog",
          "icon": "bi bi-book"
        }
      ]
    }
  ]
}

JSON syntax is derived from JavaScript object notation. Each object is defined in curly braces and the data in it is represented by key/value pairs. The keys are always strings (which isn’t always the case in JavaScript) and they must be in double quotes. The keys and values are separated by colons. For arrays we use square brackets. Have a closer look at the above JSON code and you will immediately spot all these syntax elements.

Now, what do we have here? In the first part of the code there are two categories in an array. Next is an array of three technologies. Finally, there’s an array of two projects. The categories and technologies have an id key. These ids are used as the values of the category and techs keys in the projects. Each project belongs to one category, so the category key is set to a single integer value, which corresponds to the appropriate category’s id. Each project has an arbitrary number of technologies associated with it. These are represented by their ids in an array that is assigned to the techs key.

In the example above the first project’s category is Games, so category is set to 2. It uses two technologies, C# and Unity, hence the array with the ids 1 and 2 assigned to techs.

We’ll now create C# classes that will be instantiated and populated with the data from the JSON file.

Model Classes

Let’s create a Models folder in the root of our project with five classes in it. Actually, there are going to be six classes, but in five files. We’re going to name them ProjectModel, CategoryModel, TechModel, LinkModel and DataModel. Simpler names would be fine too, but let’s make them stand out.

Let’s start with CategoryModel. This class will represent a category and its instances will be populated with the data from the first part of the JSON file, so the code assigned to the key categories. Let’s have a look at it again:

{
  "categories": [
    {
      "id": 1,
      "name": "Web Projects",
      "icon": "bi bi-globe"
    },
    {
      "id": 2,
      "name": "Games",
      "icon": "bi bi-controller"
    }
  ],
  ...

So, each category has an id, which is an integer, name and icon, which are strings. Our classes will need these properties too. We’ll map the C# properties to the JSON keys using the JsonPropertyName attribute from the System.Text.Json.Serialization namespace. Here’s the code:

using System.Text.Json.Serialization;

namespace MyPortfolio.Models
{
    public class CategoryModel
    {
        [JsonPropertyName("id")]
        public int Id { get; set; }

        [JsonPropertyName("name")]
        public string Name { get; set; }

        [JsonPropertyName("icon")]
        public string Icon { get; set; }
    }
}

In a similar way, let’s implement the TechModel class:

using System.Text.Json.Serialization;

namespace MyPortfolio.Models
{
    public class TechModel
    {
        [JsonPropertyName("id")]
        public int Id { get; set; }

        [JsonPropertyName("name")]
        public string Name { get; set; }

        [JsonPropertyName("icon")]
        public string Icon { get; set; }
    }
}

As far as a project is concerned, here’s its JSON representation:

{
      "id": 1,
      "name": "Forest Monsters",
      "description": "A 2D game made with Unity with C# scripting. Your task is to save the enchanted forest from a bad sorcerer. You have to kill lots of monsters on your way. This is a typical platformer game. You jump from platform to platform, collect items, shoot monsters, avoid bombs and poison, move toward the door to the next level. Some of the platforms can move, which makes the game more difficult.",
      "imageUrl": "images/Forest Monsters.png",
      "category": 2,
      "techs": [1, 2],
      "links": [              
        {
          "destination": "https://github.com/prospero-apps/forest-monsters",
          "displayText": "Github",
          "icon": "devicon-github-original"
        },
        {
          "destination": "https://youtu.be/i7xyMbhdg_8",
          "displayText": "YouTube",
          "icon": "bi bi-youtube"
        },
        {
          "destination": "https://prosperocoder.com/posts/unity/forest-monsters-a-2d-platformer-game-made-with-unity",
          "displayText": "Prospero Coder blog",
          "icon": "bi bi-book"
        },
        {
          "destination": "https://prosperocoder.itch.io/forest-monsters",
          "displayText": "Download",
          "icon": "bi bi-download"
        }
      ]
    }

It has a couple keys, one of which is category. As you can see, its value is an integer. This is because we want to use the category’s id instead of a full CategoryModel object. In the same way we assign an array of integers to the techs key. These are the ids of the technologies. Finally, there is an array of links assigned to the links key. Each link object contains three key/value pairs. So, let’s implement our LinkModel class so that it matches the JSON data:

using System.Text.Json.Serialization;

namespace MyPortfolio.Models
{
    public class LinkModel
    {        
        [JsonPropertyName("destination")]
        public string Destination { get; set; }

        [JsonPropertyName("displayText")]
        public string DisplayText { get; set; }

        [JsonPropertyName("icon")]
        public string Icon { get; set; }
    }
}

We still haven’t implemented the ProjectModel class. But first let’s implement a helper class that will make it easier for us to retrieve the data directly from JSON. We’ll name the class ProjectInfo:

using System.Text.Json.Serialization;

namespace MyPortfolio.Models
{
    public class ProjectInfo
    {
        [JsonPropertyName("id")]
        public int Id { get; set; }

        [JsonPropertyName("name")]
        public string Name { get; set; }

        [JsonPropertyName("description")]
        public string Description { get; set; }

        [JsonPropertyName("imageUrl")]
        public string ImageUrl { get; set; }

        [JsonPropertyName("category")]
        public int Category { get; set; }

        [JsonPropertyName("techs")]
        public List<int> Techs { get; set; }

        [JsonPropertyName("links")]
        public List<LinkModel> Links { get; set; }
    }
}

As you can see, the Category property is an integer type, which is exactly how we defined it in JSON. Techs is a list of integers and Links is a list of LinkModels. We’ll use the category and techs ids to grab the full categories and technologies, so objects of the CategoryModel and TechModel classes. Let’s create the actual ProjectModel class, in the same ProjectModel.cs file, that will contain the full project information:

using System.Text.Json.Serialization;

namespace MyPortfolio.Models
{
    public class ProjectInfo
    {
        ...
    }

    public class ProjectModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public string ImageUrl { get; set; }
        public CategoryModel Category { get; set; }
        public List<TechModel> Techs { get; set; }
        public List<LinkModel> Links { get; set; }
    }
}

Now we have to use the ids to retrieve the full CategoryModel and TechModel data. In the Index.razor file we’ll make use of the Http.GetFromJsonAsync<T> method to retrieve the data from the JSON file. We’ll do it in a moment, but it’s going to look along these lines:

data = await Http.GetFromJsonAsync<DataModel>("data/data.json");

So, this will retrieve data from the entire JSON file. But how to we retrieve just the elements of the file, like the projects, categories and technologies? Well, the easiest way to go about it is by creating a data model that will use the root level keys from the JSON file, so categories, techs and projects. We already have the DataModel class, so let’s implement it like so:

using System.Text.Json.Serialization;

namespace MyPortfolio.Models
{
    public class DataModel
    {
        [JsonPropertyName("projects")]
        public List<ProjectInfo> Projects { get; set; }

        [JsonPropertyName("categories")]
        public List<CategoryModel> Categories { get; set; }

        [JsonPropertyName("techs")]
        public List<TechModel> Techs { get; set; }
    }
}

Now all we have to do is get access to the data in the component where it should be displayed, which is the Index component. It should be available as soon as the component in initialized. This is where component lifecycle methods come in handy.

Component Lifecycle Methods

Each Blazor component has its own individual lifecycle and each stage of its existence is represented by a method. There are five stages and each stage has one or two corresponding methods that can be overridden. In cases where there are both a synchronous and an asynchronous method, the synchronous method executes first. Here are all the stages and the lifecycle methods associated with them in order of execution:

Order of ExecutionLifecycle StageLifecycle Method
1SetParametersSetParametersAsync
2InitializedOnInitialized
3 OnInitializedAsync
4ParametersSetOnParametersSet
5 OnParametersSetAsync
6AfterRenderOnAfterRender
7 OnAfterRenderAsync
8DisposeDispose
9 DisposeAsync

And here’s what happens at each stage:

SetParameters – This is when the component is initially constructed and before the parameters are set and also when the parameters are updated from the URL or parent component (unless, in the latter case, the one of the ParametersSet stage methods is used).

Initialized – This is when the component is fully constructed and the parameters are set.

ParametersSet – This is when the component’s parameters are updated from the URL or parent component.

AfterRender – This is when the initialization is over and the StateHasChanged method is called.

Dispose – This is when the component is removed (to use this method properly, the component should implement the IDisposable/IAsyncDisposable interface.

We want the Index component to retrieve the JSON data as soon as it is initialized. Let’s override the OnInitializedAsyc lifecycle method then. In the method we’ll access the data we need:

@page "/"
@inject HttpClient Http
@using MyPortfolio.Models;

<PageTitle>Index</PageTitle>

...
@code {
    private List<ProjectModel>? projects;
    private List<CategoryModel>? categories;
    private List<TechModel>? techs;
    private DataModel? data;

    protected override async Task OnInitializedAsync()
    {
        data = await Http.GetFromJsonAsync<DataModel>("data/data.json");

        categories = data.Categories;

        techs = data.Techs;

        projects = (from project in data.Projects
                    join category in categories
                    on project.Category equals category.Id
                    select new ProjectModel
                        {
                            Id = project.Id,
                            Name = project.Name,
                            Description = project.Description,
                            ImageUrl = project.ImageUrl,
                            Category = category,
                            Techs = (from tech in techs
                            where project.Techs.Contains(tech.Id)
                            select tech).ToList(),
                            Links = project.Links
                        }).ToList();
    }
}

So, first we create private fields that will hold all the particular pieces of data, so a list of projects, a list of categories and a list of technologies. We’ll also add a data field that will hold all the data retrieved from JSON. As mentioned before, inside the OnInitializedAsync method we retrieve the data and store it in the data variable. The data variable is of the DataModel type, so it has three properties. We can access the categories and techs directly, but as for the projects, we want them to contain all the category and technologies – related stuff instead of just the indices. To do that, we use LINQ to create a list of ProjectModel  instances.

This way we have all the data we need. The next question is how to consume it in our components.

Parameters 

Let’s have a look at the Index component again:

@page "/"
...
<div class="row mt-3">
    <ProjectsDisplay />
</div>

@code {
    ...
        categories = data.Categories;

        techs = data.Techs;

        projects = ...
    }
}

In the code section we get access to the data. In the Razor section we have the ProjectsDisplay component. Its task is to display all the individual components as cards. So, the ProjectsDisplay component needs the projects-related data. How do we pass the data to it then? Well, we can use parameters.

A parameter is a mechanism for transmitting data between a parent component and its direct child. Here the parent is Index and the child is ProjectsDisplay.

We will create a parameter for transmitting project data. Let’s open the child component, ProjectsDisplay and declare a public property that will receive the parameter from the parent. It must be of the same type as the data that is transmitted and it must be decorated with the Parameter attribute:

@using MyPortfolio.Models;

<div class="col-md-6 col-lg-4 mb-2">
    <Project />
</div>
<div class="col-md-6 col-lg-4 mb-2">
    <Project />
</div>
<div class="col-md-6 col-lg-4 mb-2">
    <Project />
</div>
<div class="col-md-6 col-lg-4 mb-2">
    <Project />
</div>
<div class="col-md-6 col-lg-4 mb-2">
    <Project />
</div>

@code {
    [Parameter]
    public List<ProjectModel> Projects { get; set; }
}

Make sure the appropriate using directive is added at the top of the file so that we can use the ProjectModel class. Here we name the property simply Projects.

Now this property can be set to data from the parent component using the Projects parameter. So, the parameter name must match the property’s name. Here’s the parent component (Index):

@page "/"
...
<div class="row mt-3">
    <ProjectsDisplay Projects="@projects" />
</div>

@code {
    ...
}

Now the data is available in the child. We’ll use data binding to display it.

One-way Data Binding 

In Blazor there is one-way and two-way data binding. One-way binding is all we need here. You just get the data saved in a property in the code section by preceding the property’s name in the Razor section with the @ symbol. In two-way binding you could also set a property from an input.

So, let’s do it. We need to add a parameter to the Project component so that we can pass data from parent (ProjectsDisplay) to child (Project). Here’s the Project component:

@using MyPortfolio.Models;
<a href="">
    ...
</a>

@code {
    [Parameter]
    public ProjectModel ProjectData { get; set; }
}

Again, don’t forget to add the using directive. We know there are two projects, so we can access them individually. Here’s the ProjectsDisplay component:

@using MyPortfolio.Models;

<div class="col-md-6 col-lg-4 mb-2">
    <Project ProjectData="@Projects[0]"/>
</div>
<div class="col-md-6 col-lg-4 mb-2">
    <Project ProjectData="@Projects[1]" />
</div>

@code {
    [Parameter]
    public List<ProjectModel> Projects { get; set; }
}

The number of projects may change over time, so a better solution is to loop through all the projects. You can use the foreach loop in Razor. The general rule is that any time you switch from HTML code to C# code, you use the @ sign. Have a look:

@using MyPortfolio.Models;

@foreach(var project in Projects)
{
    <div class="col-md-6 col-lg-4 mb-2">
        <Project ProjectData="@project" />
    </div>
}

@code {
    [Parameter]
    public List<ProjectModel> Projects { get; set; }
}

Now we have to bind the data inside the Project component to the values passed from the parent. Here’s the modified code:

@using MyPortfolio.Models;
<a href="">
    <div class="card">
        <img class="img-thumbnail" src="@ProjectData.ImageUrl" />
        <div class="card-body">
            <h5 class="card-title mb-3">@ProjectData.Name</h5>
            <div class="tech-icons">
                @foreach (var tech in ProjectData.Techs)
                {
                    <div class="single-icon">
                        <i class="@tech.Icon" />
                    </div>
                }
            </div>
            <p>@ProjectData.Description</p>
            <div class="link-icons">
                @foreach (var link in ProjectData.Links)
                {
                    <div class="single-icon">
                        <i class="@link.Icon" />
                    </div>
                }
            </div>
            <p class="more-info">Click to view more...</p>
        </div>
    </div>
</a>

@code {
    [Parameter]
    public ProjectModel ProjectData { get; set; }
}

Before we run the code, we must copy the image files to the wwwroot/images folder. The image for the Forest Monsters project is already there. You can grab the file (Slugrace.png) for the Slugrace project from Github.

We’re almost there. One more thing to take care of is ensure no error occurs if there is no data. There may be a short period of time before the data is loaded when this may be the case. We can follow the pattern in the FetchData component where conditional rendering is implemented. Have a look at the Razor section of that component:

@page "/fetchdata"
@inject HttpClient Http

<PageTitle>Weather forecast</PageTitle>

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    ...
}

The part of the UI that requires data is enclosed in conditional code. If there is no data available yet, we are just informed that the data is loading. If the data is available, the table is populated by it.

And here’s our implementation in the Index component:

@page "/"
@inject HttpClient Http
@using MyPortfolio.Models;

<PageTitle>Index</PageTitle>

<h1>My Projects</h1>

@if(projects == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <div class="row mt-3">
        <ProjectsDisplay Projects="@projects" />
    </div>
}

@code {
    ...
}

Now it should work. Run the app and you should see the two projects populated with their appropriate data:

Before we continue with populating the other components, let’s take a short break to refactor our code a bit. In particular, we’ll move the code sections to separate files.

The ComponentBase Class 

The code section in all our components is placed in the same file as the Razor section. This is fine, especially if the code section isn’t very complex. But if it gets more complex, you might want to move it to a separate file. There are a couple ways of doing it, but the standard way of doing it is by moving the C# code to a separate class with the same name as the component plus the suffix -Base and then inheriting from it in the Razor file. The new class is placed in the same folder as the Razor file and it itself inherits from ComponentBase. Let’s see it in action.

The code in most our components is pretty simple, so we’ll leave it where it is. The code in the Index component, though, has gotten a little more complex. Let’s move it to a separate class.

First of all, we’ll create the IndexBase class in the Pages folder:

It must inherit from ComponentBase. Let’s move the C# code there. The file should now look like so:

using Microsoft.AspNetCore.Components;
using MyPortfolio.Models;
using System.Net.Http.Json;

namespace MyPortfolio.Pages
{
    public class IndexBase : ComponentBase
    {
        [Inject]
        HttpClient Http { get; set; }

        public List<ProjectModel>? Projects { get; set; }
        public List<CategoryModel>? Categories { get; set; }
        public List<TechModel>? Techs { get; set; }
        public DataModel? Data { get; set; }

        protected override async Task OnInitializedAsync()
        {
            Data = await Http.GetFromJsonAsync<DataModel>("data/data.json");

            Categories = Data.Categories;

            Techs = Data.Techs;

            Projects = (from project in Data.Projects
                        join category in Categories
                        on project.Category equals category.Id
                        select new ProjectModel
                        {
                            Id = project.Id,
                            Name = project.Name,
                            Description = project.Description,
                            ImageUrl = project.ImageUrl,
                            Category = category,
                            Techs = (from tech in Techs
                                     where project.Techs.Contains(tech.Id)
                                     select tech).ToList(),
                            Links = project.Links
                        }).ToList();
        }
    }
}

As you can see, there are a couple differences. Going from top to bottom, first come the using directives that you must put there. In the original file we used dependency injection to inject the HtmlClient object:

@page "/"
@inject HttpClient Http
@using MyPortfolio.Models;

In the C# code we must use the Inject attribute to achieve the same:

[Inject]
HttpClient Http { get; set; }

In the original file we used private fields:

private List<ProjectModel>? projects;
private List<CategoryModel>? categories;
private List<TechModel>? techs;
private DataModel? data;

We can’t use private fields here because we need access to them in the derived class. So, let’s turn them into public properties:

public List<ProjectModel>? Projects { get; set; }
public List<CategoryModel>? Categories { get; set; }
public List<TechModel>? Techs { get; set; }
public DataModel? Data { get; set; }

As you can see in the code above, the private fields were replaced by the public properties in the rest of the code.

Now, the original Razor file needs some modifications, too. First of all, it must inherit from IndexBase. We use the @inherits directive in Razor code to handle inheritance. Secondly, we must pass the public property Projects to the Projects parameter instead of the private field. Finally, we don’t need the code section anymore, so let’s remove it altogether. The complete Index.razor file should now look like this:

@page "/"
@using MyPortfolio.Models;
@inherits IndexBase;

<PageTitle>Index</PageTitle>

<h1>My Projects</h1>

@if(Projects == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <div class="row mt-3">
        <ProjectsDisplay Projects="@Projects" />
    </div>
}

If you now run the code, it will work the same.

So, we have our projects displayed in the Index component. We’ll add more projects soon, but now let’s take care of the sidebar. The categories and technologies there are still hardcoded and we could use the real data from the JSON file instead. Why not do just that?

Categories and Technologies Data Binding

Let’s display only the categories and technologies that we receive from the JSON file. The code sections in these two components are pretty simple, we’re not going to create separate files for them like we did with the Index component. The approach is very similar: We load the data and while it’s still loading, we display the loading message. If the data is there, we use data binding to display the data correctly. Here’s the CategoriesMenu component:

@using MyPortfolio.Models;
@inject HttpClient Http

<div class="sidebar-menu">
    @if (Categories == null)
    {
        <p><em>Loading...</em></p>
    }
    else
    {
        <h3 class="sidebar-header">Categories</h3>
        <div class="sidebar-scroll">
            @foreach (var category in Categories)
            {
                <div class="nav-item px-3">
                    <NavLink class="nav-link" href="">
                        <div class="sidebar-item">
                            <i class="@category.Icon" />
                            <p>@category.Name</p>
                        </div>
                    </NavLink>
                </div>
            }
        </div>        
    }
</div>  

@code {
    public List<CategoryModel>? Categories { get; set; }
    public DataModel? Data { get; set; }

    protected override async Task OnInitializedAsync()
    {
        Data = await Http.GetFromJsonAsync<DataModel>("data/data.json");

        Categories = Data.Categories;        
    }
}

The TechsMenu component looks very much the same:

@using MyPortfolio.Models;
@inject HttpClient Http

<div class="sidebar-menu">
    @if (Techs == null)
    {
        <p><em>Loading...</em></p>
    }
    else
    {
        <h3 class="sidebar-header">Technologies</h3>
        <div class="sidebar-scroll">
            @foreach (var tech in Techs)
            {
                <div class="nav-item px-3">
                    <NavLink class="nav-link" href="">
                        <div class="sidebar-item">
                            <i class="@tech.Icon" />
                            <p>@tech.Name</p>
                        </div>
                    </NavLink>
                </div>
            }
        </div>
    }
</div>

@code {
    public List<TechModel>? Techs { get; set; }
    public DataModel? Data { get; set; }

    protected override async Task OnInitializedAsync()
    {
        Data = await Http.GetFromJsonAsync<DataModel>("data/data.json");

        Techs = Data.Techs;
    }
}

If we now run the app, you will see that we only have the few categories and technologies specified in the JSON file:

While we’re still at the sidebar, let’s add a space in the name of our app near the top:

<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">My Portfolio</a>
        <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
            ...

Now it looks better:

I’ve only shown two of my projects so far, but I have more. Let’s add them to the JSON file, along with the categories they’re in and the technologies they use.

More Data in the JSON File 

The full code in the JSON file is long, so I’m not going to show it all here, just grab it from Github. Also remember to copy the project images to the wwwroot/images folder. With all the code and images in place, run the app. You will now see all the projects in the homepage and all the categories and technologies in the sidebar:

As you can see, the project descriptions are of varying length from project to project. Let’s say the full description will be only displayed in the project details page and here, in the project summary, we only want the initial part of it followed by an ellipsis. To this end, we’ll create a method in the Project component.

Shortened Project Description

Let’s create a method in the Project component’s code section that will shorten the description to a given length if it’s longer than that length. Then we can use the method in the Razor section:

@using MyPortfolio.Models;
<a href="">
    ...
            <div class="tech-icons">
                ...
            </div>
            <p>@ShortenDescription(ProjectData.Description, 120)</p>
            <div class="link-icons">
                ...
            </div>
            ...
</a>

@code {
    [Parameter]
    public ProjectModel ProjectData { get; set; }

    protected string ShortenDescription(string description, int length)
    {
        string shortDescription = description.Length < length ? description : description[0..length] + "...";
        return shortDescription;
    }
}

If we shorten the description to 120 characters, it should occupy two lines on a desktop display or, in general, the same number of lines regardless of the width of your device’s display:

This all looks good, but there is one more thing I want to do. The projects belong to different categories. They are displayed all together, though, under the My Projects label. Let’s group them by category.

Grouping Projects by Category

We want to group the projects by category. Then we want to display for each category its name followed by the projects (or rather project summaries) that belong there. How do we do that?

Well, the projects are displayed in the Index component, so let’s implement this functionality there. We’ll create a method that uses LINQ to group the items. Add the following method to the IndexBase class:

...
    public class IndexBase : ComponentBase
    {
        ...
        protected override async Task OnInitializedAsync()
        {
            ...
        }

        protected IOrderedEnumerable<IGrouping<int, ProjectModel>> GetGroupedProjects()
        {
            return from project in Projects
                   group project by project.Category.Id into projectGroup
                   orderby projectGroup.Key
                   select projectGroup;
        }
    }
}

This method makes use of the categories’ ids to group the projects. We also want to display the names of the categories, so let’s create a method for this too:

...
    public class IndexBase : ComponentBase
    {
        ...
        protected IOrderedEnumerable<IGrouping<int, ProjectModel>> GetGroupedProjects()
        {
            ...
        }

        protected string GetCategoryName(IGrouping<int, ProjectModel> groupedProjects)
        {
            return groupedProjects.FirstOrDefault(p => p.Category.Id == groupedProjects.Key).Category.Name;
        }
    }
}

Now we can use the methods in the Index.razor file:

@page "/"
...
@if(Projects == null)
{
    <p><em>Loading...</em></p>
}
else
{
    var projectCount = Projects.Count();

    @if(projectCount > 0)
    {              
        @foreach(var group in GetGroupedProjects())
        {
            <h4>@GetCategoryName(group) (@group.Count())</h4>

            <div class="row mt-3">
                <ProjectsDisplay Projects="@group.ToList()"></ProjectsDisplay>
            </div>
            <hr class="mb-3"/>
        }                   
    }    
}

The GetGroupedProjects method returns an IGrouping<int, ProjectModel>, so we need to convert it to a list in order for the projects to display. The Projects parameter expects a list of projects.

We also add information about the number of projects in each category and separate each group with a horizontal line.

If you run the app again, you will see all the projects grouped into categories:

As you can see, there are currently two projects in the Games category.

We now have data all over the place in our app, except the ProjectDetails component. We’ll fix this in the next part of the series where we’ll be covering navigation.


Spread the love

Leave a Reply