Bakery Tutorial

This is part of a tutorial series that shows how to build a data-driven 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

Working With Data

In this first look at working with data in a Razor Pages application, you will focus on using the BakeryContext to retrieve data for display on the home page and the ordering page, which has yet to be added to the application. You are working towards producing a home page that looks like this, where all of the products are displayed together with their description, image and price:

Bakery Template

The PageModel

First, you will inject the BakeryContext into the IndexModel class and use it to populate a collection of Product entities. Open the Pages/Index.cshtml.cs file and replace the existing contents with the following:

using Bakery.Data;
using Bakery.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;

namespace Bakery.Pages;

public class IndexModel : PageModel
{
    private readonly BakeryContext context;
    public IndexModel(BakeryContext context) =>
        this.context = context;
    
    public List<Product> Products { get; set; } = new ();
    public async Task OnGetAsync() =>
        Products = await context.Products.ToListAsync();
}

This is the PageModel file. Recall that the PageModel acts as a combined page controller and view model, the latter representing the data required for a view. Most often, the view is a page, but it could be a partial or a view component, which you will meet in a later sections of this tutorial series.

As a controller, the role of the PageModel is to process information from a request, and then prepare the model for the view (the view model). There is a one-to-one mapping between PageModels and Content Pages (the view) so the PageModel itself is the view model.

Information from the request is processed within handler methods. This PageModel has one handler method - OnGetAsync, which is executed by convention whenever an HTTP GET request is made to the page. The PageModel has a private field named context which is a BakeryContext type. It also has a constructor method that takes a BakeryContext object as a parameter. The parameter's value is provided by the dependency injection system. This pattern is known as construction injection.

The PageModel class has a public property Products - a list of products which is populated in the OnGetAsync method.

The Content Page

Now it's time to produce the UI. Replace the code in the Index content page (Pages/Index.cshtml) with the following:

@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<h1 class="fw-light">Welcome to the Bakery Shop</h1>
<div class="row">
    @foreach (var product in Model.Products)
    {
        <div class="col-3 p-1">
            <div class="card h-100">
                <img src="/images/products/@product.ImageName" class="img-fluid card-img-top" alt="@product.Name" />
                <div class="card-body">
                    <h5 class="card-title">@product.Name</h5>
                    <p class="card-text">@product.Description</p>
                    <div class="d-flex justify-content-between">
                    <span class="card-text">@(product.Price.ToString("c"))</span>
                    <a class="btn btn-danger btn-sm" asp-page="/Order" asp-route-id="@product.Id">Order Now</a>
                    </div>
                </div>
            </div>
        </div>
    }
</div>

The @model directive at the top of the page specifies the type for the page's model (IndexModel). You work with the PageModel through the content page's Model property.

The code loops through all of the products and displays their details including their image. Each product includes a hyperlink, styled like a button (using Bootstrap styling). Although it doesn't go anywhere yet, the hyperlink is generated by an anchor tag helper, which includes an asp-route attribute. This attribute is used for passing data as route values to the target page. The route parameter is named id, and the product's key value is passed to the attribute. You will see how this works in more detail when you add the Order page, which is the next step.

In the meantime, test the application by executing dotnet watch at the terminal which should result in the application launching in a browser. The home page should look like the image at the beginning of this section.

Adding The Order Page

If you inspect the href values of the Order Now buttons, you will see that they are presently empty. This is what happens if the anchor tag helper is unable to find a route for the path passed in to the asp-for attribute. You will resolve this by adding a new page which will initially attemtpt to fetch and display details of the product associated with the id value passed in the URL when the button is clicked.

Execute the following command in the terminal to add a new page called Order:

dotnet new page --name Order --namespace Bakery.Pages --output Pages

Open the newly created Order.cshtml.cs file and replace the content with the following:

using Bakery.Data;
using Bakery.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace Bakery.Pages;

public class OrderModel : PageModel
{
    private BakeryContext context;

    public OrderModel(BakeryContext context) => 
        this.context = context;

    [BindProperty(SupportsGet = true)]
    public int Id { get; set; }

    public Product Product { get; set;}

    public async Task OnGetAsync() =>  
        Product = await context.Products.FindAsync(Id);
}

Again, the BakeryContext is injected into the PageModel constructor. A public property, Product, is instantiated in the OnGetAsync method. The FindAsync method takes a value representing the primary key of the entity to be returned. In this case, the parameter passed to the FindAsync method is another public property - Id. But where does it get its value from?

The Id property is decorated with the BindProperty attribute. This attribute ensures that the property is included in the model binding process, which results in values passed as part of an HTTP request being mapped to PageModel properties and handler method parameters. By default, model binding only works for values passed in a POST request. The Order page is reached by clicking a link on the home page, which results in a GET request. You must add SupportsGet = true to opt in to model binding on GET requests.

If you recall, the anchor tag helpers on the home page that link to the Order page include an asp-route-id attribute, representing a route value named id. By default, route values are passed as name/value pairs in the query string of the URL: /order&id=1. Razor Pages also supports passing route values as segments of the URL: /order/1. This is accomplished by defining a route parameter as part of a route template within the content page. That is what you will do as part of the next step. Amend the content of Order.cshtml as follows:

@page "{id:int}"
@model Bakery.Pages.OrderModel
@{
  ViewData["Title"] = "Place your order";
}
<div class="progress mb-3" style="height:2.5rem;">
  <div class="progress-bar w-50" style="font-size:1.5rem;" role="progressbar">Select Item</div>
</div>
<div class="row">
  <div class=" col-3">
    <h3>@Model.Product.Name</h3>
    <img class="img-fluid" src="/images/products/@Model.Product.ImageName" title="@Model.Product.Name" />
    <p>@Model.Product.Description</p>
  </div>
</div>

The first line of code contains the @page directive, which is what makes this a Razor Page, and it also includes the following: "". This is a route template. This is where you define route parameters for the page. This template defines a parameter named id which will result in the anchor tag helpers on the home page generating URLs with the id value as a segment rather than as a query string value. You have also added a constraint. In this case, you have specified that the value of id must be an integer (:int). The order page cannot be reached unless a integral value for the id route value is provided. If the vaue is missing, or is not an integer, Razor Pages will return a 404 Not Found response.

Now if you run the application and click on one of the buttons on the home page, the order page is displayed with the name of the selected product:

Razor Pages Bakery

Summary

You have successfully used the BakeryContext to connect to the database and retrieve data which has been assigned to properties of the PageModel. They are exposed via the Model property in the content page. You used Razor syntax to loop through a collection of products to display them. You have also seen in this section how to pass data in URLs and leverage the BindProperty attribute to map route values to public properties in the PageModel. Finally, you have seen how to use that value to query for a specific item so that you can display details of it.

Soon you will start working with forms to collect user details for an order. The form will rely on some CSS and JavaScript. Before you build the form, you will add support for Sass - a technology that makes working with CSS easier.

Last updated: 12/3/2024 8:22:35 AM

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