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

Merge pull request #2 from dotnet-presentations/master

Sync
parents 078f5631 57e0577c
<Project>
<PropertyGroup>
<AspNetCoreVersion>3.1.3</AspNetCoreVersion>
<BlazorVersion>3.2.0-rc1.20223.4</BlazorVersion>
<EntityFrameworkVersion>3.1.3</EntityFrameworkVersion>
<SystemNetHttpJsonVersion>3.2.0-rc1.20217.1</SystemNetHttpJsonVersion>
<AspNetCoreVersion>3.1.4</AspNetCoreVersion>
<BlazorVersion>3.2.0</BlazorVersion>
<EntityFrameworkVersion>3.1.4</EntityFrameworkVersion>
<SystemNetHttpJsonVersion>3.2.0</SystemNetHttpJsonVersion>
</PropertyGroup>
</Project>
......@@ -15,8 +15,8 @@ The solution already contains four projects:
- **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.
- **BlazingPizza.Shared**: Shared model types for the app.
- **BlazingPizza.ComponentsLibrary**: A library of components and helper code to be used by the app in later sessions.
- **BlazingPizza.Shared**: This project contains the shared model types for the app.
- **BlazingPizza.ComponentsLibrary**: This is a library of components and helper code to be used by the app in later sessions.
The **BlazingPizza.Server** project should be set as the startup project.
......@@ -46,7 +46,7 @@ Add a `@code` block to *Index.razor* with a list field to keep track of the avai
}
```
The code in the `@code` block is added to the generated class for the component. The `PizzaSpecial` type is already defined for you in the Shared project.
The code in the `@code` block is added to the generated class for the component. The `PizzaSpecial` type is already defined for you in the **BlazingPizza.Shared** project.
To get the available list of specials we need to call an API on the backend. Blazor provides a preconfigured `HttpClient` through dependency injection that is already setup with the correct base address. Use the `@inject` directive to inject an `HttpClient` into the `Index` component.
......@@ -70,7 +70,7 @@ Override the `OnInitializedAsync` method in the `@code` block to retrieve the li
}
```
The `/specials` API is defined by the `SpecialsController` in the Server project.
The `/specials` API is defined by the `SpecialsController` in the **BlazingPizza.Server** project.
Once the component is initialized it will render its markup. Replace the markup in the `Index` component with the following to list the pizza specials:
......@@ -123,7 +123,9 @@ Update the `MainLayout` component to define a top bar with a branding logo and a
@inherits LayoutComponentBase
<div class="top-bar">
<img class="logo" src="img/logo.svg" />
<a class="logo" href="">
<img src="img/logo.svg" />
</a>
<NavLink href="" class="nav-tab" Match="NavLinkMatch.All">
<img src="img/pizza-slice.svg" />
......
......@@ -65,7 +65,7 @@ Now we need to implement the pizza customization dialog so we can display it whe
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.
> Note: In Visual Studio, you can right-click the *Shared* directory in Solution Explorer, then choose *Add* -> *New Item* to use the *Razor Component* item template to add a new Razor component.
The `ConfigurePizzaDialog` should have a `Pizza` parameter that specifies the pizza being configured. Component parameters are defined by adding a writable property to the component decorated with the `[Parameter]` attribute. Add a `@code` block to the `ConfigurePizzaDialog` with the following `Pizza` parameter:
......@@ -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 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.
We want the value of `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):
......
......@@ -85,7 +85,7 @@ Then add a `@code` block that makes an asynchronous request for the data we need
```csharp
@code {
List<OrderWithStatus> ordersWithStatus;
IEnumerable<OrderWithStatus> ordersWithStatus;
protected override async Task OnParametersSetAsync()
{
......@@ -108,7 +108,7 @@ It's simple to express this using `@if/else` blocks in Razor code. Update the ma
{
<text>Loading...</text>
}
else if (ordersWithStatus.Count == 0)
else if (!ordersWithStatus.Any())
{
<h2>No orders placed</h2>
<a class="btn btn-success" href="">Order some pizza</a>
......@@ -142,7 +142,7 @@ Asynchronous work when applying parameters and property values must occur during
### 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.
If you want to reset your database to see the "no orders" case, simply delete `pizza.db` from the **BlazingPizza.Server** project and reload the page in your browser.
![My orders empty list](https://user-images.githubusercontent.com/1874516/77241390-a4b49100-6bae-11ea-8dd4-e59afdd8f710.png)
......@@ -199,7 +199,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.
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, such as bool, datetime and guid, are also supported.
![Order details empty](https://user-images.githubusercontent.com/1874516/77241434-391ef380-6baf-11ea-9803-9e7e65a4ea2b.png)
......@@ -259,17 +259,24 @@ Now you can implement the polling. Update your `@code` block as follows:
{
invalidOrder = false;
orderWithStatus = await HttpClient.GetFromJsonAsync<OrderWithStatus>($"orders/{OrderId}");
StateHasChanged();
if (orderWithStatus.IsDelivered)
{
pollingCancellationToken.Cancel();
}
else
{
await Task.Delay(4000);
}
}
catch (Exception ex)
{
invalidOrder = true;
pollingCancellationToken.Cancel();
Console.Error.WriteLine(ex);
StateHasChanged();
}
StateHasChanged();
await Task.Delay(4000);
}
}
}
......@@ -280,7 +287,7 @@ The code is a bit intricate, so be sure to go through it carefully to understand
* 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.
* We're using an `async void` method to represent the polling. This method runs for arbitrarily long, even while other methods run. `async void` methods have no way to report exceptions upstream to callers (because typically the callers have already finished), so it's important to use `try/catch` and do something meaningful with any exceptions that may occur.
* We're using `CancellationTokenSource` as a way of signalling when the polling should stop. Currently it only stops if there's an exception, but we'll add another stopping condition later.
* We're using `CancellationTokenSource` as a way of signalling when the polling should stop. Currently it stops if there's an exception, or once the order is delivered.
* We need to call `StateHasChanged` to tell Blazor that the component's data has (possibly) changed. The framework will then re-render the component. There's no way that the framework could know when to re-render your component otherwise, because it doesn't know about your polling logic.
## Rendering the order details
......
......@@ -20,7 +20,7 @@ public static async Task Main(string[] args)
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddBaseAddressHttpClient();
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<OrderState>();
await builder.Build().RunAsync();
......
......@@ -23,9 +23,9 @@ 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 redirects 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 401 "Not Authorized" responses, triggering an error message in the UI. That's good, because it shows that rules are being enforced on the server!
![Secure orders](https://user-images.githubusercontent.com/1874516/77242788-a9ce0c00-6bbf-11ea-98e6-c92e8f7c5cfe.png)
![Secure orders](https://user-images.githubusercontent.com/1101362/83876158-49ffef80-a730-11ea-8c86-f1fb2b51755b.png)
## Tracking authentication state
......@@ -51,7 +51,7 @@ public static async Task Main(string[] args)
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<OrderState>();
// Add auth services
......@@ -210,136 +210,125 @@ The user is logged in and redirected back to the home page.
## Request an access token
Even though you are now logged in, placing an order still fails because the HTTP request to place the order requires a valid access token. To request an access token use the `IAccessTokenProvider` service. If requesting an access token succeeds, add it to the request with a standard Authentication header with scheme Bearer. If the token request fails, use the `NavigationManager` service to redirect the user to the authorization service to request a new token.
Even though you are now logged in, placing an order still fails because the HTTP request to place the order requires a valid access token. To request access tokens and attach them to outbound requests, use the `BaseAddressAuthorizationMessageHandler` with the `HttpClient` that you're using to make the request. This message handler will acquire access tokens using the built-in `IAccessTokenProvider` service and attach them to each request using the standard Authorization header. If an access token cannot be acquired, an `AccessTokenNotAvailableException` is thrown, which can be used to redirect the user to the login page to authorize a new token.
*BlazingPizza.Client/Pages/Checkout.razor*
To add the `BaseAddressAuthorizationMessageHandler` to our `HttpClient` in our app, we'll use the [IHttpClientFactory` helpers from ASP.NET Core](https://docs.microsoft.com/aspnet/core/fundamentals/http-requests) with a strongly typed client.
```razor
@page "/checkout"
@attribute [Authorize]
@inject OrderState OrderState
@inject HttpClient HttpClient
@inject NavigationManager NavigationManager
@inject IAccessTokenProvider TokenProvider
To create the strongly typed client, add a new `OrdersClient` class to the client project. The class should take an `HttpClient` in its constructor, and provide methods getting and placing orders:
<div class="main">
...
</div>
@code {
bool isSubmitting;
*BlazingPizza.Client/OrdersClient.cs*
async Task PlaceOrder()
```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
namespace BlazingPizza.Client
{
public class OrdersClient
{
isSubmitting = true;
private readonly HttpClient httpClient;
var tokenResult = await TokenProvider.RequestAccessToken();
if (tokenResult.TryGetToken(out var accessToken))
public OrdersClient(HttpClient httpClient)
{
var request = new HttpRequestMessage(HttpMethod.Post, "orders");
request.Content = JsonContent.Create(OrderState.Order);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Value);
var response = await HttpClient.SendAsync(request);
var newOrderId = await response.Content.ReadFromJsonAsync<int>();
OrderState.ResetOrder();
NavigationManager.NavigateTo($"myorders/{newOrderId}");
this.httpClient = httpClient;
}
else
public async Task<IEnumerable<OrderWithStatus>> GetOrders() =>
await httpClient.GetFromJsonAsync<IEnumerable<OrderWithStatus>>("orders");
public async Task<OrderWithStatus> GetOrder(int orderId) =>
await httpClient.GetFromJsonAsync<OrderWithStatus>($"orders/{orderId}");
public async Task<int> PlaceOrder(Order order)
{
NavigationManager.NavigateTo(tokenResult.RedirectUrl);
var response = await httpClient.PostAsJsonAsync("orders", order);
response.EnsureSuccessStatusCode();
var orderId = await response.Content.ReadFromJsonAsync<int>();
return orderId;
}
}
}
```
Update the `MyOrders` and `OrderDetails` components to also make authenticated HTTP requests.
Register the `OrdersClient` as a typed client, with the underlying `HttpClient` configured with the correct base address and the `BaseAddressAuthorizationMessageHandler`.
*BlazingPizza.Client/Pages/MyOrders.razor*
```csharp
builder.Services.AddHttpClient<OrdersClient>(client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
```
```razor
@page "/myorders"
@inject HttpClient HttpClient
@inject NavigationManager NavigationManager
@inject IAccessTokenProvider TokenProvider
Update each page where an `HttpClient` is used to manage orders to use the new typed `OrdersClient`. Inject an `OrdersClient` instead of an `HttpClient` and use the new client to make the API call. Wrap each call in a `try-catch` that handles exceptions of type `AccessTokenNotAvailableException` by calling the provided `Redirect()` method.
<div class="main">
...
</div>
*Checkout.razor*
@code {
List<OrderWithStatus> ordersWithStatus;
```csharp
async Task PlaceOrder()
{
isSubmitting = true;
protected override async Task OnParametersSetAsync()
try
{
var tokenResult = await TokenProvider.RequestAccessToken();
if (tokenResult.TryGetToken(out var accessToken))
{
var request = new HttpRequestMessage(HttpMethod.Get, "orders");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Value);
var response = await HttpClient.SendAsync(request);
ordersWithStatus = await response.Content.ReadFromJsonAsync<List<OrderWithStatus>>();
}
else
{
NavigationManager.NavigateTo(tokenResult.RedirectUrl);
}
var newOrderId = await OrdersClient.PlaceOrder(OrderState.Order);
OrderState.ResetOrder();
NavigationManager.NavigateTo($"myorders/{newOrderId}");
}
catch (AccessTokenNotAvailableException ex)
{
ex.Redirect();
}
}
```
*BlazingPizza.Client/Pages/OrderDetails.razor*
*MyOrders.razor*
```razor
@page "/myorders/{orderId:int}"
@using System.Threading
@inject HttpClient HttpClient
@inject NavigationManager NavigationManager
@inject IAccessTokenProvider TokenProvider
@implements IDisposable
<div class="main">
....
</div>
```csharp
protected override async Task OnParametersSetAsync()
{
try
{
ordersWithStatus = await OrdersClient.GetOrders();
}
catch (AccessTokenNotAvailableException ex)
{
ex.Redirect();
}
}
```
@code {
...
*OrderDetails.razor*
private async void PollForUpdates()
```csharp
private async void PollForUpdates()
{
invalidOrder = false;
pollingCancellationToken = new CancellationTokenSource();
while (!pollingCancellationToken.IsCancellationRequested)
{
var tokenResult = await TokenProvider.RequestAccessToken();
if (tokenResult.TryGetToken(out var accessToken))
try
{
pollingCancellationToken = new CancellationTokenSource();
while (!pollingCancellationToken.IsCancellationRequested)
{
try
{
invalidOrder = false;
var request = new HttpRequestMessage(HttpMethod.Get, $"orders/{OrderId}");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Value);
var response = await HttpClient.SendAsync(request);
orderWithStatus = await response.Content.ReadFromJsonAsync<OrderWithStatus>();
}
catch (Exception ex)
{
invalidOrder = true;
pollingCancellationToken.Cancel();
Console.Error.WriteLine(ex);
}
StateHasChanged();
await Task.Delay(4000);
}
orderWithStatus = await OrdersClient.GetOrder(OrderId);
StateHasChanged();
await Task.Delay(4000);
}
else
catch (AccessTokenNotAvailableException ex)
{
NavigationManager.NavigateTo(tokenResult.RedirectUrl);
pollingCancellationToken.Cancel();
ex.Redirect();
}
catch (Exception ex)
{
invalidOrder = true;
pollingCancellationToken.Cancel();
Console.Error.WriteLine(ex);
StateHasChanged();
}
}
...
}
```
......@@ -365,38 +354,19 @@ Next look for the commented-out `.Where` lines in `GetOrders` and `GetOrderWithS
Now if you run the app again, you'll no longer be able to see the existing order details, because they aren't associated with your user ID. If you place a new order with one account, you won't be able to see it from a different account. That makes the application much more useful.
## Ensure authentication before placing or viewing orders
Now if you're logged in, you'll be able to place orders and see order status. But if you log out then make another attempt to place an order, bad things will happen. The server will reject the `POST` request, causing a client-side exception, but the user won't know why.
## Enforcing login on specific pages
To fix this on the checkout page, let's make the UI prompt the user to log in (if necessary) as part of placing an order.
Now if you're logged in, you'll be able to place orders and see order status. But if you're not logged in and try to place an order, the flow isn't ideal. It doesn't ask you to log in until you *submit* the checkout form (because that's when the server responds 401 Not Authorized). What if you want to make certain pages require authorization, even before receiving 401 Not Authorized responses from the server?
In the `Checkout` page component, add an `OnInitializedAsync` with some logic to to check whether the user is currently authenticated. If they aren't, send them off to the login endpoint.
You can do this quite easily. In the same way that you use the `[Authorize]` attribute in server-side code, you can use that attribute in client-side Blazor pages. Let's fix the checkout page so that you have to be logged in as soon as you get there, not just when you submit its form.
```cs
@code {
[CascadingParameter] public Task<AuthenticationState> AuthenticationStateTask { get; set; }
By default, all pages allow for anonymous access, but we can specify that the user must be logged in to access the checkout page by adding the `[Authorize]` attribute at the top of `Checkout.razor`:
protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateTask;
if (!authState.User.Identity.IsAuthenticated)
{
// The server won't accept orders from unauthenticated users, so avoid
// an error by making them log in at this point
NavigationManager.NavigateTo("authentication/login?redirectUri=/checkout", true);
}
}
// Leave PlaceOrder unchanged here
}
```razor
@attribute [Authorize]
```
Try it out: now if you're logged out and get to the checkout screen, you'll be redirected to log in. The value for the `[CascadingParameter]` comes from your `AuthenticationStateProvider` via the `CascadingAuthenticationState` you added earlier.
But do you notice something a bit awkward about it? It still shows the checkout UI briefly before the browser loads the login page. We could fix that by wrapping the "checkout" UI inside an `AuthorizeView` like we did in the `LoginDisplay`. But there's an even easier way to ensure that anyone who navigates to the checkout page is logged in. We can enforce that the entire page requires authentication using the router.
To set this up, update *App.razor* to render an `AuthorizeRouteView` instead of a `RouteView` when the route is found.
Next, to make the router respect such attributes, update *App.razor* to render an `AuthorizeRouteView` instead of a `RouteView` when the route is found.
```razor
<CascadingAuthenticationState>
......@@ -422,15 +392,7 @@ To set this up, update *App.razor* to render an `AuthorizeRouteView` instead of
The `AuthorizeRouteView` will route navigations to the correct component, but only if the user is authorized. If the user is not authorized, the `NotAuthorized` content is displayed. You can also specify content to display while the `AuthorizeRouteView` is determining if the user is authorized.
By default, all pages allow for anonymous access, but we can specify that the user must be logged in to access the checkout page by adding the `[Authorize]` attribute. You add attributes to a component using the `@attribute` directive.
Update the `Checkout`, `MyOrders`, and `OrderDetails` pages to add the `[Authorize]` attribute;
```razor
@attribute [Authorize]
```
Now when you try to nativgate to any of these pages while signed out, you see the `NotAuthorized` content we setup in *App.razor*.
Now when you try to nativgate to the checkout page while signed out, you see the `NotAuthorized` content we setup in *App.razor*.
![Not authorized](https://user-images.githubusercontent.com/1874516/78410504-63b27880-75c1-11ea-8c2c-ab62c1c24596.png)
......@@ -472,7 +434,9 @@ Then replace the `NotAuthorized` content in *App.razor* with the `RedirectToLogi
</CascadingAuthenticationState>
```
If you now try to access the "myorders" page while signed out, you are redirected to the login page. And once the user is logged in, they are redirected back to the page they were trying to access thanks to the `returnUrl` parameter.
If you now try to access the checkout page while signed out, you are redirected to the login page. And once the user is logged in, they are redirected back to the page they were trying to access thanks to the `returnUrl` parameter.
## Hiding navigation options depending on authorization status
It's a bit unfortunate that users can see the My Orders tab when they are not logged in. We can hide the My Orders tab for unauthenticated users using the `AuthorizeView` component.
......@@ -489,9 +453,8 @@ Update `MainLayout` to wrap the My Orders `NavLink` in an `AuthorizeView`.
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:
We've now seen two ways to interact with the authentication/authorization system inside components:
* 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.
......@@ -519,7 +482,7 @@ To configure the authentication system to use our `PizzaAuthenticationState` ins
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.
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 `ReplaceOrder` method to `OrderState` so that you can reestablish the saved order.
*BlazingPizza.Client/Pages/Authentication.razor*
......
......@@ -205,7 +205,7 @@ Start by creating a new file `TemplatedList.razor` in the `BlazingComponents` pr
1. Async-loading of any type of data
2. Separate rendering logic for three states - loading, empty list, and showing items
We can solve async loading by accepting a delegate of type `Func<Task<List<?>>>` - we need to figure out what type should replace **?**. Since we want to support any kind of data, we need to declare this component as a generic type. We can make a generic-typed component using the `@typeparam` directive, so place this at the top of `TemplatedList.razor`.
We can solve async loading by accepting a delegate of type `Func<Task<IEnumerable<?>>>` - we need to figure out what type should replace **?**. Since we want to support any kind of data, we need to declare this component as a generic type. We can make a generic-typed component using the `@typeparam` directive, so place this at the top of `TemplatedList.razor`.
```html
@typeparam TItem
......@@ -219,9 +219,9 @@ Now that we've defined a generic type parameter we can use it in a parameter dec
```html
@code {
List<TItem> items;
IEnumerable<TItem> items;
[Parameter] public Func<Task<List<TItem>>> Loader { get; set; }
[Parameter] public Func<Task<IEnumerable<TItem>>> Loader { get; set; }
protected override async Task OnParametersSetAsync()
{
......@@ -237,7 +237,7 @@ Since we have the data, we can now add the structure of each of the states we ne
{
}
else if (items.Count == 0)
else if (!items.Any())
{
}
else
......@@ -270,7 +270,7 @@ Now that we have some `RenderFragment` parameters, we can start using them. Upda
{
@Loading
}
else if (items.Count == 0)
else if (!items.Any())
{
@Empty
}
......@@ -295,9 +295,9 @@ Let's add another `string` parameter, and finally the functions block of `Templa
```html
@code {
List<TItem> items;
IEnumerable<TItem> items;
[Parameter] public Func<Task<List<TItem>>> Loader { get; set; }
[Parameter] public Func<Task<IEnumerable<TItem>>> Loader { get; set; }
[Parameter] public RenderFragment Loading { get; set; }
[Parameter] public RenderFragment Empty { get; set; }
[Parameter] public RenderFragment<TItem> Item { get; set; }
......@@ -319,7 +319,7 @@ Lastly update the `<div class="list-group">` to contain `<div class="list-group
{
@Loading
}
else if (items.Count == 0)
else if (!items.Any())
{
@Empty
}
......@@ -336,9 +336,9 @@ else
}
@code {
List<TItem> items;
IEnumerable<TItem> items;
[Parameter] public Func<Task<List<TItem>>> Loader { get; set; }
[Parameter] public Func<Task<IEnumerable<TItem>>> Loader { get; set; }
[Parameter] public RenderFragment Loading { get; set; }
[Parameter] public RenderFragment Empty { get; set; }
[Parameter] public RenderFragment<TItem> Item { get; set; }
......@@ -355,31 +355,27 @@ else
To use the new `TemplatedList` component, we're going to edit `MyOrders.razor`.
First, we need to create a delegate that we can pass to the `TemplatedList` that will load order data. We can do this by keeping the line of code that's in `MyOrders.OnParametersSetAsync` and changing the method signature. The `@code` block should look something like:
First, we need to create a delegate that we can pass to the `TemplatedList` that will load order data. We can do this by keeping the code that's in `MyOrders.OnParametersSetAsync` and changing the method signature. The `@code` block should look something like:
```html
@code {
async Task<List<OrderWithStatus>> LoadOrders()
async Task<IEnumerable<OrderWithStatus>> LoadOrders()
{
var ordersWithStatus = new List<OrderWithStatus>();
var tokenResult = await TokenProvider.RequestAccessToken();
if (tokenResult.TryGetToken(out var accessToken))
var ordersWithStatus = Enumerable.Empty<OrderWithStatus>();
try
{
var request = new HttpRequestMessage(HttpMethod.Get, "orders");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Value);
var response = await HttpClient.SendAsync(request);
ordersWithStatus = await response.Content.ReadFromJsonAsync<List<OrderWithStatus>>();
ordersWithStatus = await OrdersClient.GetOrders();
}
else
catch (AccessTokenNotAvailableException ex)
{
NavigationManager.NavigateTo(tokenResult.RedirectUrl);
ex.Redirect();
}
return ordersWithStatus;
}
}
```
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.
This matches the signature expected by the `Loader` parameter of `TemplatedList`, it's a `Func<Task<IEnumerable<?>>>` where the **?** is replaced with `OrderWithStatus` so we are on the right track.
You can use the `TemplatedList` component now like so:
......
......@@ -125,22 +125,28 @@ You'll then need to define `RequestNotificationSubscriptionAsync`. Add this else
```cs
async Task RequestNotificationSubscriptionAsync()
{
var tokenResult = await TokenProvider.RequestAccessToken();
if (tokenResult.TryGetToken(out var accessToken))
var subscription = await JSRuntime.InvokeAsync<NotificationSubscription>("blazorPushNotifications.requestSubscription");
if (subscription != null)
{
var subscription = await JSRuntime.InvokeAsync<NotificationSubscription>("blazorPushNotifications.requestSubscription");
if (subscription != null)