Working With Forms

Any successful e-commerce site needs to be able to fulfil orders. It's difficult to do this if you don't have the contact details and shipping address of the customer. The way that sites collect this type of information is in a form.

In this section, you will add a form to the Order page. You will also add validation to the form to ensure that the information you collect is valid and complete.

Preparation

To being with, add the following style declarations to the site.css file found in wwwroot/css:

label[for="OrderQuantity"]{
  padding-left: 15px;
}

#OrderQuantity{
  margin: 0 15px;
  max-width: 100px;
}

.order-calc{
  display: inline-block;
  margin: 0 10px;
}

.input-validation-error, .input-validation-error:focus {
  background: #ffccba;
  color: #d63301;
}

.form-control.input-validation-error:focus{
  border-color: #d63301;
  box-shadow: 0 0 0 0.2rem rgba(255, 123, 123, 0.5);
}
      
.field-validation-error {
  color: #be3e16;
}

.validation-summary-errors {
  color: #be3e16;
}

The first three styles are purely cosmetic. The last four of these styles will help to identify the location of field validation errors easily. Now, add the highlighted lines below to Order.cshtml.cs:

using System;
using System.ComponentModel.DataAnnotations;
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;}
        [BindProperty, EmailAddress, Required, Display(Name="Your Email Address")]
        public string OrderEmail { get; set; }
        [BindProperty, Required(ErrorMessage="Please supply a shipping address"), Display(Name="Shipping Address")]
        public string OrderShipping { get; set; } 
        [BindProperty, Display(Name="Quantity")]
        public int OrderQuantity { get; set; } = 1;

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

The additional using directive brings the DataAnnotations namespace into play. This namespace includes a number of attributes, some of which are used to decorate the properties that have been added to the PageModel. The OrderEmail property has DataAnnotation attributes applied (along with the BindProperty attribute):

  • EmailAddress - which validates whether a string matches the pattern of an email address
  • Required - which specifies that a value is required
  • Display - enables you to customise the value used in the UI for the property

Two of these attributes play a role in validation, as you will see later. The OrderQuantity property is assigned a default value of 1.

Adding The Form

Add the following lines to the bottom of Order.cshtml:

<form method="post">
    <div class="row">
        <div class="col-3">
            <img src="~/Images/Products/Thumbnails/@Model.Product.ImageName" class="img-fluid img-thumbnail" alt="Image of @Model.Product.Name"/>
        </div>
        <div class="col-9">
            <ul class="orderPageList" data-role="listview">
                <li>
                    <div>
                        <p class="description">@Model.Product.Description</p>
                    </div>                
                </li>
                <li class="email">
                    <div class="form-group">
                        <label asp-for="OrderEmail"></label>
                        <input asp-for="OrderEmail" class="form-control form-control-sm" />                
                        <span asp-validation-for="OrderEmail"></span>
                    </div>
                </li>
                <li class="shipping">
                    <div class="form-group">
                        <label asp-for="OrderShipping"></label>
                        <textarea rows="4" asp-for="OrderShipping" class="form-control form-control-sm"></textarea>               
                        <span asp-validation-for="OrderShipping"></span>
                    </div>
                </li>
                <li class="quantity">
                    <div class="form-group row">
                        <label asp-for="OrderQuantity" class="col-1 col-form-label"></label>
                        <input asp-for="OrderQuantity" class="form-control form-control-sm"/>
                        x
                        <span class="order-calc" id="orderPrice">@Model.Product.Price.ToString("f")</span>
                        =
                        <span class="order-calc" id="orderTotal">@Model.Product.Price.ToString("f")</span>
                        <span asp-validation-for="OrderQuantity"></span>
                    </div>
                </li>
            </ul>
            <p class="actions">
                <input type="hidden" asp-for="Product.Id" />
                <button class="btn btn-danger order-button">Place Order</button>
            </p>
        </div>
    </div>
</form>


@section scripts{
<script type="text/javascript">
    $(function () {
        var price = parseFloat($("#orderPrice").text()).toFixed(2),
            total = $("#orderTotal"),
            orderQty = $("#OrderQuantity");

        orderQty.on('change', function () {
            var quantity = parseInt(orderQty.val());
            if (!quantity || quantity < 1) {
                orderQty.val(1);
                quantity = 1;
            } else if (quantity.toString() !== orderQty.val()) {
                orderQty.val(quantity);
            }
            total.text("$" + (price * quantity).toFixed(2));
        });
    });
</script>
}

The form makes use of tag helpers to render the labels, inputs and the validation messages. The tag helpers that target input elements are particularly powerful. PageModel properties are passed to the asp-for attributes on the input tag helpers. The input tag helpers then render the correct name attribute based on the property, so that model binding works seamlessly. Any values assigned to the properties are assigned to the input. The input's type attribute is generated according to the data type of the property.

The form itself will also be targeted by a tag helper, which will ensure that a request verification token is rendered.

The code ends with a block of JavaScript that calculates the total price based on the quantity of items being ordered. The <script> tags are placed in an @section block named scripts, which is positioned carefully in the layout page to render its contents below other site-wide scripts, including jQuery, which this script block depends upon.

Next, you need a handler method for processing the form. Add the following to the Order.cshtml.cs file:

public async Task<IActionResult> OnPostAsync()
{
    Product = await db.Products.FindAsync(Id);
    if(ModelState.IsValid){
        return RedirectToPage("OrderSuccess");
    }
    return Page();
}

For the moment, all that this method does is to check that ModelState.IsValid. This ensures that the model binding feature is satisfied that all submitted values pass validation checks, and that all required values are present. If there are any errors in validation, entries are added to the ModelStateobject and the current page is redisplayed (return Page()). If the submission is valid, the user is redirected to an OrderSuccess page, which you will add next.

This pattern is known as Post-Redirect-Get (PRG) and is designed to minimise the chance of duplicate submissions resulting from double posting.

If there are errors in the form, the page is displayed again and the validation tag helpers will display details of the validation errors.

Now add the OrderSuccess page to the application using the following command:

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

Replace the content of the OrderSuccess.cshtml file with the following:

@page
@model Bakery.Pages.OrderSuccessModel
@{
}
<ol id="orderProcess">
    <li><span class="step-number">1</span>Choose Item</li>
    <li><span class="step-number">2</span>Details &amp; Submit</li>
    <li class="current"><span class="step-number">3</span>Receipt</li>
</ol>
<h1>Order Confirmation</h1>

Now it's time to test that the form works and is processed correctly. Launch the application by typing dotnet run, and then navigate to https://localhost:5001. Select the Lemon Tart product, and ensure that the form has displayed correctly:

Lemon Tart

Now press the Place Order button without entering an email or shipping address, so that you can test the validation. Both fields should turn pink and the error message should appear below them:

Razor Pages Form Validation

You can perform further tests such as removing the value in the Quantity box, or entering a random string into the email input. Each time, the error messages should appear once you have submitted the form.

Adding Client Side Validation

At the moment, all the validation is performed on the server. The form is posted, and the entire page is re-rendered to provide feedback to the user. The round trip is not really noticeable to you while developing the site, because the client and the server are on the same machine. However, in a real world application, there could be some delay before the user receives any feedback. Validating on the client will provide the user with instant feedback.

Validating on the client should be seen as a courtesy to the user only. It should never replace server-side validation. It is very easy for anyone with a small amount of HTML/JavaScript knowledge to circumvent client-side validation.

Client side validation is included by default in Razor Pages, but it needs to be enabled. You do that by including the jQuery Validation and jQuery Unobtrusive Validation libraries in your page. The code for including these scripts is already available in a partial named _ValidationScriptsPartial.cshtml which is located in the Pages/Shared folder. To include it, just add the partial tag helper to the scripts section in Order.cshtml as illustrated in the highlighted line of code below:

@section scripts{
<partial name="_ValidationScriptsPartial"></partial>
<script type="text/javascript">
    $(function () {
        var price = parseFloat($("#orderPrice").text()).toFixed(2),
            total = $("#orderTotal"),
            orderQty = $("#OrderQuantity");

        orderQty.on('change', function () {
            var quantity = parseInt(orderQty.val());
            if (!quantity || quantity < 1) {
                orderQty.val(1);
                quantity = 1;
            } else if (quantity.toString() !== orderQty.val()) {
                orderQty.val(quantity);
            }
            total.text("$" + (price * quantity).toFixed(2));
        });
    });
</script>
}

Now if you try submitting the form with missing values, the errors appear without the form actually being posted to the server. And if you provide values that satisfy the validation, you should get taken to the OrderSuccess page:

Razor Pages Bakery

Summary

In this section, you have created a form using tag helpers and added both server-side and client-side validation. You have tested the form successfully. So far, you haven't done anything meaningful with the posted values. You will do so in the next section, when you will use the posted values to construct an email and send it.

Last updated: 07/12/2018 07:40:29

© 2019 - Learn Razor Pages.
All rights reserved.