Working With Data

In this first look at working with data, 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. As a reminder, the home page should look similar to the ASP.NET Web Pages version here:

ASP.NET Web Pages Bakery Template

All of the products are displayed together with their descriptions, image and price, with one of the products selected randomly to appear as the featured product at the top of the page.

A small amount of preparation is required to manage the display of the data. So to begin with, add the following code to the existing site.css file located in wwwroot/css:

body{
  color: #696969;
}
a:link {
  color: #3b3420;
  text-decoration: none;
}

a:visited {
  color: #3b3420;
  text-decoration: none;
}

a:hover {
  color: #a52f09;
  text-decoration: none;
}

a:active {
  color: #a52f09;
}

a.order-button, a.order-button:hover{
  color: #fdfcf7;
}

.productInfo, .action{
  max-width: 200px;
}

p{
  font-size: 0.8rem;
}

#orderProcess {
  list-style: none;
  padding: 0;
  clear: both;
}

#orderProcess li {
  color: #696969;
  display: inline;
  font-size: 1.2em;
  margin-right: 15px;
  padding: 3px 0px 0px 5px;
}

.step-number {
    background-color: #edece8;
    border: 1px solid #e6e4d9;
    font-size: 1.5em;
    margin-right: 5px;
    padding: 3px 10px;
}

.current .step-number {
    background-color: #a52f09;
    border-color: #712107;
    color: #fefefe;
} 

.orderPageList{
    padding-inline-start: 20px;
}

.actions .order-button{
  margin-left:20px;
}

The PageModel

Now, open the Pages/Index.cshtml.cs file and replace the contents with the following:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bakery.Data;
using Bakery.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;

namespace Bakery.Pages
{
    public class IndexModel : PageModel
    {
        private readonly BakeryContext db;  

        public IndexModel(BakeryContext db) => this.db = db;

        public List<Product> Products { get; set; } = new List<Product>();  
        public Product FeaturedProduct { get; set; }  

        public async Task OnGetAsync()
        {
            Products = await db.Products.ToListAsync();
            FeaturedProduct = Products.ElementAt(new Random().Next(Products.Count));
        }
    }
}

This is the PageModel file. The PageModel acts as a combined page controller and view model. As a controller, its role is to process information from a request, and then prepare a model for the view (the viewmodel). There is a one-to-one mapping between PageModels and Content Pages (the view) so the PageModel itself is the viewmodel.

Information from the request is processed within handler methods. This PageModel has one handler method - OnGetAsync, which is executed by convention as a result of HTTP requests made with the GET verb. The PageModel has a private field named db 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 parameter is assigned to the private field within the constructor (using an expression body).

The PageModel class has two public properties - a list of products, and a single product representing the featured product that appears at the top of the page. The list is populated by the following code from the OnGetAsync method:

Products = await db.Products.ToListAsync();

The next line of code in the OnGetAsync method assigns one of the products to the FeaturedProduct property randomly:

FeaturedProduct = Products.ElementAt(new Random().Next(Products.Count));

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>Welcome to Fourth Coffee!</h1>

<div id="featuredProduct" class="row"> 
    <div class="col-sm-8">
        <img alt="Featured Product" src="~/Images/Products/@Model.FeaturedProduct.ImageName" class="img-fluid rounded"/>
    </div>
    <div id="featuredProductInfo" class="col-sm-4">
        <div id="productInfo">
            <h2>@Model.FeaturedProduct.Name</h2>
            <p class="price">[email protected]("{0:f}", Model.FeaturedProduct.Price)</p>
            <p class="description">@Model.FeaturedProduct.Description</p>
        </div>
        <div id="callToAction">
            <a class="btn btn-danger order-button" asp-page="/order" asp-route-id="@Model.FeaturedProduct.Id" title="Order @Model.FeaturedProduct.Name">Order Now</a>
        </div>
    </div>
</div>

<div id="productsWrapper" class="row">
@foreach (var product in Model.Products)
{
    <div class="col-sm-3">
        <a asp-page="/order" asp-route-id="@product.Id" title="Order @product.Name">
            <div class="productInfo">
                <h3>@product.Name</h3>
                <img class="product-image img-fluid img-thumbnail" src="~/Images/Products/Thumbnails/@product.ImageName" alt="Image of @product.Name" />
                <p class="description">@product.Description</p>
            </div>
        </a>

        <div class="action">
            <p class="price float-left">[email protected]("{0:f}", product.Price)</p>
            <a class="btn btn-sm btn-danger order-button float-right" asp-page="/order" asp-route-id="@product.Id" title="Order @product.Name">Order Now</a>
        </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 top section of the HTML shows the featured product. The bottom section loops through all of the products and displays their thumbnail 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. You will see how this works when you add the Order page, which is the next step.

In the meantime, test the application by executing dotnet run at the terminal and then browse to https://localhost:5001. The home page should look like this:

Bakery Home Page

Mobile users

The original Web Pages Bakery template uses device detection or browser sniffing to serve the needs of mobile users. If the user is detected as using a mobile device (largely inferred from values found in the user-agent header), the site switches to using a different layout file (sitelayout.mobile.cshtml). There are two problems with this approach: first, you need to keep your device detection library up to date, otherwise it may end up failing more than it succeeds; and second, you need to maintain multiple versions of layout files and style sheets for the site.

These days, the solution to the problem of dealing with differing device resolutions is to use Responsive Design, a technique that detects the screen size available, and reflows content accordingly. This capability is built into Bootstrap, and you can see how it works by resizing the browser that you have open currently. As soon as the width of the browser window falls below 576px, the display changes dramatically:

Responsive Design

The featured product image has scaled down in size, and the other product panels have stacked vertically. In addition, the menu is now hidden, to be invoked when the user taps/clicks on the button with the 3 horizontal bars.

The 576px breakpoint is determined by the use of the col-sm-* class in the divs, where sm is the relevant part of the CSS class name. If we had used e.g. col-md-3 instead, the contents would have stacked when the browser width fell below 768px. You can learn more about the Bootstrap Grid system here.

Of course, because responsive design techniques are not device-dependant, you don't need to rely on emulators to test them.

Adding The Order Page

Add a new page by executing the following command in the terminal:

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 System;
using System.Threading.Tasks;
using Bakery.Data;
using Bakery.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace Bakery.Pages
{
    public class OrderModel : PageModel
    {
        private BakeryContext db;

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

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

        public Product Product { get; set;}

        public async Task OnGetAsync() =>  Product = await db.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. Route values are passed as part of the URL. If the receiving page has a matching route parameter defined, the value is passed as a segment of the URL e.g. order/3. Otherwise it is passed as a query string value: order?id=3. Either way, the incoming value will be bound to the Id property.

Next, amend the content of Order.cshtml as follows:

@page "{id:int}"
@model Bakery.Pages.OrderModel
@{
    ViewData["Title"] = "Place Your Order";
}
<ol id="orderProcess">
    <li><span class="step-number">1</span>Choose Item</li>
    <li class="current"><span class="step-number">2</span>Details &amp; Submit</li>
    <li><span class="step-number">3</span>Receipt</li>
</ol>
<h1>Place Your Order: @Model.Product.Name</h1>

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). You have also added a constraint. In this case, you have specified that the value of id must be an integer (:int). Basically, this means that the order page cannot be reached unless a integral value for the id route value is provided.

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, where you looped through the 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.

In the next section, you will enable the user to provide contact and shipping details for their order via a form.

Last updated: 07/12/2018 07:38:59

© 2019 - Learn Razor Pages.
All rights reserved.