Unverified Commit 62df399e authored by Daniel Roth's avatar Daniel Roth Committed by GitHub
Browse files

Update to Blazor WebAssembly 3.2 Preview 2 (#220)

parent c2672531
......@@ -334,3 +334,6 @@ ASALocalRun/
# Don't ignore server launchSettings.json. We need a specific port number for auth to work.
!**/BlazingPizza.Server/Properties/launchSettings.json
# Visual Studio Code folder
.vscode/
......@@ -10,8 +10,6 @@ Blazor is a single-page app framework for building client-side web apps using .N
Go ahead and clone this repo to your machine, then dive in and [get started](/docs/00-get-started.md)!
Until the first full release, Blazor will always require the newest **preview** release of .NET Core, and the newest **preview** release of Visual Studio or VS Code and the C# extension.
## Sessions
| Session | Topics |
......
......@@ -8,7 +8,8 @@ We've set up the initial solution for you for the pizza store app in this repo.
The solution already contains four projects:
![image](https://user-images.githubusercontent.com/1874516/57006654-3e3e1300-6b97-11e9-8053-b6ec9c31614d.png)
![image](https://user-images.githubusercontent.com/1874516/77238114-e2072780-6b8a-11ea-8e44-de6d7910183e.png)
- **BlazingPizza.Client**: This is the Blazor project. It contains the UI components for the app.
- **BlazingPizza.Server**: This is the ASP.NET Core project hosting the Blazor app and also the backend services for the app.
......@@ -19,7 +20,7 @@ The **BlazingPizza.Server** project should be set as the startup project.
When you run the app, you'll see that it currently only contains a simple home page.
![image](https://user-images.githubusercontent.com/1874516/51783774-afcb7880-20f3-11e9-9c22-2f330380ff1e.png)
![image](https://user-images.githubusercontent.com/1874516/77238160-25fa2c80-6b8b-11ea-8145-e163a9f743fe.png)
Open *Pages/Index.razor* in the **BlazingPizza.Client** project to see the code for the home page.
......@@ -29,7 +30,7 @@ Open *Pages/Index.razor* in the **BlazingPizza.Client** project to see the code
<h1>Blazing Pizzas</h1>
```
The home page is implemented as a single component. The `@page` directive specifies that the Index component is a routable page with the specified route.
The home page is implemented as a single component. The `@page` directive specifies that the `Index` component is a routable page with the specified route.
## Display the list of pizza specials
......@@ -93,7 +94,8 @@ Once the component is initialized it will render its markup. Replace the markup
Run the app by hitting `Ctrl-F5`. Now you should see a list of the specials available.
![image](https://user-images.githubusercontent.com/1874516/57006743-1602e400-6b98-11e9-96cb-ff4829cf459f.png)
![image](https://user-images.githubusercontent.com/1874516/77239386-6c558880-6b97-11ea-9a14-83933146ba68.png)
## Create the layout
......@@ -138,7 +140,7 @@ The `NavLink` component is the same as an anchor tag, except that it adds an `ac
Run the app by hitting `Ctrl-F5`. With our new layout our pizza store app now looks like this:
![image](https://user-images.githubusercontent.com/1874516/57006730-e81d9f80-6b97-11e9-813d-9c35b62efa53.png)
![image](https://user-images.githubusercontent.com/1874516/77239419-aa52ac80-6b97-11ea-84ae-f880db776f5c.png)
Next up - [Customize a pizza](02-customize-a-pizza.md)
......@@ -23,7 +23,8 @@ 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/77239615-f56dbf00-6b99-11ea-8535-ddcc8bc0d8ae.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.
......@@ -108,7 +109,8 @@ Update *Pages/Index.razor* to show the `ConfigurePizzaDialog` when a pizza speci
Run the app and select a pizza special to see the skeleton of the `ConfigurePizzaDialog`.
![initial-pizza-dialog](https://user-images.githubusercontent.com/1874516/51804297-e8d02e00-2256-11e9-85a6-da0becf7130d.png)
![initial-pizza-dialog](https://user-images.githubusercontent.com/1874516/77239685-e3d8e700-6b9a-11ea-8adf-5ee8a69f08ae.png)
Unfortunately at this point there's no functionality in place to close the dialog. We'll add that shortly. Let's get to work on the dialog itself.
......@@ -149,7 +151,7 @@ If you wanted to implement two-way binding manually, you could do so by combinin
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 update event only fires after the slider is released.
......@@ -159,7 +161,7 @@ But if we use `@bind` with no further changes, the behavior isn't exactly what w
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="@Pizza.Size" @bind:event="oninput" />
<input type="range" min="@Pizza.MinimumSize" max="@Pizza.MaximumSize" step="1" @bind="Pizza.Size" @bind:event="oninput" />
```
The pizza size should now update as you move the slider.
......@@ -206,7 +208,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++)
{
......@@ -255,7 +257,7 @@ void RemoveTopping(Topping topping)
You should now be able to add and remove toppings.
![Add and remove toppings](https://user-images.githubusercontent.com/1874516/51805012-f50cb900-225f-11e9-8642-4e6d34a48c3f.png)
![Add and remove toppings](https://user-images.githubusercontent.com/1874516/77239789-c0626c00-6b9b-11ea-9030-0bcccdee6da7.png)
## Component events
......@@ -273,11 +275,11 @@ Add `@onclick` event handlers to the `ConfigurePizzaDialog` that trigger the `On
```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>
```
......@@ -312,7 +314,7 @@ bool showingConfigureDialog;
Order order = new Order();
```
In the `Index` component add an event handler for the `OnConfirm`event that adds the configured pizza to the order and wire it up to the `ConfigurePizzaDialog`.
In the `Index` component add an event handler for the `OnConfirm` event that adds the configured pizza to the order and wire it up to the `ConfigurePizzaDialog`.
```html
<ConfigurePizzaDialog
......@@ -341,7 +343,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)
......@@ -383,7 +385,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>
......@@ -407,7 +409,8 @@ async Task PlaceOrder()
You should now be able to add and remove configured pizzas from the order and submit the order.
![Order list pane](https://user-images.githubusercontent.com/1874516/51805192-59c91300-2262-11e9-9b6f-d8f2d606feda.png)
![Order list pane](https://user-images.githubusercontent.com/1874516/77239878-b55c0b80-6b9c-11ea-905f-0b2558ede63d.png)
Even though the order was successfully added to the database, there's nothing in the UI yet that indicates this happened. That's what we'll address in the next session.
......
......@@ -21,7 +21,8 @@ Open `Shared/MainLayout.razor`. As an experiment, let's try adding a new link el
If you run the app now, you'll see the link, styled as expected:
![image](https://user-images.githubusercontent.com/1101362/51804403-60528d00-2258-11e9-8d2b-ab00d33c74cb.png)
![My orders link](https://user-images.githubusercontent.com/1874516/77241321-a03ba880-6bad-11ea-9a46-c73be397cb5e.png)
This shows it's not strictly necessary to use `<NavLink>`. We'll see the reason to use it momentarily.
......@@ -51,7 +52,7 @@ As you can guess, we will make the link actually work by adding a component to m
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)
![My orders blank page](https://user-images.githubusercontent.com/1874516/77241343-fc9ec800-6bad-11ea-8176-febf614ed4ad.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.
......@@ -70,7 +71,7 @@ Replace the `<a>` tag you just added in `MainLayout` with the following (which i
Now you'll see the links are correctly highlighted according to navigation state:
![image](https://user-images.githubusercontent.com/1101362/51804583-ca6c3180-225a-11e9-86cb-58a5a469e3f7.png)
![My orders nav link](https://user-images.githubusercontent.com/1874516/77241358-412a6380-6bae-11ea-88da-424434d34393.png)
## Displaying the list of orders
......@@ -139,7 +140,7 @@ The asynchronous flow we've implemented above means the component will render tw
If you want to reset your database to see the "no orders" case, simply delete `pizza.db` from the Server project and reload the page in your browser.
![image](https://user-images.githubusercontent.com/1101362/51804723-5894e780-225c-11e9-9cef-68c15f3f4b2e.png)
![My orders empty list](https://user-images.githubusercontent.com/1874516/77241390-a4b49100-6bae-11ea-8dd4-e59afdd8f710.png)
## Rendering a grid of orders
......@@ -174,7 +175,7 @@ Replace the `<text>TODO: show orders</text>` code with the following:
It looks like a lot of code, but there's nothing special here. It simply uses a `@foreach` to iterate over the `ordersWithStatus` and outputs a `<div>` for each one. The net result is as follows:
![image](https://user-images.githubusercontent.com/1101362/51804902-300ded00-225e-11e9-85b7-6aa2ac764123.png)
![My orders grid](https://user-images.githubusercontent.com/1874516/77241415-feb55680-6bae-11ea-89ba-f8367ef6a96c.png)
## Adding an Order Details display
......@@ -196,7 +197,7 @@ Once again we'll add a component to handle this. In the `Pages` directory, creat
This code illustrates how components can receive parameters from the router by declaring them as tokens in the `@page` directive. If you want to receive a `string`, the syntax is simply `{parameterName}`, which matches a `[Parameter]` name case-insensitively. If you want to receive a numeric value, the syntax is `{parameterName:int}`, as in the example above. The `:int` is an example of a *route constraint*. Other route constraints are supported too.
![image](https://user-images.githubusercontent.com/1101362/51805000-cc84bf00-225f-11e9-824b-348561ccc2fa.png)
![Order details empty](https://user-images.githubusercontent.com/1874516/77241434-391ef380-6baf-11ea-9803-9e7e65a4ea2b.png)
If you're wondering how routing actually works, let's go through it step-by-step.
......@@ -318,7 +319,8 @@ This accounts for the three main states of the component:
2. If we haven't yet loaded the data
3. If we have got some data to show
![image](https://user-images.githubusercontent.com/1101362/51805193-5c2b6d00-2262-11e9-98a6-c5a8ec4bb54f.png)
![Order details status](https://user-images.githubusercontent.com/1874516/77241460-a7fc4c80-6baf-11ea-80c1-3286374e9e29.png)
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.
......@@ -360,7 +362,7 @@ Finally, back in `OrderDetails.razor`, replace text `TODO: show more details` wi
```html
<div class="track-order-body">
<div class="track-order-details">
<OrderReview Order="@orderWithStatus.Order" />
<OrderReview Order="orderWithStatus.Order" />
</div>
</div>
```
......@@ -369,7 +371,8 @@ Finally, back in `OrderDetails.razor`, replace text `TODO: show more details` wi
Finally, you have a functional order details display!
![image](https://user-images.githubusercontent.com/1101362/51805236-ea9fee80-2262-11e9-814b-8f92f5dbe0de.png)
![Order details](https://user-images.githubusercontent.com/1874516/77241512-2e189300-6bb0-11ea-9740-fe778e0ce622.png)
## See it update in realtime
......@@ -439,7 +442,6 @@ async Task PlaceOrder()
{
var newOrderId = await HttpClient.PostJsonAsync<int>("orders", order);
order = new Order();
NavigationManager.NavigateTo($"myorders/{newOrderId}");
}
```
......
......@@ -20,17 +20,18 @@ public static async Task Main(string[] args)
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddBaseAddressHttpClient();
builder.Services.AddScoped<OrderState>();
await builder.Build().RunAsync();
}
```
note: the reason why we choose scoped over singleton is for symmetry with a server-side-components application. Singleton usually means *for all users*, where as scoped means *for the current unit-of-work*.
> Note: the reason why we choose scoped over singleton is for symmetry with a server-side-components application. Singleton usually means *for all users*, where as scoped means *for the current unit-of-work*.
## Updating Index
Now that this type is registered in DI, we can `@inject` it into the Index page.
Now that this type is registered in DI, we can `@inject` it into the `Index` page.
```razor
@page "/"
......@@ -41,13 +42,11 @@ Now that this type is registered in DI, we can `@inject` it into the Index page.
Recall that `@inject` is a convenient shorthand to both retrieve something from DI by type, and define a property of that type.
You can test this now by running the app again. If you try to inject something that isn't found in the DI container, then it will throw an exception and the Index will fail to come up.
-------
You can test this now by running the app again. If you try to inject something that isn't found in the DI container, then it will throw an exception and the `Index` page will fail to come up.
Now, let's add properties and methods to this class that will represent and manipulate the state of an `Order` and a `Pizza`.
Move the `configuringPizza`, `showingConfigureDialog` and `order` to be properties on the `OrderState` class. I like to make them `private set` so they can only be manipulated via methods on `OrderState`.
Move the `configuringPizza`, `showingConfigureDialog` and `order` fields to be properties on the `OrderState` class. Make them `private set` so they can only be manipulated via methods on `OrderState`.
```csharp
public class OrderState
......@@ -60,7 +59,7 @@ public class OrderState
}
```
Now let's move some of the methods from the `Index` to `OrderState`. We won't move PlaceOrder into OrderState because that triggers a navigation, so instead we'll just add a ResetOrder method.
Now let's move some of the methods from the `Index` to `OrderState`. We won't move `PlaceOrder` into `OrderState` because that triggers a navigation, so instead we'll just add a `ResetOrder` method.
```csharp
public void ShowConfigurePizzaDialog(PizzaSpecial special)
......@@ -104,7 +103,7 @@ public void RemoveConfiguredPizza(Pizza pizza)
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:
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` should look like this:
```csharp
async Task PlaceOrder()
......
......@@ -8,7 +8,7 @@ It's time to fix this by adding a "checkout" screen that requires customers to e
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
```razor
<div class="main">
<div class="checkout-cols">
<div class="checkout-order-details">
......@@ -25,7 +25,7 @@ Start by adding a new page component, `Checkout.razor`, with a `@page` directive
To implement `PlaceOrder`, copy the method with that name from `Index.razor` into `Checkout.razor`:
```cs
```razor
@code {
async Task PlaceOrder()
{
......@@ -40,27 +40,17 @@ As usual, you'll need to `@inject` values for `OrderState`, `HttpClient`, and `N
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="@CheckoutBtnCssClass">
```razor
<a href="checkout" class="@(Order.Pizzas.Count == 0 ? "btn btn-warning disabled" : "btn btn-warning")">
Order >
</a>
```
Please note, we removed the `disabled` attribute, since HTML links do not support it. Instead, we are going to control the behavior through the `CheckoutBtnCssClass` property. To complete this change add the property to the `@code` block:
```cs
@code{
List<PizzaSpecial> specials;
string CheckoutBtnCssClass => OrderState.Order.Pizzas.Count == 0 ? "btn btn-warning disabled" : "btn btn-warning";
\\ Leave existing code here
}
```
Note that we removed the `disabled` attribute, since HTML links do not support it, and added appropriate styling instead.
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.
Now, when you run the app, you should be able to reach the checkout page by clicking the *Order* button, and from there you can click *Place order* to confirm it.
![image](https://user-images.githubusercontent.com/1101362/59218134-674ebc00-8bb7-11e9-97d6-0c9985f10acf.png)
![Confirm order](https://user-images.githubusercontent.com/1874516/77242251-d2530780-6bb9-11ea-8535-1c41decf3fcc.png)
## Capturing the delivery address
......@@ -68,7 +58,7 @@ We've now got a good place to put some UI for entering a delivery address. As us
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
```razor
@code {
[Parameter] public Address Address { get; set; }
}
......@@ -76,7 +66,7 @@ Create a new component in the `BlazingPizza.Client` project's `Shared` folder ca
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
```razor
<div class="form-field">
<label>Name:</label>
<div>
......@@ -126,7 +116,7 @@ The markup here is going to be a bit tedious, so you probably want to copy and p
Finally, you can actually use your `AddressEditor` inside the `Checkout.razor` component:
```html
```razor
<div class="checkout-cols">
<div class="checkout-order-details">
... leave this div unchanged ...
......@@ -134,14 +124,14 @@ Finally, you can actually use your `AddressEditor` inside the `Checkout.razor` c
<div class="checkout-delivery-address">
<h4>Deliver to...</h4>
<AddressEditor Address="@OrderState.Order.DeliveryAddress" />
<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)
![Address editor](https://user-images.githubusercontent.com/1874516/77242320-79d03a00-6bba-11ea-9e40-4bf747d4dcdc.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.
......@@ -158,7 +148,7 @@ As yet, customers can still leave the "delivery address" fields blank and merril
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
```csharp
[Route("orders")]
[ApiController]
public class OrdersController : Controller
......@@ -172,7 +162,7 @@ public class OrdersController : Controller
Open `Address.cs` from the `BlazingPizza.Shared` 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
```csharp
using System.ComponentModel.DataAnnotations;
namespace BlazingPizza
......@@ -200,12 +190,11 @@ namespace BlazingPizza
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)
![Server validation](https://user-images.githubusercontent.com/1874516/77242384-067af800-6bbb-11ea-8dd0-74f457d15afd.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.
......@@ -219,7 +208,7 @@ The way Blazor's forms and validation system works is based around something cal
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 `EditForm` around the whole of the contents of the `main` div:
```html
```razor
<div class="main">
<EditForm Model="OrderState.Order.DeliveryAddress">
<div class="checkout-cols">
......@@ -237,7 +226,7 @@ You can have multiple `EditForm` components at once, but they can't overlap (bec
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
```razor
<DataAnnotationsValidator />
<ValidationSummary />
```
......@@ -252,7 +241,7 @@ If you ran your application now, you could still submit a blank form (and the se
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
```razor
<EditForm Model="OrderState.Order.DeliveryAddress" OnValidSubmit="PlaceOrder">
```
......@@ -260,15 +249,15 @@ As you can probably guess, the `<button>` no longer triggers `PlaceOrder` direct
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)
![Validation summary](https://user-images.githubusercontent.com/1874516/77242430-9d47b480-6bbb-11ea-96ef-8865468375fb.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.
Obviously it's pretty disgusting to display all the validation messages so far away from the text boxes. 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
```razor
<div class="form-field">
<label>Name:</label>
<div>
......@@ -284,11 +273,11 @@ In case you're wondering, the syntax `@(() => Address.Name)` is a *lambda expres
Now things look a lot better:
![image](https://user-images.githubusercontent.com/1101362/59221927-4b034d00-8bc0-11e9-96ef-6c41ad727cb7.png)
![Validation messages](https://user-images.githubusercontent.com/1874516/77242484-03ccd280-6bbc-11ea-8dd1-5d723b043ee2.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
```csharp
[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; }
```
......@@ -316,24 +305,12 @@ Go back to `AddressEditor.razor` once again. Replace each of the `<input>` eleme
Do this for all the properties. The behavior is now much better! As well as having the validation messages update individually for each form field as you change focus, you'll get a neat "valid" or "invalid" highlight around each one:
![image](https://user-images.githubusercontent.com/1101362/59222864-6a9b7500-8bc2-11e9-9f90-ae1d47fc23d7.png)
![Input components](https://user-images.githubusercontent.com/1874516/77242542-ba30b780-6bbc-11ea-8018-be022d6cac0b.png)
The green/red styling is achieved by applying CSS classes, so you can change the appearance of these effects or remove them entirely if you wish.
`InputText` isn't the only built-in input component, though it is the only one we need in this case. Others include `InputCheckbox`, `InputDate`, `InputSelect`, and more.
## Bonus challenge
If you're keen and have time, can you prevent accidental double-submission of the form?
Currently, if it takes a while for the form post to reach the server, the user could click submit multiple times and send multiple copies of their order. Try declaring a `bool isSubmitting` property that, when `true`, results in the *Place order* button being disabled. Remember to set it back to `false` when the submission is completed (successfully or not), otherwise the user might get stuck.
To check your solution works, you might want to slow down the server by adding the following line at the top of `PlaceOrder()` inside `OrdersController.cs`:
```cs
await Task.Delay(5000); // Wait 5 seconds
```
## Up next
Up next we'll add [authentication and authorization](https://github.com/dotnet-presentations/blazor-workshop/blob/master/docs/06-authentication-and-authorization.md)
......@@ -23,15 +23,15 @@ public class OrdersController : Controller
The `AuthorizeAttribute` class is located in the `Microsoft.AspNetCore.Authorization` namespace.
If you try to run your application now, you'll find that you can no longer place orders, nor can you retrieve details of orders already placed. Requests to these endpoints will return HTTP 302 redirections to a login URL that doesn't exist. That's good, because it shows that rules are being enforced on the server!
If you try to run your application now, you'll find that you can no longer place orders, nor can you retrieve details of orders already placed. Requests to these endpoints will return HTTP 302 redirects to a login URL that doesn't exist. That's good, because it shows that rules are being enforced on the server!
![image](https://user-images.githubusercontent.com/1101362/51806888-77ed3e00-2277-11e9-80c7-ffe7b9b2268c.png)
![Secure orders](https://user-images.githubusercontent.com/1874516/77242788-a9ce0c00-6bbf-11ea-98e6-c92e8f7c5cfe.png)
## Tracking authentication state
The client code needs a way to track whether the user is logged in, and if so *which* user is logged in, so it can influence how the UI behaves. Blazor has a built-in DI service for doing this: the `AuthenticationStateProvider`.
Server-side Blazor comes with a built-in `AuthenticationStateProvider` that hooks into server-side authentication features to determine who's logged in. But your application runs on the client, so you'll need to implement your own `AuthenticationStateProvider` that gets the login state somehow.
Blazor Server comes with a built-in `AuthenticationStateProvider` that hooks into server-side authentication features to determine who's logged in. But Blazor pizza is a Blazor WebAssembly app that runs on the client, so you'll need to implement your own `AuthenticationStateProvider` that gets the login state somehow.
To start, create a new class named `ServerAuthenticationStateProvider` in the root of your `BlazingPizza.Client` project:
......@@ -65,9 +65,11 @@ public static async Task Main(string[] args)
builder.RootComponents.Add<App>("app");
builder.Services.AddBaseAddressHttpClient();
builder.Services.AddScoped<OrderState>();
// Add auth services
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
......@@ -129,7 +131,7 @@ Let's put the `LoginDisplay` in the UI somewhere. Open `MainLayout`, and update
Because you're supplying fake login information, the user will appear to be signed in as "Fake user", and clicking the "sign out" link will not change that:
![image](https://user-images.githubusercontent.com/1101362/59272849-cb708f00-8c4e-11e9-9201-d350fb7ec9f9.png)
![Fake user](https://user-images.githubusercontent.com/1874516/77243292-a6d61a00-6bc5-11ea-8250-d841988b6dda.png)
Note that you still can't retrieve any order information. The server won't be fooled by the fake login information.
......@@ -187,7 +189,7 @@ When you click "sign in", you should actually be able to sign in with Twitter an
> Tip: If after logging in, the flow doesn't complete, it probably means your application is running on the wrong port. Change the port to port `64589` or `64590` by editing `BlazingPizza.Server/Properties/launchSettings.json`, and try again.
![image](https://user-images.githubusercontent.com/1101362/51807619-f4d0e580-2280-11e9-9891-2a9cd7b2a49b.png)
![Signed in](https://user-images.githubusercontent.com/1874516/77243353-5d39ff00-6bc6-11ea-8962-8ed862c50c4b.png)
For the OAuth flow to succeed in this example, you *must* be running on `http(s)://localhost:64589` or `http(s)://localhost:64590`, and not any other port. That's because the Twitter application ID in `appsettings.Development.json` references an application configured with those values. To deploy a real application, you'll need to use the [Twitter Developer Console](https://developer.twitter.com/apps) to register a new application, get your own client ID and secret, and register your own callback URLs.
......@@ -203,7 +205,7 @@ In the `Checkout` page component, add an `OnInitializedAsync` with some logic to
```cs
@code {
[CascadingParameter] Task<AuthenticationState> AuthenticationStateTask { get; set; }
[CascadingParameter] public Task<AuthenticationState> AuthenticationStateTask { get; set; }
protected override async Task OnInitializedAsync()
{
......@@ -344,7 +346,7 @@ Finally, let's be a bit friendlier to logged out users. Instead of just saying *
</div>
</NotAuthorized>
<Authorizing>
Please wait...
<div class="main">Please wait...</div>
</Authorizing>
</AuthorizeRouteView>
```
......
......@@ -79,18 +79,18 @@ It would be a shame if users accidentally deleted pizzas from their order (and e
Add a static `JSRuntimeExtensions` class to the Client project with a `Confirm` extension method off of `IJSRuntime`. Implement the `Confirm` method to call the built-in JavaScript `confirm` function.
```csharp
public static class JSRuntimeExtensions
public static class JSRuntimeExtensions
{
public static ValueTask<bool> Confirm(this IJSRuntime jsRuntime, string message)
{
public static ValueTask<bool> Confirm(this IJSRuntime jsRuntime, string message)
{
return jsRuntime.InvokeAsync<bool>("confirm", message);
}
return jsRuntime.InvokeAsync<bool>("confirm", message);
}
}
```
Inject the `IJSRuntime` service into the `Index` component so that it can be used there to make JavaScript interop calls.
```
```razor
@page "/"
@inject HttpClient HttpClient
@inject OrderState OrderState
......@@ -121,7 +121,7 @@ In the `Index` component update the event handler for the `ConfiguredPizzaItems`
Run the app and try removing a pizza from the order.
![Confirm pizza removal](https://user-images.githubusercontent.com/1874516/51843485-06f76600-230b-11e9-91e6-517f6d78f13c.png)
![Confirm pizza removal](https://user-images.githubusercontent.com/1874516/77243688-34b40400-6bca-11ea-9d1c-331fecc8e307.png)
Notice that we didn't have to update the signature of `ConfiguredPizzaItem.OnRemoved` to support async. This is another special property of `EventCallback`, it supports both synchronous event handlers and asynchronous event handlers.
......
......@@ -35,13 +35,13 @@ It looks like:
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>netstandard2.0</TargetFramework>
<RazorLangVersion>3.0</RazorLangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components" Version="3.1.0" />