Unverified Commit b6c29b13 authored by Steve Sanderson's avatar Steve Sanderson Committed by GitHub
Browse files

Update to 3.0 preview 6

parent fa013e3f
<Project>
<PropertyGroup>
<AspNetCoreVersion>3.0.0-preview6.19307.2</AspNetCoreVersion>
<EntityFrameworkVersion>3.0.0-preview6.19304.10</EntityFrameworkVersion>
</PropertyGroup>
</Project>
......@@ -6,12 +6,12 @@ In this session we'll update the pizza store app to enable users to customize th
When the user clicks a pizza special a pizza customization dialog should pop up to allow the user to customize their pizza and add it to their order. To handle DOM UI events in a Blazor app, you specify which event you want to handle using the corresponding HTML attribute and then specify the C# delegate you want called. The delegate may optionally take an event specific argument, but it's not required.
In *Pages/Index.razor* add the following `onclick` handler to the list item for each pizza special:
In *Pages/Index.razor* add the following `@onclick` handler to the list item for each pizza special:
```html
@foreach (var special in specials)
{
<li onclick="@(() => Console.WriteLine(special.Name))" style="background-image: url('@special.ImageUrl')">
<li @onclick="@(() => Console.WriteLine(special.Name))" style="background-image: url('@special.ImageUrl')">
<div class="pizza-info">
<span class="title">@special.Name</span>
@special.Description
......@@ -23,7 +23,7 @@ In *Pages/Index.razor* add the following `onclick` handler to the list item for
Run the app and check that the pizza name is written to the browser console whenever a pizza is clicked.
![onclick-event](https://user-images.githubusercontent.com/1874516/51804286-ce965000-2256-11e9-87fc-a8770ccc70d8.png)
![@onclick-event](https://user-images.githubusercontent.com/1874516/51804286-ce965000-2256-11e9-87fc-a8770ccc70d8.png)
The `@` symbol is used in Razor files to indicate the start of C# code. Surround the C# code with parens if needed to clarify where the C# code begins and ends.
......@@ -52,10 +52,10 @@ void ShowConfigurePizzaDialog(PizzaSpecial special)
}
```
Update the `onclick` handler to call the `ShowConfigurePizzaDialog` method instead of `Console.WriteLine`.
Update the `@onclick` handler to call the `ShowConfigurePizzaDialog` method instead of `Console.WriteLine`.
```html
<li onclick="@(() => ShowConfigurePizzaDialog(special))" style="background-image: url('@special.ImageUrl')">
<li @onclick="@(() => ShowConfigurePizzaDialog(special))" style="background-image: url('@special.ImageUrl')">
```
## Implement the pizza customization dialog
......@@ -134,7 +134,7 @@ Now the dialog shows a slider that can be used to change the pizza size. However
We want to make it so the value of the `Pizza.Size` will reflect the value of the slider. When the dialog opens, the slider gets its value from `Pizza.Size`. Moving the slider should update the pizza size stored in `Pizza.Size` accordingly. This concept is called two-way binding.
If you wanted to implement two-way binding manually, you could do so by combining value and onchange, as in the following code (which you don't actually need to put in your application, because there's an easier solution):
If you wanted to implement two-way binding manually, you could do so by combining value and @onchange, as in the following code (which you don't actually need to put in your application, because there's an easier solution):
```html
<input
......@@ -143,23 +143,23 @@ If you wanted to implement two-way binding manually, you could do so by combinin
max="@Pizza.MaximumSize"
step="1"
value="@Pizza.Size"
onchange="@((UIChangeEventArgs e) => Pizza.Size = int.Parse((string) e.Value))" />
@onchange="@((UIChangeEventArgs e) => Pizza.Size = int.Parse((string) e.Value))" />
```
In Blazor you can use the `bind` attribute to specify a two-way binding with this behavior. The equivalent markup using `bind` looks like this:
In Blazor you can use the `@bind` directive attribute to specify a two-way binding with this behavior. The equivalent markup using `@bind` looks like this:
```html
<input type="range" min="@Pizza.MinimumSize" max="@Pizza.MaximumSize" step="1" bind="@Pizza.Size" />
<input type="range" min="@Pizza.MinimumSize" max="@Pizza.MaximumSize" step="1" @bind="@Pizza.Size" />
```
But if we use `bind` with no further changes, the behavior isn't exactly what we want. Give it a try and see how it behaves. The `onchange` event only fires after the slider is released.
But if we use `@bind` with no further changes, the behavior isn't exactly what we want. Give it a try and see how it behaves. The update event only fires after the slider is released.
![Slider with default bind](https://user-images.githubusercontent.com/1874516/51804870-acec9700-225d-11e9-8e89-7761c9008909.gif)
We'd prefer to see updates as the slider is moved. Data binding in Blazor allows for this by letting you specify what value you want to bind to and what event triggers a change using the syntax `bind-<value>-<event>`. So, to bind using the `oninput` event instead do this:
We'd prefer to see updates as the slider is moved. Data binding in Blazor allows for this by letting you specify what event triggers a change using the syntax `@bind:<eventname>`. So, to bind using the `oninput` event instead do this:
```html
<input type="range" min="@Pizza.MinimumSize" max="@Pizza.MaximumSize" step="1" bind-value-oninput="@Pizza.Size" />
<input type="range" min="@Pizza.MinimumSize" max="@Pizza.MaximumSize" step="1" @bind-value="@Pizza.Size" @bind-value:event="oninput" />
```
The pizza size should now update as you move the slider.
......@@ -206,7 +206,7 @@ Add the following markup in the dialog body for displaying a drop down list with
}
else
{
<select class="custom-select" onchange="@ToppingSelected">
<select class="custom-select" @onchange="@ToppingSelected">
<option value="-1" disabled selected>(select)</option>
@for (var i = 0; i < toppings.Count; i++)
{
......@@ -222,7 +222,7 @@ Add the following markup in the dialog body for displaying a drop down list with
<div class="topping">
@topping.Topping.Name
<span class="topping-price">@topping.Topping.GetFormattedPrice()</span>
<button type="button" class="delete-topping" onclick="@(() => RemoveTopping(topping.Topping))">x</button>
<button type="button" class="delete-topping" @onclick="@(() => RemoveTopping(topping.Topping))">x</button>
</div>
}
</div>
......@@ -269,15 +269,15 @@ Add two parameters to the `ConfigurePizzaDialog` component: `OnCancel` and `OnCo
[Parameter] EventCallback OnConfirm { get; set; }
```
Add `onclick` event handlers to the `ConfigurePizzaDialog` that trigger the `OnCancel` and `OnConfirm` events.
Add `@onclick` event handlers to the `ConfigurePizzaDialog` that trigger the `OnCancel` and `OnConfirm` events.
```html
<div class="dialog-buttons">
<button class="btn btn-secondary mr-auto" onclick="@OnCancel">Cancel</button>
<button class="btn btn-secondary mr-auto" @onclick="@OnCancel">Cancel</button>
<span class="mr-center">
Price: <span class="price">@(Pizza.GetFormattedTotalPrice())</span>
</span>
<button class="btn btn-success ml-auto" onclick="@OnConfirm">Order ></button>
<button class="btn btn-success ml-auto" @onclick="@OnConfirm">Order ></button>
</div>
```
......@@ -341,7 +341,7 @@ Create a new `ConfiguredPizzaItem` component for displaying a configured pizza.
```html
<div class="cart-item">
<a onclick="@OnRemoved" class="delete-item">x</a>
<a @onclick="@OnRemoved" class="delete-item">x</a>
<div class="title">@(Pizza.Size)" @Pizza.Special.Name</div>
<ul>
@foreach (var topping in Pizza.Toppings)
......@@ -371,7 +371,7 @@ Add the following markup to the `Index` component just below the main `div` to a
@foreach (var configuredPizza in order.Pizzas)
{
<ConfiguredPizzaItem Pizza="configuredPizza" OnRemoved="() => RemoveConfiguredPizza(configuredPizza)" />
<ConfiguredPizzaItem Pizza="configuredPizza" OnRemoved="@(() => RemoveConfiguredPizza(configuredPizza))" />
}
</div>
}
......@@ -383,7 +383,7 @@ Add the following markup to the `Index` component just below the main `div` to a
<div class="order-total @(order.Pizzas.Any() ? "" : "hidden")">
Total:
<span class="total-price">@order.GetFormattedTotalPrice()</span>
<button class="btn btn-warning" disabled="@(order.Pizzas.Count == 0)" onclick="@PlaceOrder">
<button class="btn btn-warning" disabled="@(order.Pizzas.Count == 0)" @onclick="@PlaceOrder">
Order >
</button>
</div>
......
......@@ -27,13 +27,19 @@ This shows it's not strictly necessary to use `<NavLink>`. We'll see the reason
## Adding a "My Orders" page
If you click "My Orders", nothing will seem to happen. Open your browser's dev tools and look in the JavaScript console. You should see that attempting to navigate to `myorders` produces an error similar to:
If you click "My Orders", you'll end up on a page that says "Page not found". Obviously this is because you haven't yet added anything that matches the URL `myorders`. But if you're watching really carefully, you might notice that on this occasion it's not just doing client-side (SPA-style) navigation, but instead is doing a full-page reload.
```
Error: System.InvalidOperationException: 'Router' cannot find any component with a route for '/myorders'.
```
What's really happening is this:
1. You click on the link to `myorders`
2. Blazor, running on the client, tries to match this to a client-side component based on `@page` directive attributes.
3. However, no match is found, so Blazor falls back on a full-page load navigation in case the URL is meant to be handled by server-side code.
4. However, the server doesn't have anything that matches this either, so it falls back on rendering the client-side Blazor application.
5. This time, Blazor sees that nothing matches on either client *or* server, so it falls back on rendering the `NotFoundContent` block from your `App.razor` component.
If you want to, try changing the content in the `NotFoundContent` block in `App.razor` to see how you can customize this message.
As you can guess, we will fix this by adding a component to match this route. Create a file in the `Pages` folder called `MyOrders.razor`, with the following content:
As you can guess, we will make the link actually work by adding a component to match this route. Create a file in the `Pages` folder called `MyOrders.razor`, with the following content:
```html
@page "/myorders"
......@@ -47,6 +53,8 @@ Now when you run the app, you'll be able to visit this page:
![image](https://user-images.githubusercontent.com/1101362/51804512-c855a300-2259-11e9-8770-b4b8c318ba9d.png)
Also notice that this time, no full-page load occurs when you navigate, because the URL is matched entirely within the client-side SPA. As such, navigation is instantaneous.
## Highlighting navigation position
Look closely at the top bar. Notice that when you're on "My orders", the link *isn't* highlighted in yellow. How can we highlight links when the user is on them? By using a `<NavLink>` instead of a plain `<a>` tag. The only thing a `NavLink` does is toggle its own `active` CSS class depending on whether it matches the current navigation state.
......@@ -182,7 +190,7 @@ It looks like a lot of code, but there's nothing special here. It simply uses a
## Adding an Order Details display
If you click on the "Track" link buttons next to an order, the browser will attempt a client-side navigation to `myorders/<id>` (e.g., `http://example.com/myorders/37`). Currently this will just log an error because no component matches this route.
If you click on the "Track" link buttons next to an order, the browser will attempt to navigation to `myorders/<id>` (e.g., `http://example.com/myorders/37`). Currently this will result in a "Page not found" message because no component matches this route.
Once again we'll add a component to handle this. In the `Pages` directory, create a file called `OrderDetails.razor`, containing:
......@@ -206,17 +214,11 @@ If you're wondering how routing actually works, let's go through it step-by-step
1. When the app first starts up, code in `Startup.cs` tells the framework to render `App` as the root component.
2. The `App` component (in `App.razor`) contains a `<Router>`. `Router` is a built-in component that interacts with the browser's client-side navigation APIs. It registers a navigation event handler that gets notification whenever the user clicks on a link.
3. Whenever the user clicks a link, code in `Router` checks whether the destination URL is within the same SPA (i.e., whether it's under the `<base href>` value). If it's not, traditional full-page navigation occurs as usual. But if the URL is within the SPA, `Router` will handle it.
3. Whenever the user clicks a link, code in `Router` checks whether the destination URL is within the same SPA (i.e., whether it's under the `<base href>` value, and it matches some component's declared routes). If it's not, traditional full-page navigation occurs as usual. But if the URL is within the SPA, `Router` will handle it.
4. `Router` handles it by looking for a component with a compatible `@page` URL pattern. Each `{parameter}` token needs to have a value, and the value has to be compatible with any constraints such as `:int`.
* If there is a matching component, that's what the `Router` will render. This is how all the pages in your application have been rendering all along.
* If there's no matching component, and the router has a *fallback component* then the fallback component will be shown.
* If there's no matching component and no fallback component, then it's an error.
We won't do it here, but you can specify a fallback component as a parameter to the `<Router>` to show a friendly error page for a URL that the application doesn't understand.
```html
<Router AppAssembly="typeof(Program).Assembly" FallbackComponent="typeof(MyFallbackComponent)" />
```
* If there's no matching component, the router tries a full-page load in case it matches something on the server.
* If the server chooses to re-render the client-side Blazor app (which is also what happens if a visitor is initially arriving at this URL and the server thinks it may be a client-side route), then Blazor concludes the nothing matches on either server or client, so it displays whatever `NotFoundContent` is configured.
## Polling for order details
......@@ -225,7 +227,7 @@ The `OrderDetails` logic will be quite different from `MyOrders`. Instead of sim
What's more, we'll also account for the possibility of `OrderId` being invalid. This might happen if:
* No such order exists
* Or later, when we've implement authentication, if the order is for a different user and you're not allowed to see it
* Or later, when we've implemented authentication, if the order is for a different user and you're not allowed to see it
Before we can implement the polling, we'll need to add the following directives at the top of `OrderDetails.razor`, typically directly under the `@page` directive:
......@@ -330,39 +332,53 @@ This accounts for the three main states of the component:
![image](https://user-images.githubusercontent.com/1101362/51805193-5c2b6d00-2262-11e9-98a6-c5a8ec4bb54f.png)
The last bit of UI we want to add is the actual contents of the order. Update the `<div class="track-order-body">` and add more content inside to iterate over the pizzas in the order and their toppings, rendering it all:
The last bit of UI we want to add is the actual contents of the order. To do this, we'll create another reusable component.
Create a new file, `OrderReview.razor` inside the `Shared` directory, and have it receive an `Order` and render its contents as follows:
```html
<div class="track-order-body">
<div class="track-order-details">
@foreach (var pizza in orderWithStatus.Order.Pizzas)
@foreach (var pizza in Order.Pizzas)
{
<p>
<strong>
@(pizza.Size)"
@pizza.Special.Name
(£@pizza.GetFormattedTotalPrice())
</strong>
</p>
<ul>
@foreach (var topping in pizza.Toppings)
{
<p>
<strong>
@(pizza.Size)"
@pizza.Special.Name
(£@pizza.GetFormattedTotalPrice())
</strong>
</p>
<ul>
@foreach (var topping in pizza.Toppings)
{
<li>+ @topping.Topping.Name</li>
}
</ul>
<li>+ @topping.Topping.Name</li>
}
</ul>
}
<p>
<strong>
Total price:
£@orderWithStatus.Order.GetFormattedTotalPrice()
</strong>
</p>
<p>
<strong>
Total price:
£@Order.GetFormattedTotalPrice()
</strong>
</p>
@functions {
[Parameter] Order Order { get; set; }
}
```
Finally, back in `OrderDetails.razor`, replace text `TODO: show more details` with your new `OrderReview` component:
```html
<div class="track-order-body">
<div class="track-order-details">
<OrderReview Order="@orderWithStatus.Order" />
</div>
</div>
```
(Don't forget to add the extra `div` with CSS class `track-order-details`, as this is necessary for correct styling.)
Finally, you have a functional order details display!
![image](https://user-images.githubusercontent.com/1101362/51805236-ea9fee80-2262-11e9-814b-8f92f5dbe0de.png)
......
......@@ -97,11 +97,24 @@ public void RemoveConfiguredPizza(Pizza pizza)
}
```
## Exploring state changes
Remember to remove the corresponding methods from `Index.razor`. You must also remember to remove the `order`, `configuringPizza`, and `showingConfigureDialog` fields entirely from `Index.razor`, since you'll be getting the state data from the injected `OrderState`.
At this point it should be possible to get the `Index` component compiling again by updating references to refer to various bits attached to `OrderState`. For example, the remaining `PlaceOrder` method in `Index.razor` may look something like this:
```cs
async Task PlaceOrder()
{
var newOrderId = await HttpClient.PostJsonAsync<int>("orders", OrderState.Order);
OrderState.ResetOrder();
UriHelper.NavigateTo($"myorders/{newOrderId}");
}
```
At this point it should be possible to get the `Index` component compiling again by updating references to refer to various bits attached to `OrderState`. Feel free to create convenience properties for things like `OrderState.Order` or `OrderState.Order.Pizzas` if it feels better to you that way.
Feel free to create convenience properties for things like `OrderState.Order` or `OrderState.Order.Pizzas` if it feels better to you that way.
Try this out and verify that everything still works.
Try this out and verify that everything still works. In particular, verify that you've fixed the original bug: you can now add some pizzas, navigate to "My orders", navigate back, and your order has no longer been lost.
## Exploring state changes
This is a good opportunity to explore how state changes and rendering work in Blazor, and how `EventCallback` solves some common problems. The detail of what are happening now became more complicated now that `OrderState` involved.
......@@ -111,7 +124,7 @@ This is a good opportunity to explore how state changes and rendering work in Bl
## Conclusion
So let's sum up what the *AppState pattern* provides:
- Moves shared state outside of components into OrderState
- Moves shared state outside of components into `OrderState`
- Components call methods to trigger a state change
- `EventCallback` takes care of dispatching change notifications
......@@ -120,4 +133,4 @@ We've covered a lot of information as well about rendering and eventing:
- Dispatching of events depends on the event handler delegate target
- Use `EventCallback` to have the most flexible and friendly behavior for dispatching events
Next up - [Authentication and authorization](05-authentication-and-authorization.md)
Next up - [Checkout without validation](05-checkout-with-validation.md)
# Checkout with Validation
If you take a look at the `Order` class in `BlazingPizza.Shared`, you might notice that it holds a `DeliveryAddress` property of type `Address`. However, nothing in the pizza ordering flow yet populates this data, so all your orders just have a blank delivery address.
It's time to fix this by adding a "checkout" screen that requires customers to enter a valid address.
## Inserting checkout into the flow
Start by adding a new page component, `Checkout.razor`, with a `@page` directive matching the URL `/checkout`. For the initial markup, let's display the details of the order using your `OrderReview` component:
```html
<div class="main">
<div class="checkout-cols">
<div class="checkout-order-details">
<h4>Review order</h4>
<OrderReview Order="@OrderState.Order" />
</div>
</div>
<button class="checkout-button btn btn-warning" @onclick="@PlaceOrder">
Place order
</button>
</div>
```
To implement `PlaceOrder`, copy the method with that name from `Index.razor` into `Checkout.razor`:
```cs
@functions {
async Task PlaceOrder()
{
var newOrderId = await HttpClient.PostJsonAsync<int>("orders", OrderState.Order);
OrderState.ResetOrder();
UriHelper.NavigateTo($"myorders/{newOrderId}");
}
}
```
As usual, you'll need to `@inject` values for `OrderState`, `HttpClient`, and `UriHelper` so that it can compile, just like you did in `Index.razor`.
Next, let's bring customers here when they try to submit orders. Back in `Index.razor`, make sure you've deleted the `PlaceOrder` method, and then change the order submission button into a regular HTML link to the `/checkout` URL, i.e.:
```html
<a href="checkout" class="btn btn-warning" disabled="@(Order.Pizzas.Count == 0)">
Order >
</a>
```
Now, when you run the app, you should be able to reach the checkout page by clicking the *Order* button, and from there can click *Place order* to confirm it.
![image](https://user-images.githubusercontent.com/1101362/59218134-674ebc00-8bb7-11e9-97d6-0c9985f10acf.png)
## Capturing the delivery address
We've now got a good place to put some UI for entering a delivery address. As usual, let's factor this out into a reusable component. You never know when you're going to be asking for addresses in other places.
Create a new component in the `BlazingPizza.Client` project's `Shared` folder called `AddressEditor.razor`. It's going to be a general way to edit `Address` instances, so have it receive a parameter of this type:
```cs
@functions {
[Parameter] Address Address { get; set; }
}
```
The markup here is going to be a bit tedious, so you probably want to copy and paste this. We'll need input elements for each of the properties on an `Address`:
```html
<div class="form-field">
<label>Name:</label>
<div>
<input @bind="@Address.Name" />
</div>
</div>
<div class="form-field">
<label>Line 1:</label>
<div>
<input @bind="@Address.Line1" />
</div>
</div>
<div class="form-field">
<label>Line 2:</label>
<div>
<input @bind="@Address.Line2" />
</div>
</div>
<div class="form-field">
<label>City:</label>
<div>
<input @bind="@Address.City" />
</div>
</div>
<div class="form-field">
<label>Region:</label>
<div>
<input @bind="@Address.Region" />
</div>
</div>
<div class="form-field">
<label>Postal code:</label>
<div>
<input @bind="@Address.PostalCode" />
</div>
</div>
@functions {
[Parameter] Address Address { get; set; }
}
```
Finally, you can actually use your `AddressEditor` inside the `Checkout.razor` component:
```html
<div class="checkout-cols">
<div class="checkout-order-details">
... leave this div unchanged ...
</div>
<div class="checkout-delivery-address">
<h4>Deliver to...</h4>
<AddressEditor Address="@OrderState.Order.DeliveryAddress" />
</div>
</div>
```
Your checkout screen now asks for a delivery address:
![image](https://user-images.githubusercontent.com/1101362/59219467-76833900-8bba-11e9-960b-67aec2e2f8c7.png)
If you submit an order now, any address data that you entered will actually be saved in the database with the order, because it's all part of the `Order` object that gets serialized and sent to the server.
If you're really keen to verify the data gets saved, consider downloading a tool such as [DB Browser for SQLite](https://sqlitebrowser.org/) to inspect the contents of your `pizza.db` file. But you don't strictly need to do this.
## Adding server-side validation
As yet, customers can still leave the "delivery address" fields blank and merrily order a pizza to be delivered nowhere in particular. When it comes to validation, it's normal to implement rules both on the server and on the client:
* Client-side validation is a courtesy to your users. It can provide instant feedback while they are editing a form. However, it can easily be bypassed by anyone with a basic knowledge of the browser dev tools.
* Server-side validation is where the real enforcement is.
As such it's usually best to start by implementing server-side validation, so you know your app is robust no matter what happens client-side. If you go and look at `OrdersController.cs` in the `BlazingPizza.Server` project, you'll see that this API endpoint is decorated with the `[ApiController]` attribute:
```cs
[Route("orders")]
[ApiController]
public class OrdersController : Controller
{
// ...
}
```
`[ApiController]` adds various server-side conventions, including enforcement of `DataAnnotations` validation rules. So all we need to do is put some `DataAnnotations` validation rules onto the model classes.
Open `Address.cs` from the `BlazingPizza.Server` project, and put a `[Required]` attribute onto each of the properties except for `Id` (which is autogenerated, because it's the primary key) and `Line2`, since not all addresses need a second line. You can also place some `[MaxLength]` attributes if you wish, or any other `DataAnnotations` rules:
```cs
using System.ComponentModel.DataAnnotations;
namespace BlazingPizza
{
public class Address
{
public int Id { get; set; }
[Required, MaxLength(100)]
public string Name { get; set; }
[Required, MaxLength(100)]
public string Line1 { get; set; }
[MaxLength(100)]
public string Line2 { get; set; }
[Required, MaxLength(50)]
public string City { get; set; }
[Required, MaxLength(20)]
public string Region { get; set; }
[Required, MaxLength(20)]
public string PostalCode { get; set; }
}
}
```
Now, recompile and run your application, and you should be able to observe the validation rules being enforced on the server. If you try to submit an order with a blank delivery address, then the server will reject the request and you'll see an HTTP 400 ("Bad Request") error in the browser's *Network* tab:
![image](https://user-images.githubusercontent.com/1101362/59220316-93207080-8bbc-11e9-8bbb-f3f5ec8a29d6.png)
... whereas if you fill out the address fields fully, the server will allow you to place the order. Check that both of these cases behave as expected.
## Adding client-side validation
Blazor has a comprehensive system for data entry forms and validation. We'll now use this to apply the same `DataAnnotations` rules on the client that are already being enforced on the server.
The way Blazor's forms and validation system works is based around something called an `EditContext`. An `EditContext` tracks the state of an editing process, so it knows which fields have been modified, what data has been entered, and whether or not the fields are valid. Various built-in UI components hook into the `EditContext` both to read its state (e.g., display validation messages) and to write to its state (e.g., to populate it with the data entered by the user).
### Using EditForm
One of the most important built-in UI components for data entry is the `EditForm`. This renders as an HTML `<form>` tag, but also sets up an `EditContext` to track what's going on inside the form. To use this, go to your `Checkout.razor` component, and wrap an `EditContext` around the whole of the contents of the `main` div:
```html
<div class="main">
<EditForm Model="@OrderState.Order.DeliveryAddress">
<div class="checkout-cols">
... leave unchanged ...
</div>
<button class="checkout-button btn btn-warning" @onclick="@PlaceOrder">
Place order
</button>
</EditForm>
</div>
```
You can have multiple `EditForm` components at once, but they can't overlap (because HTML's `<form>` elements can't overlap). By specifying a `Model`, we're telling the internal `EditContext` which object it should validate when the form is submitted (in this case, the delivery address).
Let's start by displaying validation messages in a very basic (and not very attractive) way. Inside the `EditForm`, right at the bottom, add the following two components:
```html
<DataAnnotationsValidator />
<ValidationSummary />
```
The `DataAnnotationsValidator` hooks into events on the `EditContext` and executes `DataAnnotations` rules. If you wanted to use a different validation system other than `DataAnnotations`, you'd swap `DataAnnotationsValidator` for something else.
The `ValidationSummary` simply renders an HTML `<ul>` containing any validation messages from the `EditContext`.
### Handling submission
If you ran your application now, you could still submit a blank form (and the server would still respond with an HTTP 400 error). That's because your `<button>` isn't actually a `submit` button. Modify the `button` by adding `type="submit"` and **removing** its `@onclick` attribute entirely.
Next, instead of triggering `PlaceOrder` directly from the button, you need to trigger it from the `EditForm`. Add the following `OnValidSubmit` attribute onto the `EditForm`:
```html
<EditForm Model="@OrderState.Order" OnValidSubmit="@PlaceOrder">
```
As you can probably guess, the `<button>` no longer triggers `PlaceOrder` directly. Instead, the button just asks the form to be submitted. And then the form decides whether or not it's valid, and if it is, *then* it will call `PlaceOrder`.
Try it out: you should no longer be able to submit an invalid form, and you'll see validation messages (albeit unattractive ones).
![image](https://user-images.githubusercontent.com/1101362/59221577-85201f00-8bbf-11e9-9de6-f24f93a6a483.png)
### Using ValidationMessage
Obviously it's pretty disgusting to display all the validation messages so far away from the textboxes. Let's move them to better places.
Start by removing the `<ValidationSummary>` component entirely. Then, switch over to `AddressEditor.razor`, and add separate `<ValidationMessage>` components next to each of the form fields. For example,
```html
<div class="form-field">
<label>Name:</label>
<div>
<input @bind="@Address.Name" />
<ValidationMessage For="@(() => Address.Name)" />
</div>
</div>
```
Do the equivalent for all of the form fields.
In case you're wondering, the syntax `@(() => Address.Name)` is a *lambda expression*, and we use this syntax as a way of describing which property to read the metadata from, without actually evaluating the property's value.
Now things look a lot better:
![image](https://user-images.githubusercontent.com/1101362/59221927-4b034d00-8bc0-11e9-96ef-6c41ad727cb7.png)
If you want, you can improve the readability of the messages by specifying custom ones. For example, instead of displaying *The City field is required*, you could go to `Address.cs` and do this:
```cs
[Required(ErrorMessage = "How do you expect to receive the pizza if we don't even know what city you're in?"), MaxLength(50)]
public string City { get; set; }
```
### Better validation UX using the built-in input components