In the previous article, we saw how to use the Blazor material table to display all the data fetched from the API, and how to apply pagination, searching, and sorting actions. But in real-world applications, we want to see more information about a single product, once we click on it. Well, in this article, we are going to use additional MudBlazor material components to create such a page. We are going to use Grid, Charts, Expansion Panel, Rating, and other components to accomplish our task. As in the previous article, we have already prepared our Web API project so we can focus only on the Blazor WebAssembly project and MudBlazor material components implementation.

To download the source code for both client and server projects, you can visit the Display Product Details repository.

If you want to learn more about Blazor WebAssembly, we strongly suggest visiting our Blazor WebAssembly series of articles, where you can read about Blazor WebAssembly development, authentication, authorization, JSInterop, and other topics as well.

We are going to divide this article into the following sections:

Let’s start.

Fetching Data From the API

Before we start with the client application’s work, we have to run our API project. This will seed the additional data to our database, or create a new one with all the data if you didn’t follow along with our previous article.

Now, we can move on to the client application. Here, we want to start with the IHttpClientRepository modification:

public interface IHttpClientRepository
{
    Task<PagingResponse<Product>> GetProducts(ProductParameters productParameters);
    Task<Product> GetProduct(Guid id);
}

When we click on a single product, we want to call this GetProduct method and pass the product’s id as an argument. Of course, we have to implement it in the HttpClientRepository class:

public async Task<Product> GetProduct(Guid id)
{
    var uri = $"products/{id}";

    using (var response = await _client.GetAsync(uri))
    {
        response.EnsureSuccessStatusCode();

        var stream = await response.Content.ReadAsStreamAsync();

        var product = await JsonSerializer.DeserializeAsync<Product>(stream, _options);

        return product;
    }
}

Here we use the HttpClient to fetch the data from the API’s action and read the content as a stream. Then we deserialize our stream and return the product.

With this out of the way, we can start creating our ProductDetails page. We are going to create two files ProductDetails.razor and ProductDetails.razor.cs in the Pages folder:

Adding ProductDetails component

Now, let’s just add the route to the template file by providing a single productId parameter:

@page "/product/{productId:guid}"

If you are not familiar with this logic or routing overall, we strongly suggest reading our Blazor WebAssembly Routing article.

Additionally, we have to modify the class file:

public partial class ProductDetails
{
    public Product Product { get; set; } = new Product();

    [Inject]
    public IHttpClientRepository Repository { get; set; }

    [Parameter]
    public Guid ProductId { get; set; }

    protected async override Task OnInitializedAsync()
    {
        Product = await Repository.GetProduct(ProductId);
    }
}

Here, we inject our repository service and provide a single parameter to accept the ProductId value from the route. Then, we call the GetProduct method from the repository to fetch that product from the API.

Providing Navigation Link

To be able to navigate to our ProductDetails page, we have to modify the FetchData.razor file:

<RowTemplate>
    <MudTd DataLabel="Image">
        <MudLink Href="@($"product/{context.Id}")">
            <MudAvatar Color="Color.Transparent" Size="Size.Large" Image="@context.ImageUrl" />
        </MudLink>
    </MudTd>
    <MudTd DataLabel="Name">
        <MudLink Href="@($"product/{context.Id}")">@context.Name</MudLink>
    </MudTd>
    <MudTd DataLabel="Supplier">@context.Supplier</MudTd>
    <MudTd DataLabel="Price">[email protected]</MudTd>
</RowTemplate>

We use the MudLink component to enable navigation to our ProductDetials page. We wrap the image and the name of the product with the link and provide the route to the page. The Href property accepts only a string type value, so we have to use the string interpolation to provide that value.

That’s all. We can start the app, and navigate to the FetchData page:

Using MudBlazor Material Component - MudLink to create page navigation

As we can see, both the image and the name are links now. Also, we can confirm that the href attribute contains a valid route.

Now, we can start using some MudBlazor material components to display our product details on the page.

Using MudBlazor Material Components to Display Product’s Details

Let’s start with displaying basic info about our product by modifying the ProductDetails.razor file:

@page "/product/{productId:guid}"

<MudGrid>
    <MudItem md="12" sm="12">
        <MudText Typo="Typo.h3">@Product.Name</MudText>
    </MudItem>
    <MudItem md="12" sm="12">Rating Placeholder</MudItem>
    <MudItem lg="4" md="6" sm="12">
        <img width="300" src="@Product.ImageUrl" alt="@Product.Name" />
    </MudItem>
    <MudItem lg="4" md="6" sm="12">
        <MudPaper Class="pa-4">
            <MudText Typo="Typo.h4" Class="mb-4">Price: [email protected]("{0:0.00}", Product.Price)</MudText>
            <MudText Typo="Typo.body1">
                <strong>Manufacture date:</strong> @Product.ManufactureDate.ToShortDateString()
            </MudText>
            <MudText Typo="Typo.body1" Class="mb-4"><strong>Supplier: </strong>@Product.Supplier</MudText>

            <MudIcon Icon="@Icons.Filled.LocalShipping" Size="Size.Large" />
            <MudElement HtmlTag="span" Style="position:relative; top:-13px; margin-left:5px;">
                Free shipping for this product.
            </MudElement>
            <MudFab Icon="@Icons.Filled.ShoppingCart" Color="Color.Tertiary" 
                    Label="Add to cart" Class="mt-4"/>
        </MudPaper>
    </MudItem>
</MudGrid>

Here, we use the MudGrid component to prepare a layout for our items. We create each item with the MudItem component. You can also see that we use different breakpoints like lg, md, and sm. These breakpoints enable our items to spread equally on our page depending on the page size. You can read more about these breakpoints on this link.

So, we first create a title with our product’s name, and below a simple placeholder for our rating component, which we are going to implement pretty soon. Then, we create an image, which takes 1/4 of the large screen size, 1/2 of the medium screen size, and the entire small screen size. We use the same breakpoints for the next item, which is a more complex one. In this one, we use a MudPaper component to create a simple wrapper with some shadow and borders around our content. Then, we show different details of our product, including the free shipping information, and the cart button.

We can see two new components here: MudElement and MudFab. The first one enables us to render any HTML element we want to use on our page. In this case, we use the span element. The second one renders the button with an additional label.

Now, if we start our app, and click on the first product (because we’ve seeded the data only for that one), we can see our page:

Using MudBlazor Material Components to create a basic details page

Excellent.

Let’s now add the rating part.

Adding Rating MudBlazor Component

To add rating functionality to our product, we are going to use the MudRating component in the ProductDetails.razor file. Let’s replace the rating placeholder text with a real component:

<MudGrid>
    <MudItem md="12" sm="12">
        <MudText Typo="Typo.h3">@Product.Name</MudText>
    </MudItem>
    <MudItem md="12" sm="12">
        <MudRating SelectedValue="@_currentRating" Size="Size.Medium" FullIcon="@Icons.Filled.ThumbUp"
                   EmptyIcon="@Icons.Filled.ThumbUpOffAlt" SelectedValueChanged="RatingValueChanged"/> 
        <MudElement HtmlTag="span" Style="position:relative; top:-8px; color:gray;">
            (@_reviewCount reviews) | (@_questionCount questions)
        </MudElement>
    </MudItem>
...

Our MudRating component has various properties that we can use for reacting to events and component styling. The SelectedValue property accepts an integer, in the current case from 1 to 5, to show the total rate for the product. By default, the max number is 5, but we can change that by using the MaxValue property. We use the Size property to determine the size of our component and we also use thumb up/down icons to display values of the rating component. Finally, with the SelectedValueChanged event callback, this component will call the RatingValueChanged method every time the SelectedValue changes. You can also see, that we use the MudElement control again to show some additional info next to the rating component.

Since we are using fields and methods, which we didn’t create yet, we have to modify the ProductDetails.razor.cs file and add them:

public partial class ProductDetails
{
    public Product Product { get; set; } = new Product();

    [Inject]
    public IHttpClientRepository Repository { get; set; }

    [Parameter]
    public Guid ProductId { get; set; }

    private int _currentRating;
    private int _reviewCount;
    private int _questionCount;

    protected async override Task OnInitializedAsync()
    {
        Product = await Repository.GetProduct(ProductId);
        _reviewCount = Product.Reviews.Count;
        _questionCount = Product.QAs.Count;
        _currentRating = CalculateRating();
    }

    private int CalculateRating()
    {
        var rating = Product.Reviews.Any() ? Product.Reviews.Average(r => r.Rate) : 0;
        return Convert.ToInt32(Math.Round(rating));
    }

    private void RatingValueChanged(int value) => 
        Console.WriteLine($"The product is rated with {value} thumbs.");
}

Here we have a private field to store the values for the current rating, number of reviews, and number of questions. We populate these as soon as our component initializes. We also have two methods: CalculateRating and RatingValueChanged. The first one calculates the current rating from all the reviews that we fetch from the database, and the second one is our method that we call every time SelectedValue changes.

Now, let’s inspect our result:

Blazor Material Rating Component

And there we go. Our rating component looks great.

Including Additional Product’s Information with Expansion Panel

The first thing we are going to do here is to create three new files (AdditionalProductInfo.razor, AdditionalProductInfo.razor.cs and Reviews.razor) in the Components folder.

Then, let’s slightly modify the Product property in the ProductDetails.razror.cs file:

public Product Product { get; set; } = new Product
{
    Reviews = new List<Review>(),
    Declaration = new Declaration(),
    QAs = new List<QA>()
};

We have to add this modification to avoid an object reference exception when using any of these properties in our AdditionalProductInfo component.

Now, let’s start with the AdditionalProductInfo.razor.cs file modification:

public partial class AdditionalProductInfo
{
    [Parameter]
    public Product Product { get; set; }
    [Parameter]
    public int ReviewCount { get; set; }
    [Parameter]
    public int QuestionCount { get; set; }
}

These all are just simple parameters that we are going to accept from the parent – ProductDetails – component.

Next, we are going to modify the AdditionalProductInfo.razor file:

<MudGrid>
    <MudItem lg="8" md="12" sm="12">
        <MudExpansionPanels Elevation="2">
            <MudExpansionPanel HideIcon="true">
                <TitleContent>
                    <div class="d-flex">
                        <MudText Class="mt-1"><strong>User Reviews</strong></MudText>
                        <MudBadge Content="ReviewCount" Color="Color.Primary" Overlap="true" Class="d-flex ml-auto">
                            <MudIcon Icon="@Icons.Material.Filled.RateReview" Color="Color.Default" />
                        </MudBadge>
                    </div>
                </TitleContent>
                <ChildContent>
                    <Reviews UserReviews="Product.Reviews" />
                </ChildContent>
            </MudExpansionPanel>
        </MudExpansionPanels>
    </MudItem>
</MudGrid>

Here, inside the MudGrid component, we use the MudExpansionPanels component with a single MudExpansionPanel component (we will add more later on). Inside it, we create title content to show user reviews with some text, a badge that will show the number of reviews, and a single icon. Also, we show a child content by using our additional Reviews component. For this to work, we have to modify the Reviews.razor file:

@using Entities.Models;

<MudGrid Class="mb-4">
    @foreach (var review in UserReviews)
    {
            <MudItem md="3">
                <MudText Typo="Typo.h4">@review.Rate.ToString(" 0.0 ")</MudText>
                <MudText Typo="Typo.body2" Class="mt-1 mb-2">@review.User</MudText>
                <MudProgressLinear Color="Color.Secondary" Value="review.Rate" Min="1" Max="5" />
            </MudItem>
            <MudItem md="6">
                <MudText Typo="Typo.body2" Class="mt-4">@review.Comment</MudText>
            </MudItem>
    }
</MudGrid>

@code {
    [Parameter]
    public ICollection<Review> UserReviews { get; set; }
}

All the components, in this file, are familiar to us except the MudProgressLinear component. We use it here just to show the linear representation of a product’s rate.

Finally, we have to call the AdditionalProductInfo component inside the ProductDetails.razor file just below the MudGrid component:

<AdditionalProductInfo Product="Product" ReviewCount="_reviewCount" QuestionCount="_questionCount" />

That’s it.

We can start our app, and click the first product in the list:

Expansion panel MudBlazor

Also, as soon as we expand the Expansion Panel:

Expanded expansion panel

We can see our reviews.

Awesome.

Let’s move on and add two more panels.

Additional Panels for Questions and Declaration

We are going to expand the MudExpansionsPanel component with an additional panel in the AdditionalProductInfo.razor file:

<MudExpansionPanel HideIcon="true">
    <TitleContent>
        <div class="d-flex">
            <MudText Class="mt-1"><strong>Questions and Answers</strong></MudText>
            <MudBadge Content="QuestionCount" Color="Color.Primary" Overlap="true" Class="d-flex ml-auto">
                <MudIcon Icon="@Icons.Material.Filled.QuestionAnswer" Color="Color.Default" />
            </MudBadge>
        </div>
    </TitleContent>
    <ChildContent>
        <QuestionsAnswers QAs="Product.QAs" />
    </ChildContent>
</MudExpansionPanel>

So, nothing new here but we have to create our QuestionsAnswers component in the Components folder:

@using Entities.Models;

<MudTable Items="QAs" Hover="true" Breakpoint="Breakpoint.Sm">
    <HeaderContent>
        <MudTh>User</MudTh>
        <MudTh>Question</MudTh>
        <MudTh>Answer</MudTh>
    </HeaderContent>
    <RowTemplate>
        <MudTd DataLabel="User">@context.User</MudTd>
        <MudTd DataLabel="Question">@context.Question</MudTd>
        <MudTd DataLabel="Answer">@context.Answer</MudTd>
    </RowTemplate>
</MudTable>

@code {
    [Parameter]
    public ICollection<QA> QAs { get; set; }
}

As you can see, we are using a table component to show our data.

Now, let’s inspect the result:

QuestionsAnswers comopnent using MudBlazor Material Components

Awesome.

The icon shows two comments, and we can see both of them in our panel.

Lastly, let’s add the declaration panel in the same way we did for the previous two panels.

To do that, we have to add a new panel:

<MudExpansionPanel>
    <TitleContent>
        <MudText Class="mt-1"><strong>Declaration</strong></MudText>
    </TitleContent>
    <ChildContent>
        <Declarations ProductDeclaration="Product.Declaration" />
    </ChildContent>
</MudExpansionPanel>

And create a new Declarations component:

@using Entities.Models;

@if (ProductDeclaration != null)
{
    <MudGrid>
        <MudItem lg="3" md="3" sm="12">
            <MudText Typo="Typo.body2"><strong>Model: </strong></MudText>
        </MudItem>
        <MudItem lg="9" md="9" sm="12">
            <MudText Typo="Typo.body2">@ProductDeclaration.Model</MudText>
        </MudItem>
        <MudItem lg="3" md="3" sm="12">
            <MudText Typo="Typo.body2"><strong>Origin: </strong></MudText>
        </MudItem>
        <MudItem lg="9" md="9" sm="12">
            <MudText Typo="Typo.body2">@ProductDeclaration.Origin</MudText>
        </MudItem>
        <MudItem lg="3" md="3" sm="12">
            <MudText Typo="Typo.body2"><strong>Customer rights: </strong></MudText>
        </MudItem>
        <MudItem lg="9" md="9" sm="12">
            <MudText Typo="Typo.body2">@ProductDeclaration.CustomerRights</MudText>
        </MudItem>
    </MudGrid>
}

@code {
    [Parameter]
    public Declaration ProductDeclaration { get; set; }
}

Great.

Let’s check our result:

Declaration added

It looks great.

Using Charts in ProductDetails Page

As the last part of our page, we are going to add charts that will show information about the yearly sale of that exact product. Here, we won’t fetch the data from the API but we are going to simulate a local data storage that provides data for our charts.

That said, let’s start with the new DataStorage class creation in our client project:

public static class DataStorage
{
    public static (List<ChartSeries> ChartData, string[] Labels) GetChartData(string productName)
    {
        var chartData = new List<ChartSeries>
        {
            new ChartSeries
            {
                Name = productName,
                Data = new double[] { 23, 35, 52, 46, 40, 50 }
            }
        };

        var labels = new string[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun" };

        return (chartData, labels);
    }
}

The line chart expects the list of ChartSeries containing the Name and Data properties, and also the string array for the labels. That’s exactly what we return from this class using the Tuple with named properties. We are going to use this method for the pie chart as well. It also expects a string array for labels, and a double array for the data, which we can extract from the ChartData property.

The next step is to create two files Charts.razor and Charts.razor.cs in the Components folder to display our charts.

Let’s first modify the Charts.razor.cs class:

public partial class Charts
{
    [Parameter]
    public string ProductName { get; set; }

    private bool _lineChartDisplayed = true;
    private List<ChartSeries> _lineChartSeries;
    private double[] _pieChartData;
    private string[] _chartLabels;

    private ChartOptions _lineChartOptions = new ChartOptions
    {
        YAxisLines = true
    };

    protected override void OnParametersSet()
    {
        var chartData = DataStorage.GetChartData(ProductName);
        _lineChartSeries = chartData.ChartData;
        _pieChartData = chartData.ChartData.Select(d => d.Data).First();
        _chartLabels = chartData.Labels;
    }

    private void SwitchChart() => _lineChartDisplayed = !_lineChartDisplayed;
}

Here, we create a public ProductName property and declare it as a parameter since the parent component is going to provide that for this component. Then, we create several private variables, which we are going to use in the HTML markup of this component. We are going to use the _lineChartDisplayed variable to switch between line and pie charts. Furthermore, we are going to use the _lineChartsSeries variable to provide the data for the line chart, _pieChartData variable to provide the data for the pie chart, and _chartLabels to provide the labels for both charts. Since we want to have Y-axis lines for the line chart, we state that in the line chart options configuration.

As an addition to these variables, we have two methods as well. In the OnParameterSet lifecycle method, we fetch the data from the fake storage and populate our variables. Additionally, in the SwitchChart method, we just enable switching between two charts.

Using the Chart Component in the HTML Template file

Once we are done with all the class modifications, we can proceed to the Charts.razor file modification:

<MudGrid Class="mt-3">
    <MudItem lg="6" md="6" sm="6">
        <MudText Typo="Typo.h4">Sales over the last six months</MudText>
    </MudItem>
    <MudItem>
        <MudButton OnClick="SwitchChart" Color="Color.Primary" Variant="Variant.Filled">Switch Chart</MudButton>
    </MudItem>
    @if (_lineChartDisplayed)
    {
        <MudItem lg="10" sm="12">
            <MudChart ChartType="ChartType.Line" ChartSeries="_lineChartSeries" XAxisLabels="_chartLabels"
                      ChartOptions="_lineChartOptions" />
        </MudItem>
    }
    else
    {
        <MudItem lg="8" sm="12">
            <MudChart ChartType="ChartType.Pie" InputData="_pieChartData" InputLabels="_chartLabels"
                      Width="300px" Height="300px" />
        </MudItem>
    }
</MudGrid>

Here, we create a title for the chart part of the page, create a button to switch between charts, and also show the charts themselves. To organize it on the screen, we use the MudGrid and MudItem components.

Now, we can start our app, navigate to the FetchData page and click the first product:

Line chart for the Product details page

As you can see, in addition to all the other material components, we have the line chart as well. Moreover, if we click the Switch Chart button:

Pie chart for the Product details page

We are going to see the pie chart.

Excellent.

Now we have a complete product details page.

Conclusion

In this article, we have used a lot of different material components to create a nice looking and interactive product details page for our Blazor material application.

In the next one, we are going to learn how to use forms, dialogs, and upload components to create a user form for the product creation.

Until that one.

All the best.