Unverified Commit 078f5631 authored by Ricardo Santos's avatar Ricardo Santos Committed by GitHub
Browse files

Merge pull request #1 from dotnet-presentations/master

Sync fork
parents 06da08a4 6e347d50
<Project>
<PropertyGroup>
<AspNetCoreVersion>3.1.3</AspNetCoreVersion>
<BlazorVersion>3.2.0-preview3.20168.3</BlazorVersion>
<BlazorVersion>3.2.0-rc1.20223.4</BlazorVersion>
<EntityFrameworkVersion>3.1.3</EntityFrameworkVersion>
<SystemNetHttpJsonVersion>3.2.0-preview3.20175.8</SystemNetHttpJsonVersion>
<SystemNetHttpJsonVersion>3.2.0-rc1.20217.1</SystemNetHttpJsonVersion>
</PropertyGroup>
</Project>
......@@ -55,7 +55,7 @@ To get the available list of specials we need to call an API on the backend. Bla
@inject HttpClient HttpClient
```
The `@inject` directive essentially defines a new property on the component where the first token specified the property type and the second token specifies the property name. The property is populated for you using dependency injection.
The `@inject` directive essentially defines a new property on the component where the first token specifies the property type and the second token specifies the property name. The property is populated for you using dependency injection.
Override the `OnInitializedAsync` method in the `@code` block to retrieve the list of pizza specials. This method is part of the component lifecycle and is called when the component is initialized. Use the `GetFromJsonAsync<T>()` method to handle deserializing the response JSON:
......@@ -94,14 +94,14 @@ Once the component is initialized it will render its markup. Replace the markup
</div>
```
Run the app by hitting `Ctrl-F5`. Now you should see a list of the specials available.
Run the app by hitting `Ctrl-F5`. Now you should see a list of the specials that are available.
![image](https://user-images.githubusercontent.com/1874516/77239386-6c558880-6b97-11ea-9a14-83933146ba68.png)
## Create the layout
Next we'll set up the layout for app.
Next we'll set up the layout for the app.
Layouts in Blazor are also components. They inherit from `LayoutComponentBase`, which defines a `Body` property that can be used to specify where the body of the layout should be rendered. The layout component for our pizza store app is defined in *Shared/MainLayout.razor*.
......@@ -113,9 +113,9 @@ Layouts in Blazor are also components. They inherit from `LayoutComponentBase`,
</div>
```
To see how the layout is associated with your pages, look at the `<Router>` component in `App.razor`. Notice the `DefaultLayout` parameter which determines the layout used for any page that doesn't specify its own layout directly.
To see how the layout is associated with your pages, look at the `<Router>` component in `App.razor`. Notice that the `DefaultLayout` parameter determines the layout used for any page that doesn't specify its own layout directly.
You can also override this `DefaultLayout` on a per-page basis. To do so, you can add directive such as `@layout SomeOtherLayout` at the top of any `.razor` page component. However, you don't need to do so in this application.
You can also override this `DefaultLayout` on a per-page basis. To do so, you can add a directive such as `@layout SomeOtherLayout` at the top of any `.razor` page component. However, you will not need to do so in this application.
Update the `MainLayout` component to define a top bar with a branding logo and a nav link for the home page:
......@@ -136,11 +136,11 @@ Update the `MainLayout` component to define a top bar with a branding logo and a
</div>
```
The `NavLink` component is provided by Blazor. Components can be used from components, which is done by specifying an element with the component's type name along with attributes for any component parameters.
The `NavLink` component is provided by Blazor. Components can be used from components by specifying an element with the component's type name along with attributes for any component parameters.
The `NavLink` component is the same as an anchor tag, except that it adds an `active` class if the current URL matches the link address. `NavLinkMatch.All` means that the link should be active only when it matches the entire current URL (not just a prefix). We'll examine the `NavLink` component in more detail in a later session.
Run the app by hitting `Ctrl-F5`. With our new layout our pizza store app now looks like this:
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/77239419-aa52ac80-6b97-11ea-84ae-f880db776f5c.png)
......
......@@ -63,7 +63,7 @@ Update the `@onclick` handler to call the `ShowConfigurePizzaDialog` method inst
Now we need to implement the pizza customization dialog so we can display it when the user selects a pizza. The pizza customization dialog will be a new component that lets you specify the size of your pizza and what toppings you want, shows the price, and lets you add the pizza to your order.
Add a *ConfigurePizzaDialog.razor* file under the *Shared* directory. Since this component is not a separate page, it does not need the `@page` directive.
Add a *ConfigurePizzaDialog.razor* file under the *Shared* directory. Since this component is not a separate page, it does not need the `@page` directive.
> Note: In Visual Studio, you can right-click the *Shared* directory in Solution Explorer, then choose *Add* -> *New Item*, then use the *Razor Component* item template.
......@@ -134,7 +134,7 @@ Now the dialog shows a slider that can be used to change the pizza size. However
![Slider](https://user-images.githubusercontent.com/1430011/57576985-eff40400-7421-11e9-9a1b-b22d96c06bcb.png)
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.
We want the value of the `Pizza.Size` to 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):
......@@ -148,17 +148,17 @@ If you wanted to implement two-way binding manually, you could do so by combinin
@onchange="@((ChangeEventArgs e) => Pizza.Size = int.Parse((string) e.Value))" />
```
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:
In Blazor you can use the `@bind` directive attribute to specify a two-way binding with this same behavior. The equivalent markup using `@bind` looks like this:
```html
<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.
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 event triggers a change using the syntax `@bind:<eventname>`. 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 which 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" />
......@@ -297,9 +297,9 @@ void CancelConfigurePizzaDialog()
}
```
Now, what happens when you click the dialog cancel button is that `Index.CancelConfigurePizzaDialog` will execute, and then the `Index` component will render itself. Since `showingConfigureDialog` is now `false` the dialog will not be displayed.
Now when you click the dialog's Cancel button, `Index.CancelConfigurePizzaDialog` will execute, and then the `Index` component will rerender itself. Since `showingConfigureDialog` is now `false` the dialog will not be displayed.
Normally what happens when you trigger an event (like clicking the cancel button) is that the component that defined the event handler delegate will rerender. You could define events using any delegate type like `Action` or `Func<string, Task>`. Sometimes you want to use an event handler delegate that doesn't belong to a component - if you used a normal delegate type to define the event then nothing will be rendered or updated.
Normally what happens when you trigger an event (like clicking the Cancel button) is that the component that defined the event handler delegate will rerender. You could define events using any delegate type like `Action` or `Func<string, Task>`. Sometimes you want to use an event handler delegate that doesn't belong to a component - if you used a normal delegate type to define the event then nothing will be rendered or updated.
`EventCallback` is a special type that is known to the compiler that resolves some of these issues. It tells the compiler to dispatch the event to the component that contains the event handler logic. `EventCallback` has a few more tricks up its sleeve, but for now just remember that using `EventCallback` makes your component smart about dispatching events to the right place.
......@@ -362,7 +362,7 @@ Create a new `ConfiguredPizzaItem` component for displaying a configured pizza.
}
```
Add the following markup to the `Index` component just below the main `div` to add a right side pane for displaying the configured pizzas in the current order.
Add the following markup to the `Index` component just below the `main` div to add a right side pane for displaying the configured pizzas in the current order.
```html
<div class="sidebar">
......
# Show order status
Your customers can order pizzas, but so far have no way to see the status of their orders. In this session you'll implement a "My orders" page that lists multiple orders, plus an "Order details" view showing the contents and status of an individual order.
Your customers can order pizzas, but so far they have no way to see the status of their orders. In this session you'll implement a "My orders" page that lists multiple orders, plus an "Order details" view showing the contents and status of an individual order.
## Adding a navigation link
Open `Shared/MainLayout.razor`. As an experiment, let's try adding a new link element *without* using `NavLink`. Add a plain HTML `<a>` tag pointing to `myorders`:
Open `Shared/MainLayout.razor`. As an experiment, let's try adding a new link element *without* using a `NavLink` component. Add a plain HTML `<a>` tag pointing to `myorders`:
```html
<div class="top-bar">
......@@ -28,17 +28,17 @@ 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", 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.
If you click "My Orders", you'll end up on a page that says "Sorry, there's nothing at this address.". 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.
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.
3. However, since no match is found, Blazor falls back on 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 `NotFound` block from your `App.razor` component.
If you want to, try changing the content in the `NotFound` block in `App.razor` to see how you can customize this message.
If you want to, try changing the content in the `NotFound` block in your `App.razor` component to see how you can customize this message.
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:
......@@ -58,7 +58,7 @@ Also notice that this time, no full-page load occurs when you navigate, because
## 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.
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` component instead of a plain `<a>` tag. The only special thing a `NavLink` component does is toggle its own `active` CSS class depending on whether its `href` matches the current navigation state.
Replace the `<a>` tag you just added in `MainLayout` with the following (which is identical apart from the tag name):
......@@ -69,7 +69,7 @@ Replace the `<a>` tag you just added in `MainLayout` with the following (which i
</NavLink>
```
Now you'll see the links are correctly highlighted according to navigation state:
Now you'll see the links are correctly highlighted according to the navigation state:
![My orders nav link](https://user-images.githubusercontent.com/1874516/77241358-412a6380-6bae-11ea-88da-424434d34393.png)
......@@ -97,7 +97,7 @@ Then add a `@code` block that makes an asynchronous request for the data we need
Let's make the UI display different output in three different cases:
1. While we're waiting for the data to load
2. If it turns out that the user has never placed any orders
2. If the user has never placed any orders
3. If the user has placed one or more orders
It's simple to express this using `@if/else` blocks in Razor code. Update the markup inside your component as follows:
......@@ -136,7 +136,11 @@ If `<a href="">` (with an empty string for `href`) surprises you, remember that
The asynchronous flow we've implemented above means the component will render twice: once before the data has loaded (displaying "Loading.."), and then once afterwards (displaying one of the other two outputs).
### 4. How can I reset the database?
### 4. Why are we using OnParametersSetAsync?
Asynchronous work when applying parameters and property values must occur during the OnParametersSetAsync lifecycle event. We will be adding a parameter in a later session.
### 5. How can I reset the database?
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.
......@@ -179,7 +183,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 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.
If you click on the "Track" link buttons next to an order, the browser will attempt to navigate to `myorders/<id>` (e.g., `http://example.com/myorders/37`). Currently this will result in a "Sorry, there's nothing at this address." 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:
......@@ -201,17 +205,17 @@ This code illustrates how components can receive parameters from the router by d
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.
1. When the app first starts up, code in `Program.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, 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.
3. Whenever the user clicks on 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, 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 `NotFound` content is configured.
* 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 that nothing matches on either server or client, so it displays whatever `NotFound` content is configured.
## Polling for order details
The `OrderDetails` logic will be quite different from `MyOrders`. Instead of simply fetching the data once when the component is instantiated, we'll poll the server every few seconds for updated data. This will make it possible to show the order status in (nearly) real-time, and later, to display the delivery driver's location on a moving map.
The `OrderDetails` logic will be quite different from `MyOrders`. Instead of fetching the data just once when the component is instantiated, we'll poll the server every few seconds for updated data. This will make it possible to show the order status in (nearly) real-time, and later, to display the delivery driver's location on a moving map.
What's more, we'll also account for the possibility of `OrderId` being invalid. This might happen if:
......@@ -271,7 +275,7 @@ Now you can implement the polling. Update your `@code` block as follows:
}
```
The code is a bit intricate, so be sure to go through it carefully and be sure to understand each aspect of it. Here are some notes:
The code is a bit intricate, so be sure to go through it carefully to understand each aspect of it before proceeding. Here are some notes:
* This uses `OnParametersSet` instead of `OnInitialized` or `OnInitializedAsync`. `OnParametersSet` is another component lifecycle method, and it fires when the component is first instantiated *and* any time its parameters change value. If the user clicks a link directly from `myorders/2` to `myorders/3`, the framework will retain the `OrderDetails` instance and simply update its `OrderId` parameter in place.
* As it happens, we haven't provided any links from one "my orders" screen to another, so the scenario never occurs in this application, but it's still the right lifecycle method to use in case we change the navigation rules in the future.
......@@ -378,7 +382,7 @@ Finally, you have a functional order details display!
The backend server will update the order status to simulate an actual dispatch and delivery process. To see this in action, try placing a new order, then immediately view its details.
Initially, the order status will be *Preparing*, then after 10-15 seconds will change to *Out for delivery*, then 60 seconds later will change to *Delivered*. Because `OrderDetails` polls for updates, the UI will update without the user having to refresh the page.
Initially, the order status will be *Preparing*, then after 10-15 seconds the order status will change to *Out for delivery*, then 60 seconds later it will change to *Delivered*. Because `OrderDetails` polls for updates, the UI will update without the user having to refresh the page.
## Remember to Dispose!
......@@ -425,7 +429,7 @@ Once you've put in this fix, you can try again to start lots of concurrent polli
Right now, once users place an order, the `Index` component simply resets its state and their order appears to vanish without a trace. This is not very reassuring for users. We know the order is in the database, but users don't know that.
It would be nice if, once the order is placed, you navigated to the details display for that order automatically. This is quite easy to do.
It would be nice if, once the order is placed, the app automatically navigated to the "order details" display for that order. This is quite easy to do.
Switch back to your `Index` component code. Add the following directive at the top:
......
# Refactor state management
In this section we'll revisit some of the code we've already written and try to make it nicer. We'll also talk more about eventing and how events cause the UI to update.
In this session we'll revisit some of the code we've already written and try to make it nicer. We'll also talk more about eventing and how events cause the UI to update.
## A problem
......@@ -8,7 +8,7 @@ You might have noticed this already, but our application has a bug! Since we're
## A solution
We're going to fix this bug by introducing something we've dubbed the *AppState pattern*. The basics are that you want to add an object to the DI container that you will use to coordinate state between related components. Because the *AppState* object is managed by the DI container, it can outlive the components and hold on to state even when the UI is changing a lot. Another benefit of the *AppState pattern* is that it leads to greater separation between presentation (components) and business logic.
We're going to fix this bug by introducing something we've dubbed the *AppState pattern*. The *AppState pattern* adds an object to the DI container that you will use to coordinate state between related components. Because the *AppState* object is managed by the DI container, it can outlive the components and hold on to state even when the UI changes. Another benefit of the *AppState pattern* is that it leads to greater separation between presentation (components) and business logic.
## Getting started
......
......@@ -42,12 +42,12 @@ 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.:
```razor
<a href="checkout" class="@(Order.Pizzas.Count == 0 ? "btn btn-warning disabled" : "btn btn-warning")">
<a href="checkout" class="@(OrderState.Order.Pizzas.Count == 0 ? "btn btn-warning disabled" : "btn btn-warning")">
Order >
</a>
```
Note that we removed the `disabled` attribute, since HTML links do not support it, and added appropriate styling instead.
> 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 you can click *Place order* to confirm it.
......@@ -193,7 +193,7 @@ namespace BlazingPizza
}
```
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:
Now, after you recompile and run your application, 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:
![Server validation](https://user-images.githubusercontent.com/1874516/77242384-067af800-6bbb-11ea-8dd0-74f457d15afd.png)
......@@ -248,7 +248,7 @@ Next, instead of triggering `PlaceOrder` directly from the button, you need to t
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).
Try it out: you should no longer be able to submit an invalid form, and you'll see validation messages (albeit unattractive ones) where you placed the `ValidationSummary`.
![Validation summary](https://user-images.githubusercontent.com/1874516/77242430-9d47b480-6bbb-11ea-96ef-8865468375fb.png)
......
......@@ -51,7 +51,7 @@ public static async Task Main(string[] args)
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddBaseAddressHttpClient();
builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<OrderState>();
// Add auth services
......@@ -126,7 +126,7 @@ To flow the authentication state information through your app, you need to add o
</CascadingAuthenticationState>
```
At first this will appear to do nothing, but in fact this has made available a *cascading parameter* to all descendant components. A cascading parameter is a parameter that isn't passed down just one level in the hierarchy, but through any number of levels.
At first this will appear to do nothing, but in fact this has made a *cascading parameter* available to all descendant components. A cascading parameter is a parameter that isn't passed down just one level in the hierarchy, but through any number of levels.
Finally, you're ready to display something in the UI!
......@@ -170,7 +170,7 @@ Create a new component called `LoginDisplay` in the client project's `Shared` fo
You can use `AuthorizeView` anywhere you need UI content to vary by authorization state, such as controlling the visibility of menu entries based on a user's roles. In this case, we're using it to tell the user who they are, and conditionally show either a "log in" or "log out" link as applicable.
The links to register, log in, and see the user profile are normal links that navigate to the `Authentication` component. The sign out link is a button and has additional logic to prevent forged request logging the user out. Using a button ensures that the sign out can only be triggered by a user action, and the `SignOutSessionStateManager` service maintains state across the sign out flow to ensure the whole flow originated with a user action.
The links to register, log in, and see the user profile are normal links that navigate to the `Authentication` component. The sign out link is a button and has additional logic to prevent a forged request from logging the user out. Using a button ensures that the sign out can only be triggered by a user action, and the `SignOutSessionStateManager` service maintains state across the sign out flow to ensure the whole flow originated with a user action.
Let's put the `LoginDisplay` in the UI somewhere. Open `MainLayout`, and update the `<div class="top-bar">` as follows:
......@@ -347,7 +347,7 @@ Update the `MyOrders` and `OrderDetails` components to also make authenticated H
Although the server requires authentication before accepting queries for order information, it still doesn't distinguish between users. All signed-in users can see the orders from all other signed-in users. We have authentication, but no authorization!
To verify this, place an order while signed in with one Twitter account. Then sign out and back in using a different Twitter account. You'll still be able to see the same order details.
To verify this, place an order while signed in with one account. Then sign out and back in using a different account. You'll still be able to see the same order details.
This is easily fixed. Back in the `OrdersController` code, look for the commented-out line in `PlaceOrder`, and uncomment it:
......@@ -491,7 +491,7 @@ The My Orders tab should now only be visible when the user is logged in.
We've now seen that there are three basic ways to interact with the authentication/authorization system inside components:
* Receive a `Task<AuthenticationState>` as a You can use a cascading parameter. This is useful when you want to use the `AuthenticationState` in procedural logic such as an event handler.
* Receive a `Task<AuthenticationState>` as a cascading parameter. This is useful when you want to use the `AuthenticationState` in procedural logic such as an event handler.
* Wrap content in an `AuthorizeView`. This is useful when you just need to vary some UI content according to authorization status.
* Place an `[Authorize]` attribute on a routable component. This is useful if you want to control the reachability of an entire page based on authorization conditions.
......@@ -499,7 +499,7 @@ We've now seen that there are three basic ways to interact with the authenticati
We've just introduced a pretty serious defect into the application. Since you're building a client-side SPA, the application state (such as the current order) is held in the browser's memory. When you redirect away to log in, that state is discarded. When the user is redirected back, their order has now become empty!
Check you can reproduce this bug. Start logged out, and create an order. When you try to place the order, you get redirected to the login page. After logging in, you are then redirected to the checkout page, put your pizzas in your order have now gone missing! This is a common concern with browser-based single-page applications (SPAs), but fortunately there are straightforward solutions.
Check you can reproduce this bug. Start logged out, and create an order. When you try to place the order, you get redirected to the login page. After logging in, you are then redirected to the checkout page, but your pizzas in your order have now gone missing! This is a common concern with browser-based single-page applications (SPAs), but fortunately there is a straightforward solution.
We'll fix the bug by persisting the order state. Blazor's authentication library makes this straight forward to do.
......@@ -516,8 +516,7 @@ To configure the authentication system to use our `PizzaAuthenticationState` ins
```csharp
// Add auth services
builder.Services.AddRemoteAuthentication<RemoteAuthenticationState, ApiAuthorizationProviderOptions>();
builder.Services.AddApiAuthorization();
builder.Services.AddApiAuthorization<PizzaAuthenticationState>();
```
Now we need to add logic to persist the current order, and then reestablish the current order from the persisted state after the user has successfully logged in. To do that, update the `Authentication` component to use `RemoteAuthenticatorViewCore` instead of `RemoteAuthenticatorView`. Override `OnInitialized` to setup the order state to be persisted, and implement the `OnLogInSucceeded` callback to reestablish the order state. You'll need to add a `RepaceOrder` method to `OrderState` so that you can reestablish the saved order.
......@@ -589,11 +588,9 @@ But what if we want the user to be redirected back to the home page after they l
```csharp
// Add auth services
builder.Services.AddRemoteAuthentication<PizzaAuthenticationState, ApiAuthorizationProviderOptions>();
builder.Services.AddApiAuthorization(options =>
builder.Services.AddApiAuthorization<PizzaAuthenticationState>(options =>
{
options.AuthenticationPaths.LogOutSucceededPath = "";
options.ProviderOptions.ConfigurationEndpoint = "_configuration/BlazingPizza.Client"; // temporary workaround
});
```
......
......@@ -32,7 +32,7 @@ Open *Map.razor* and take a look at the code:
The `Map` component uses dependency injection to get an `IJSRuntime` instance. This service can be used to make JavaScript calls to browser APIs or existing JavaScript libraries by calling the `InvokeVoidAsync` or `InvokeAsync<TResult>` method. The first parameter to this method specifies the path to the JavaScript function to call relative to the root `window` object. The remaining parameters are arguments to pass to the JavaScript function. The arguments are serialized to JSON so they can be handled in JavaScript.
The `Map` component first renders a `div` with a unique ID for the map and then calls the `deliveryMap.showOrUpdate` function to display the map in the specified element with the specified markers pass to the `Map` component. This is done in the `OnAfterRenderAsync` component lifecycle event to ensure that the component is done rendering its markup. The `deliveryMap.showOrUpdate` function is defined in the *wwwroot/deliveryMap.js* file, which then uses [leaflet.js](http://leafletjs.com) and [OpenStreetMap](https://www.openstreetmap.org/) to display the map. The details of how this code works isn't really important - the critical point is that it's possible to call any JavaScript function this way.
The `Map` component first renders a `div` with a unique ID for the map and then calls the `deliveryMap.showOrUpdate` function to display the map in the specified element with the specified markers passed to the `Map` component. This is done in the `OnAfterRenderAsync` component lifecycle event to ensure that the component is done rendering its markup. The `deliveryMap.showOrUpdate` function is defined in the *wwwroot/deliveryMap.js* file, which then uses [leaflet.js](http://leafletjs.com) and [OpenStreetMap](https://www.openstreetmap.org/) to display the map. The details of how this code works isn't really important - the critical point is that it's possible to call any JavaScript function this way.
How do these files make their way to the Blazor app? For a Blazor library project (using `Sdk="Microsoft.NET.Sdk.Razor"`) any files in the `wwwroot/` folder will be bundled with the library. The server project will automatically serve these files using the static files middleware.
......
......@@ -40,8 +40,8 @@ It looks like:
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Components" Version="3.1.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="3.1.3" />
</ItemGroup>
</Project>
......@@ -49,7 +49,7 @@ It looks like:
There are a few things here worth understanding.
Firstly, the package targets `netstandard2.1`. Blazor Server uses `netcoreapp3.1` and Blazor WebAssembly uses `netstandard2.1` - so targeting `netstandard2.0` means that it will work for either scenario.
Firstly, the package targets `netstandard2.0`. Blazor Server uses `netcoreapp3.1` and Blazor WebAssembly uses `netstandard2.1` - so targeting `netstandard2.0` means that it will work for either scenario.
Additional, the `<RazorLangVersion>3.0</RazorLangVersion>` sets the Razor language version. Version 3 is needed to support components and the `.razor` file extension.
......@@ -116,7 +116,7 @@ Next, to give this dialog some conditional behavior, let's add a parameter of ty
}
```
Do build and make sure that everything compiles at this stage. Next we'll get down to using this new component.
Build the solution and make sure that everything compiles at this stage. Next we'll get down to using this new component.
## Adding a reference to the templated library
......@@ -174,9 +174,9 @@ We'll use this new templated component from `Index.razor`. Open `Index.razor` an
@if (OrderState.ShowingConfigureDialog)
{
<ConfigurePizzaDialog
Pizza="@OrderState.ConfiguringPizza"
OnConfirm="@OrderState.ConfirmConfigurePizzaDialog"
OnCancel="@OrderState.CancelConfigurePizzaDialog" />
Pizza="OrderState.ConfiguringPizza"
OnConfirm="OrderState.ConfirmConfigurePizzaDialog"
OnCancel="OrderState.CancelConfigurePizzaDialog" />
}
```
......@@ -213,7 +213,7 @@ We can solve async loading by accepting a delegate of type `Func<Task<List<?>>>`
Making a generic-typed component works similarly to other generic types in C#, in fact `@typeparam` is just a convenient Razor syntax for a generic .NET type.
note: We don't yet have support for type-parameter-constraints. This is something we're looking to add in the future.
> Note: We don't yet have support for type-parameter-constraints. This is something we're looking to add in the future.
Now that we've defined a generic type parameter we can use it in a parameter declaration. Let's add a parameter to accept a delegate we can use to load data, and then load the data in a similar fashion to our other components.
......@@ -253,7 +253,7 @@ else
}
```
Now, these are our three states of the dialog, and we'd like accept a content parameter for each one so the caller can plug in the desired content. We do this by defining three `RenderFragment` parameters. Since we have multiple we'll just give them their own descriptive names instead of calling them `ChildContent`. However, the content for showing an item needs to take a parameter. We can do this by using `RenderFragment<T>`.
Now, these are our three states of the dialog, and we'd like to accept a content parameter for each one so the caller can plug in the desired content. We do this by defining three `RenderFragment` parameters. Since we have multiple `RenderFragment` parameters we'll just give each one their own descriptive names instead of calling them `ChildContent`. The content for showing an item needs to take a parameter. We can do this by using `RenderFragment<T>`.
Here's an example of the three parameters to add:
......@@ -381,7 +381,7 @@ First, we need to create a delegate that we can pass to the `TemplatedList` that
This matches the signature expected by the `Loader` parameter of `TemplatedList`, it's a `Func<Task<List<?>>>` where the **?** is replaced with `OrderWithStatus` so we are on the right track.
If you use the `TemplatedList` component now like so:
You can use the `TemplatedList` component now like so:
```html
<div class="main">
......@@ -489,10 +489,10 @@ To prove that the list is really working correctly we can try the following:
## Summary
So what have we seen in this section?
So what have we seen in this session?
1. It's possible to write components that accept *content* as a parameter - even multiple content parameters
2. Templated components can be used to abstract things, like showing a dialog, or async loading of data
3. Components can be generic types which makes them more reusable
3. Components can be generic types, which makes them more reusable
Next up - [Progressive web app](09-progressive-web-app.md)
......@@ -10,7 +10,7 @@ Blazor uses standard web technologies, which means you can take advantage of the
## Adding a service worker
As a prerequisite to most of the PWA-type APIs, your application will need a *service worker*. This is a JavaScript file, usually quite small, that provides event handlers that the browser can invoke outside the context of your running application, for example when fetching resources from your domain, or when a push notification arrives. You can learn more about service workers in Google's [Web Fundamentals guide](https://developers.google.com/web/fundamentals/primers/service-workers).
As a prerequisite to most of the PWA-type APIs, your application will need a *service worker*. This is a JavaScript file that is usually quite small. It provides event handlers that the browser can invoke outside the context of your running application, for example when fetching resources from your domain, or when a push notification arrives. You can learn more about service workers in Google's [Web Fundamentals guide](https://developers.google.com/web/fundamentals/primers/service-workers).
Even though Blazor applications are built in .NET, your service worker will still be JavaScript because it runs outside the context of your application. Technically it would be possible to create a service worker that starts up the Mono WebAssembly runtime and then runs .NET code within the service worker context, but this is a lot of work that may be unnecessary considering that you might only need a few lines of JavaScript code.
......@@ -31,7 +31,7 @@ self.addEventListener('fetch', event => {
This service worker doesn't really do anything yet. It just installs itself, and then whenever any `fetch` event occurs (meaning that the browser is performing an HTTP request to your origin), it simply opts out of processing the request so that the browser handles it normally. If you want, you can come back to this file later and add some more advanced functionality like offline support, but we don't need that just yet.
Enable the service worker by adding the following `<script>` element into your `index.html` file, for example beneath the other `<script>` elements:
Enable the service worker by adding the following `<script>` element into your `index.html` file beneath the other `<script>` elements:
```html
<script>navigator.serviceWorker.register('service-worker.js');</script>
......@@ -43,7 +43,9 @@ If you run your app now, then in the browser's dev tools console, you should see
Installing service worker...
```
Note that this only happens during the first page load after each time you modify `service-worker.js`. It doesn't re-install on each load if that file's contents (compared byte-for-byte) haven't changed. Try it out: check that you can make some trivial change to the file (such as adding a comment or changing whitespace) and observe that it reinstalls after those changes, but not at other times.
> Note that this only happens during the first page load after each time you modify `service-worker.js`. It doesn't reinstall on each load if that file's contents (compared byte-for-byte) haven't changed.
Try it out: check that you can make some trivial change to the file (such as adding a comment or changing whitespace) and observe that the service worker reinstalls after those changes, but it does not reinstall if you do not make any changes.
This might not seem to achieve anything yet, but is a prerequisite for the following steps.
......@@ -108,11 +110,14 @@ Before you can send push notifications to a user, you have to ask them for permi
You can ask for this permission any time you want, but for the best chance of success, ask users only when it's really clear why they would want to subscribe. You might want to have a *Send me updates* button, but for simplicity we'll ask users when they get to the checkout page, since at that point it's clear the user is serious about placing an order.
In `Checkout.razor`, at the very end of `OnInitializedAsync`, add the following:
In `Checkout.razor`, add the following `OnInitialized` method:
```cs
// In the background, ask if they want to be notified about order updates
_ = RequestNotificationSubscriptionAsync();
protected override void OnInitialized()
{
// In the background, ask if they want to be notified about order updates
_ = RequestNotificationSubscriptionAsync();
}
```
You'll then need to define `RequestNotificationSubscriptionAsync`. Add this elsewhere in your `@code` block:
......@@ -139,7 +144,13 @@ async Task RequestNotificationSubscriptionAsync()
}
```
This code invokes a JavaScript function that you'll find in `BlazingPizza.ComponentsLibrary/wwwroot/pushNotifications.js`. The JavaScript code there calls the `pushManager.subscribe` API and returns the results to .NET.
You'll also need to inject the `IJSRuntime` service into the `Checkout` component.
```razor
@inject IJSRuntime JSRuntime
```
The `RequestNotificationSubscriptionAsync` code invokes a JavaScript function that you'll find in `BlazingPizza.ComponentsLibrary/wwwroot/pushNotifications.js`. The JavaScript code there calls the `pushManager.subscribe` API and returns the results to .NET.
If the user agrees to receive notifications, this code sends the data to your server where the tokens are stored in your database for later use.
......@@ -149,7 +160,7 @@ To try this out, start placing an order and go to the checkout screen. You shoul
Choose *Allow* and check in the browser dev console that it didn't cause any errors. If you want, set a breakpoint on the server in `NotificationsController`'s `Subscribe` action method, and run with debugging. You should be able to see the incoming data from the browser, which includes an endpoint URL as well as some cryptographic tokens.
Once you've either allowed or blocked notifications for a given site, your browser won't ask you again. If you need to reset things for further testing, if you're using Chrome or Edge beta, you can click the "information" icon to the left of the address bar, and change *Notifications* back to *Ask (default)* as in this screenshot:
Once you've either allowed or blocked notifications for a given site, your browser won't ask you again. If you need to reset things for further testing, and you're using either Chrome or Edge beta, you can click the "information" icon to the left of the address bar, and change *Notifications* back to *Ask (default)* as in this screenshot:
![image](https://user-images.githubusercontent.com/1101362/66354317-58f19080-e95c-11e9-8c24-dfa2d19b45f6.png)
......@@ -221,7 +232,7 @@ With this in place, once you place an order, as soon as the order moves into *Ou
![image](https://user-images.githubusercontent.com/1101362/66355395-0bc2ee00-e95f-11e9-898d-23be0a17829f.png)
If you're using Chrome or Edge beta, this will appear even if you're not still on the Blazing Pizza app, but only if your browser is running (or the next time you open the browser). If you're using the installed PWA, the notification should be delivered even if you're not running the app at all.
If you're using either Chrome or the latest Edge browser, this will appear even if you're not still on the Blazing Pizza app, but only if your browser is running (or the next time you open the browser). If you're using the installed PWA, the notification should be delivered even if you're not running the app at all.
## Handling clicks on notifications
......@@ -240,7 +251,7 @@ Now, once your service worker has updated, the next time you click on an incomin
## Summary
This chapter showed how, even though Blazor applications are written in .NET, you still have full access to benefit from modern browser/JavaScript capabilities. You can create a OS-installable app that looks and feels as native as you like, while having the always-updated benefits of a web app.
This chapter showed how, even though Blazor applications are written in .NET, you still have full access to the benefits of modern browser/JavaScript capabilities. You can create a OS-installable app that looks and feels as native as you like, while having the always-updated benefits of a web app.
If you want to go further on the PWA journey, as a more advanced challenge you could consider adding offline support. It's relatively easy to get the basics working - just see [The Offline Cookbook](https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook) for a variety of service worker samples representing different offline strategies, any of which can work with a Blazor app. However, since Blazing Pizza requires server APIs to do anything interesting like view or place orders, you would need to update your components to provide a sensible behavior when the network isn't reachable (for example, use cached data if that makes sense, or provide UI that appears if you're offline and try to do something that requires network access).
......
......@@ -59,17 +59,16 @@ You can create a signing certificate using an existing key vault, or create a ne
To create a new key vault:
- Sign in to the Azure portal at https://portal.azure.com.
- From the Azure portal menu, or from the Home page, select **Create a resource**.
- In the Search box, enter **Key Vault**.
- From the results list, choose **Key Vault**.
- On the Key Vault section, choose **Create**.
- From the results list, choose **Key vaults**.
- On the Key Vault section, choose **Add**.
- On the **Create key vault** section, provide the following information:
- **Name**: A unique name is required.
- **Subscription**: Choose your subscription.
- **Resource Group**: Choose the resource group for your key vault.
- In the **Location** pull-down menu, choose a location.
- **Resource group**: Choose the resource group for your key vault.
- **Key vault name**: A unique name is required.
- In the **Region** pull-down menu, choose a location.
- Leave the other options to their defaults.
- After providing the information above, select **Create**.
- After providing the information above, select **Review + create** to create your key vault.
Browse to your key vault in the Azure portal and select **Certificates**. Select **Generate/Import** to create a new certificate.
......@@ -95,7 +94,7 @@ Select **Configuration** in the left nav for the app service. Add the `WEBSITE_L
![Load certificates setting](https://user-images.githubusercontent.com/1874516/78463547-e8b99280-7692-11ea-9d02-394b20c653cd.png)
Now update **appsettings.json* in the server project configure the app to use the certificate in production.
Now update **appsettings.json** in the server project configure the app to use the certificate in production.
```json
"IdentityServer": {
......@@ -121,4 +120,4 @@ Publishing the app may take a few minutes. Once the app has finished deploying i
Congrats!
Once your done showing off your completed Blazor app to your friend, be sure to clean up any Azure resources that you no longer wish to maintain.
Once your done showing off your completed Blazor app to your friends, be sure to clean up any Azure resources that you no longer wish to maintain.
......@@ -17,7 +17,7 @@ This is a rough guide of what topics are best to introduce with each section.
- Show the Router component in App.razor
- Introduce `@code` - this is like the old `@functions` feature from `.cshtml`. Get users comfortable with the idea of defining properties, fields, methods, even nested classes
- Components are stateful so have a place to keep state in components is useful
- Introduce parameters - parameters should be non-public
- Introduce parameters - parameters should be public
- Introduce using a component from markup in razor - show how to pass parameters
- Introduce @inject and DI - can show how that's a shorthand for a property in @code
- Introduce http + JSON in Blazor (`GetFromJsonAsync`)
......
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace BlazingPizza.Client
......@@ -11,7 +13,7 @@ namespace BlazingPizza.Client
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddBaseAddressHttpClient();
builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
}
......