Bakery Tutorial

This is part of a tutorial series that shows how to build a data-drive web application using Visual Studio Code, ASP.NET Core Razor Pages, Entity Framework Core and SQLite and .NET 7.

The source code for the completed application is available on GitHub

View Components in Razor Pages

If you frequently engage in online shopping, you likely recognise the familiar sight of a basket icon displaying a number representing the quantity of items it holds.

Shopping Basket

These icons are typically found in a consistent area across all pages of a website - the Layout file in the context of a Razor Pages application. But how does this basket obtain its data?

The primary data source for a layout page is the current page itself. One approach could involve generating the basket's data in every page of the site, or alternatively, using a base page that all pages derive from. Alterntively, it is technical possible to inject some kind of service into the layout page that provides the data, However, these methods are not advisable due to maintenance concerns and potential issues with tight coupling.

A flexible and maintainable solution for this requirement is the use of a view component. By employing a view component, you can encapsulate the logic and rendering for the shopping basket into a testable and reusable component. View components are resposible for their own data retrieval, so they promote separation of concerns.

View components consist of a class file and a .cshtml view file. The class file contains the logic for generating the model. It can be thought of as a mini-controller, just as the Razor PageModel file is considered to be a page controller. The view file contains the template used to generate the HTML to be plugged in to the page that hosts the view component.

The class file must conform to the following rules:

  • It must derive from the ViewComponent class
  • It must have "ViewComponent" as a suffix to the class name or it must be decorated with the [ViewComponent] attribute (or derive from a class that's decorated with the [ViewComponent] attribute)
  • It must implement a method named Invoke with a return type of IViewComponentResult (or InvokeAsync returning Task<IViewComponentResult> if you need to call asynchronous methods).

By default, the view file is named Default.cshtml (case-insensitive). The view file's placement within the application's file structure is important, because the framework searches pre-defined locations for it:

/Pages/Shared/Components/<component name>/Default.cshtml /Views/Shared/Components/<component name>/Default.cshtml

The framework will also search by walking up the directory tree from the location of the calling page until it reaches the root Pages folder. This is the same search pattern as for partials. You will use the first search location as the location for your basket view component.

Add a folder to the Pages/Shared folder name Components and within that, add a folder named Basket. Having created the folder structure, execute the following command to generate a Razor view file within the Basket folder:

dotnet new view -n Default -o Pages/Shared/Components/Basket 

Next, add a C# class file named BasketViewComponent to the Basket folder using the following command:

dotnet new class -n BasketViewComponent -o Pages/Shared/Components/Basket

Change the content of the BasketViewComponent class as follows. The logic depends on System.Text.Json, and is very similar to the code you used for the cookie in the previous section. It deserialses the basket cookie and passes the resulting data to a View method that returns a ViewViewComponentResult which is an implementation of IViewComponentResult:

using System.Text.Json;
using Microsoft.AspNetCore.Mvc;

namespace Bakery.Pages.Components.Basket;

public class BasketViewComponent : ViewComponent
{
    public IViewComponentResult Invoke()
    {
        Models.Basket basket = new();
        if(Request.Cookies[nameof(Basket)] is not null)
        {
            basket = JsonSerializer.Deserialize<Models.Basket?>(Request.Cookies[nameof(Basket)]!);
        }
        return View(basket);
    }
}

In this example, the data retrieval logic requires no external dependencies. More commonly, you might obtain data from a database. View components are just like PageModel classes in that they act as the controller for a view, although in this case, it's a partial view. So just like with a PageModel, you can inject dependencies into their constructor. The following example illustrates how you would do that and use the InvokeAsync method to retrieve data asyncronously:

public class BasketViewComponent : ViewComponent
{
    private readonly BakeryContext context;

    public BasketViewComponent(BakeryContext context)
    {
        this.context = context;
    }

    public async Task<IViewComponentResult> InvokeAsync()
    {
        var data = await context.Products.ToListAsync();
        return View(data);
    }
}

Now to the example and the view itself (which is essentially a partial page). Open the Default.cshtml file you created and replace its content with this code, which makes use of the Bootstrap badge component and icons to generate a button with a shopping basket and an indication of the number of items it holds, like the image at the beginning of this section:

@model Bakery.Models.Basket
@if(Model.Items.Any())
{
    <button type="button" class="btn btn-sm btn-primary position-relative" title="@Model.NumberOfItems items in basket">
        <i class="bi bi-cart4"></i>
        <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
            @Model.NumberOfItems
        </span>
    </button>
}

The cart icon comes from Bootstrap icons, which you need to include in the page. Open the Layout.cshtml file and add the following to the head element:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">

This references version 1.10.5 from a CDN, which is the current version at the time of writing.

Finally, you need to render the view component. You can do this in one of two ways. You can use the Component.InvokeAsync method, or you can use a tag helper. You will use the first of these options. You can find out more about using the tag helper approach here.

Open the Layout.cshtml file and place the highlighted line after the existing navigation:

<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
    <ul class="navbar-nav flex-grow-1">
        <li class="nav-item">
            <a class="nav-link text-dark" asp-page="/About">About</a>
        </li>
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
        </li>
    </ul>
    @await Component.InvokeAsync(nameof(Models.Basket))
</div>

Run the application and order a few products. You should see the view component appear after ordering the first one, and the number on the badge increment with each additional order.

Summary

Whenever you need to include a data-driven widget in a layout page, the first solution to reach for is a view component. In this section, you creaeted a class that derives from ViewComponent and a view file named Default.cshtml located in one of the default search locations. The ViewComponent class is responisble for data retrieval logic and the view file is the presentation template.

The application is developing nicely and acquiring new features. In the next section, you will use Mailkit to add email capability so that you can send order confirmations to customers.

Last updated: 23/11/2023 09:12:30

© 2018 - 2024 - Mike Brind.
All rights reserved.
Contact me at Outlook.com