Unverified Commit 243db528 authored by Daniel Roth's avatar Daniel Roth Committed by GitHub
Browse files

Update to Preview5 (#63)

parent 1854cdf5
......@@ -2,7 +2,7 @@
Welcome to the Blazor app building workshop!
Blazor is an experimental single-page app framework for building client-side web apps using .NET and WebAssembly. In this workshop we will build a complete Blazor app and learn about the various Blazor framework features along the way.
Blazor is an single-page app framework for building client-side web apps using .NET and WebAssembly. In this workshop we will build a complete Blazor app and learn about the various Blazor framework features along the way.
![Blazing Pizza](https://user-images.githubusercontent.com/1874516/51886593-5a5bc980-2388-11e9-9329-7e015901e45d.png)
......
......@@ -4,48 +4,10 @@ In this session, you'll setup your machine for Blazor development and build your
## Setup
Install the following:
To get started with Blazor, follow the instructions on https://blazor.net/get-started.
1. [.NET Core 2.1 SDK](https://go.microsoft.com/fwlink/?linkid=873092) (2.1.500 or later).
1. [Visual Studio 2017](https://go.microsoft.com/fwlink/?linkid=873093) (15.9 or later) with the *ASP.NET and web development* workload selected.
1. The latest [Blazor Language Services extension](https://go.microsoft.com/fwlink/?linkid=870389) from the Visual Studio Marketplace.
1. The Blazor templates on the command-line:
## Build your first app
```console
dotnet new -i Microsoft.AspNetCore.Blazor.Templates
```
## Build and run your first Blazor app
To create a Blazor project in Visual Studio:
1. Select **File** > **New** > **Project**. Select **Web** > **ASP.NET Core Web Application**. Name the project "BlazorApp1" in the **Name** field. Select **OK**.
![New ASP.NET Core project](https://raw.githubusercontent.com/aspnet/Blazor.Docs/gh-pages/docs/tutorials/build-your-first-blazor-app/_static/new-aspnet-core-project.png)
1. The **New ASP.NET Core Web Application** dialog appears. Make sure **.NET Core** is selected at the top. Also select **ASP.NET Core 2.1**. Choose the **Blazor** template and select **OK**.
![New Blazor app dialog](https://raw.githubusercontent.com/aspnet/Blazor.Docs/gh-pages/docs/tutorials/build-your-first-blazor-app/_static/new-blazor-app-dialog.png)
1. Once the project is created, press **Ctrl-F5** to run the app *without the debugger*. Running with the debugger (**F5**) isn't supported at this time.
> [!NOTE]
> If not using Visual Studio, create the Blazor app at a command prompt on Windows, macOS, or Linux:
>
> ```console
> dotnet new blazor -o BlazorApp1
> cd BlazorApp1
> dotnet run
> ```
>
> Navigate to the app using the localhost address and port provided in the console window output after `dotnet run` is executed. Use **Ctrl-C** in the console window to shutdown the app.
The Blazor app runs in the browser:
![Blazor app Home page](https://user-images.githubusercontent.com/1874516/39509497-5515c3ea-4d9b-11e8-887f-019ea4fdb3ee.png)
Congrats! You just built and ran your first Blazor app!
If you have more time, try out the rest of the [introductory Blazor tutorial](https://docs.microsoft.com/en-us/aspnet/core/tutorials/build-your-first-razor-components-app)
Once you have your first Blazor app running, try [building your first Blazor app](https://docs.microsoft.com/aspnet/core/tutorials/build-your-first-blazor-app).
Next up - [Components and layout](01-components-and-layout.md)
......@@ -8,7 +8,7 @@ We've setup the initial solution for you for the pizza store app in this repo. G
The solution already contains four projects:
![image](https://user-images.githubusercontent.com/1874516/51783836-f2418500-20f4-11e9-95d4-4e9b34e6b2ca.png)
![image](https://user-images.githubusercontent.com/1874516/57006654-3e3e1300-6b97-11e9-8053-b6ec9c31614d.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 +19,7 @@ Run the app by hitting `Ctrl-F5`. Currently the app only contains a simple home
![image](https://user-images.githubusercontent.com/1874516/51783774-afcb7880-20f3-11e9-9c22-2f330380ff1e.png)
Open *Pages/Index.cshtml* to see the code for the home page.
Open *Pages/Index.razor* to see the code for the home page.
```
@page "/"
......@@ -33,7 +33,7 @@ The home page is implemented as a single component. The `@page` directive specif
First we'll update the home page to display the list of available pizza specials. The list of specials will be part of the state of the `Index` component.
Add a `@functions` block to *Index.cshtml* with a list field to keep track of the available specials:
Add a `@functions` block to *Index.razor* with a list field to keep track of the available specials:
```csharp
@functions {
......@@ -60,7 +60,7 @@ Override the `OnInitAsync` method in the `@functions` block to retrieve the list
protected async override Task OnInitAsync()
{
specials = await HttpClient.GetJsonAsync<List<PizzaSpecial>>("/specials");
specials = await HttpClient.GetJsonAsync<List<PizzaSpecial>>("specials");
}
}
```
......@@ -89,16 +89,16 @@ Once the component is initialized it will render its markup. Replace the markup
</div>
```
![Pizza specials list](https://user-images.githubusercontent.com/1874516/51797486-8b0ef800-21fc-11e9-9d2e-a703d6574537.png)
![image](https://user-images.githubusercontent.com/1874516/57006743-1602e400-6b98-11e9-96cb-ff4829cf459f.png)
## Create the layout
Next we'll setup the layout for app.
Layouts in Blazor are also components. They inherit from `BlazorLayoutComponent`, 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.cshtml*.
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*.
```html
@inherits BlazorLayoutComponent
@inherits LayoutComponentBase
<div class="content">
@Body
......@@ -106,7 +106,7 @@ Layouts in Blazor are also components. They inherit from `BlazorLayoutComponent`
```
To apply a layout use the `@layout` directive. Typically this is done in a `_ViewImports.cshtml` file, which then gets inherited hierarchically. See *Pages/_ViewImports.cshtml*.
To apply a layout use the `@layout` directive. Typically this is done in a `_Imports.razor` file, which then gets inherited hierarchically. See *Pages/_Imports.razor*.
```
@layout MainLayout
......@@ -115,7 +115,7 @@ To apply a layout use the `@layout` directive. Typically this is done in a `_Vie
Update the `MainLayout` component to define a top bar with a branding logo and a nav link for the home page:
```html
@inherits BlazorLayoutComponent
@inherits LayoutComponentBase
<div class="top-bar">
<img class="logo" src="img/logo.svg" />
......@@ -137,6 +137,7 @@ The `NavLink` component is the same as an anchor tag, except that it adds an `ac
With our new layout our pizza store app now looks like this:
![Pizza store layout](https://user-images.githubusercontent.com/1874516/51797487-9feb8b80-21fc-11e9-8c91-52dfc86d057f.png)
![image](https://user-images.githubusercontent.com/1874516/57006730-e81d9f80-6b97-11e9-813d-9c35b62efa53.png)
Next up - [Customize a pizza](02-customize-a-pizza.md)
......@@ -6,7 +6,7 @@ In this session we'll update the pizza store app to enable users to customize th
When the user clicks a pizza special a pizza customization dialog should pop up to allow the user to customize their pizza and add it to their order. To handle DOM UI events in a Blazor app, you specify which event you want to handle using the corresponding HTML attribute and then specify the C# delegate you want called. The delegate may optionally take an event specific argument, but it's not required.
In *Pages/Index.cshtml* add the following `onclick` handler to the list item for each pizza special:
In *Pages/Index.razor* add the following `onclick` handler to the list item for each pizza special:
```html
@foreach (var special in specials)
......@@ -27,7 +27,7 @@ Run the app and check that the pizza name is written to the browser console when
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.
Update the `@functions` block in *Index.cshtml* to add some additional fields for tracking the pizza being customized and whether the pizza customization dialog is visible.
Update the `@functions` block in *Index.razor* to add some additional fields for tracking the pizza being customized and whether the pizza customization dialog is visible.
```csharp
List<PizzaSpecial> specials;
......@@ -62,7 +62,7 @@ Update the `onclick` handler to call the `ShowConfigurePizzaDialog` method inste
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.cshtml* 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.
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 `@functions` block to the `ConfigurePizzaDialog` with the following `Pizza` parameter:
......@@ -95,7 +95,7 @@ Add the following basic markup for the `ConfigurePizzaDialog`:
</div>
```
Update *Pages/Index.cshtml* to show the `ConfigurePizzaDialog` when a pizza special has been selected. The `ConfigurePizzaDialog` is styled to overlay the current page, so it doesn't really matter where you put this code block.
Update *Pages/Index.razor* to show the `ConfigurePizzaDialog` when a pizza special has been selected. The `ConfigurePizzaDialog` is styled to overlay the current page, so it doesn't really matter where you put this code block.
```html
@if (showingConfigureDialog)
......@@ -174,7 +174,7 @@ The user should also be able to select additional toppings. Add a list property
protected async override Task OnInitAsync()
{
toppings = await HttpClient.GetJsonAsync<List<Topping>>("/toppings");
toppings = await HttpClient.GetJsonAsync<List<Topping>>("toppings");
}
}
```
......@@ -250,13 +250,13 @@ You should now be able to add and remove toppings.
## Component events
The Cancel and Order buttons don't do anything yet. We need some way to communicate to the `Index` component when the user adds the pizza to their order or cancels. We can do that by defining component events. Component events are action parameters that parent components can subscribe to.
The Cancel and Order buttons don't do anything yet. We need some way to communicate to the `Index` component when the user adds the pizza to their order or cancels. We can do that by defining component events. Component events are callback parameters that parent components can subscribe to.
Add two parameters to the `ConfigurePizzaDialog` component: `OnCancel` and `OnConfirm`. Both parameters should be of type `Action`.
Add two parameters to the `ConfigurePizzaDialog` component: `OnCancel` and `OnConfirm`. Both parameters should be of type `EventCallback`.
```csharp
[Parameter] Action OnCancel { get; set; }
[Parameter] Action OnConfirm { get; set; }
[Parameter] EventCallback OnCancel { get; set; }
[Parameter] EventCallback OnConfirm { get; set; }
```
Add `onclick` event handlers to the `ConfigurePizzaDialog` that trigger the `OnCancel` and `OnConfirm` events.
......@@ -344,7 +344,7 @@ Create a new `ConfiguredPizzaItem` component for displaying a configured pizza.
@functions {
[Parameter] Pizza Pizza { get; set; }
[Parameter] Action OnRemoved { get; set; }
[Parameter] EventCallback OnRemoved { get; set; }
}
```
......@@ -389,7 +389,7 @@ void RemoveConfiguredPizza(Pizza pizza)
async Task PlaceOrder()
{
await HttpClient.PostJsonAsync("/orders", order);
await HttpClient.PostJsonAsync("orders", order);
order = new Order();
}
```
......
......@@ -4,7 +4,7 @@ Your customers can order pizzas, but so far have no way to see the status of the
## Adding a navigation link
Open `Shared/MainLayout.cshtml`. 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 `NavLink`. Add a plain HTML `<a>` tag pointing to `myorders`:
```html
<div class="top-bar">
......@@ -23,7 +23,7 @@ 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)
This shows it's not strictly necessary to use `<NavLink>`. We'll see the reason to use it momentatily.
This shows it's not strictly necessary to use `<NavLink>`. We'll see the reason to use it momentarily.
## Adding a "My Orders" page
......@@ -33,7 +33,7 @@ If you click "My Orders", nothing will seem to happen. Open your browser's dev t
Error: System.InvalidOperationException: 'Router' cannot find any component with a route for '/myorders'.
```
As you can guess, we will fix this by adding a component to match this route. Create a file in the `Pages` folder called `MyOrders.cshtml`, with the following content:
As you can guess, we will fix this by adding a component to match this route. Create a file in the `Pages` folder called `MyOrders.razor`, with the following content:
```html
@page "/myorders"
......@@ -80,7 +80,7 @@ Then add a `@functions` block that makes an asynchronous request for the data we
protected override async Task OnParametersSetAsync()
{
ordersWithStatus = await HttpClient.GetJsonAsync<List<OrderWithStatus>>("/orders");
ordersWithStatus = await HttpClient.GetJsonAsync<List<OrderWithStatus>>("orders");
}
}
```
......@@ -118,7 +118,7 @@ It's simple to express this using `@if/else` blocks in Razor code. Update your c
protected override async Task OnParametersSetAsync()
{
ordersWithStatus = await HttpClient.GetJsonAsync<List<OrderWithStatus>>("/orders");
ordersWithStatus = await HttpClient.GetJsonAsync<List<OrderWithStatus>>("orders");
}
}
```
......@@ -184,7 +184,7 @@ It looks like a lot of code, but there's nothing special here. It simply uses a
If you click on the "Track" link buttons next to an order, the browser will attempt a client-side navigation to `myorders/<id>` (e.g., `http://example.com/myorders/37`). Currently this will just log an error because no component matches this route.
Once again we'll add a component to handle this. In the `Pages` directory, create a file called `OrderDetails.cshtml`, containing:
Once again we'll add a component to handle this. In the `Pages` directory, create a file called `OrderDetails.razor`, containing:
```html
@page "/myorders/{orderId:int}"
......@@ -205,7 +205,7 @@ 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.
2. The `App` component (in `App.cshtml`) 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.
2. The `App` component (in `App.razor`) contains a `<Router>`. `Router` is a built-in component that interacts with the browser's client-side navigation APIs. It registers a navigation event handler that gets notification whenever the user clicks on a link.
3. Whenever the user clicks a link, code in `Router` checks whether the destination URL is within the same SPA (i.e., whether it's under the `<base href>` value). If it's not, traditional full-page navigation occurs as usual. But if the URL is within the SPA, `Router` will handle it.
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's no matching component, it's an error. This will change in Blazor 0.8.0, which includes support for fallback routes (e.g., for custom "not found" pages).
......@@ -220,7 +220,7 @@ What's more, we'll also account for the possibility of `OrderId` being invalid.
* No such order exists
* Or later, when we've implement authentication, if the order is for a different user and you're not allowed to see it
Before we can implement the polling, we'll need to add the following directives at the top of `OrderDetails.cshtml`, typically directly under the `@page` directive:
Before we can implement the polling, we'll need to add the following directives at the top of `OrderDetails.razor`, typically directly under the `@page` directive:
```html
@using System.Threading
......@@ -382,7 +382,7 @@ This is wasteful of client-side memory and CPU time, network bandwidth, and serv
To fix this, we need to make `OrderDetails` stop the polling once it gets removed from the display. This is simply a matter of using the `IDisposable` interface.
In `OrderDetails.cshtml`, add the following directive at the top of the file, underneath the other directives:
In `OrderDetails.razor`, add the following directive at the top of the file, underneath the other directives:
```html
@implements IDisposable
......@@ -426,13 +426,13 @@ To use this, update the `PlaceOrder` code so it calls `UriHelper.NavigateTo`:
```cs
async Task PlaceOrder()
{
await HttpClient.PostJsonAsync("/orders", order);
await HttpClient.PostJsonAsync("orders", order);
order = new Order();
UriHelper.NavigateTo("myorders");
}
```
Now as soon as the server accepts the order, the browser will switch to the "order details" display and being polling for updates.
Now as soon as the server accepts the order, the browser will switch to the "order details" display and begin polling for updates.
Next up - [Refactor state management](04-refactor-state-management.md)
......@@ -180,7 +180,7 @@ To hook up the other side, we should subscribe to the change notifications on `I
protected async override Task OnInitAsync()
{
OrderState.StateChanged += OnOrderStateChanged;
specials = await HttpClient.GetJsonAsync<List<PizzaSpecial>>("/specials");
specials = await HttpClient.GetJsonAsync<List<PizzaSpecial>>("specials");
}
void IDisposable.Dispose()
......
......@@ -10,7 +10,7 @@ The first and most important principle is that all *real* security rules must be
As such, we're going to start by enforcing some access rules in the backend server, even before the client code knows about them.
Inside the `BlazorPizza.Server` project, you'll find `OrdersController.cs`. This is the ASP.NET Core MVC controller class that handles incoming HTTP requests for `/orders` and `/orders/{orderId}`. To require that all requests to these endpoints come from authenticated users (i.e., people who have logged in), add the `[Authorize]` attribute to the `OrdersController` class:
Inside the `BlazorPizza.Server` project, you'll find `OrdersController.cs`. This is the controller class that handles incoming HTTP requests for `/orders` and `/orders/{orderId}`. To require that all requests to these endpoints come from authenticated users (i.e., people who have logged in), add the `[Authorize]` attribute to the `OrdersController` class:
```csharp
[Route("orders")]
......@@ -37,7 +37,7 @@ We're going to use a ready-made component called `UserStateProvider`, which is r
This is all defined in the `BlazingPizza.ComponentsLibrary` project, and may end up being baked into the framework in some form, as it's more complex than many developers would want to write themselves.
To use it, update `App.cshtml`, wrapping an instance of `UserStateProvider` around the entire application:
To use it, update `App.razor`, wrapping an instance of `UserStateProvider` around the entire application:
```html
<UserStateProvider>
......@@ -153,7 +153,7 @@ async Task PlaceOrder()
// to sign in first if needed
if (await UserState.TrySignInAsync())
{
await HttpClient.PostJsonAsync("/orders", OrderState.Order);
await HttpClient.PostJsonAsync("orders", OrderState.Order);
OrderState.ResetOrder();
UriHelper.NavigateTo("myorders");
}
......@@ -172,10 +172,10 @@ One way to do that would be to use `[CascadingParameter]` to get the user's sign
There are many ways this could be done, but one particularly convienient way is to use a *layout*. Since layouts can be nested, we can make a `ForceSignInLayout` component that nests inside the existing `MainLayout`. Then, any page that uses our `ForceSignInLayout` will only display when the user is signed in, and if they aren't, it will show a prompt to sign in.
Start by creating a new component called `ForceSignInLayout.cshtml` inside the `Shared` directory, containing:
Start by creating a new component called `ForceSignInLayout.razor` inside the `Shared` directory, containing:
```html
@inherits BlazorLayoutComponent
@inherits LayoutComponentBase
@layout MainLayout
@if (UserState.CurrentUser == null) // Retrieving the login state
......@@ -200,7 +200,7 @@ else
}
```
This is a layout, because it inherits from `BlazorLayoutComponent`. It nests inside `MainLayout`, because it has its own `@layout` directive saying so.
This is a layout, because it inherits from `LayoutComponentBase`. It nests inside `MainLayout`, because it has its own `@layout` directive saying so.
Further, it uses `[CascadingParameter]` to get a `UserState` value, and uses that to decide whether to render the current page (by outputting `@Body`) or a "sign in" button instead.
......
......@@ -6,7 +6,7 @@ Users of the pizza store can now track the status of their orders in real time.
Included in the ComponentsLibrary project is a prebuilt `Map` component for displaying the location of a set of markers and animating their movements over time. We'll use this component to show the location of the user's pizza orders as they are being delivered, but first let's look at how the `Map` component is implemented.
Open *Map.cshtml* and take a look at the code:
Open *Map.razor* and take a look at the code:
```csharp
@using Microsoft.JSInterop
......@@ -32,7 +32,7 @@ Open *Map.cshtml* 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 `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` compoent lifecycle event to ensure that the component is done rendering its markup. The `deliveryMap.showOrUpdate` function is defined in the *content/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 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 *content/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? If you peek inside of the project file for the ComponentsLibrary you'll see that the files in the content directory are built into the library as embedded resources. The Blazor build infrastructure then takes care of extracting these resources and making them available as static assets.
......
......@@ -23,14 +23,13 @@ Open the project file via *right click* -> *Edit BlazingComponents.csproj*. We'r
It looks like:
```xml
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<OutputType>Library</OutputType>
<IsPackable>true</IsPackable>
<BlazorLinkOnBuild>false</BlazorLinkOnBuild>
<LangVersion>7.3</LangVersion>
<RazorLangVersion>3.0</RazorLangVersion>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
......@@ -41,11 +40,11 @@ It looks like:
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Blazor.Browser" Version="0.7.0" />
<PackageReference Include="Microsoft.AspNetCore.Blazor.Build" Version="0.7.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.Browser" Version="3.0.0-preview5-19227-01" />
</ItemGroup>
</Project>
```
There are a few things here worth understanding.
......@@ -54,11 +53,9 @@ Firstly, it's recommended that all Blazor project targets C# version 7.3 or newe
Next, the `<IsPackable>true</IsPackable>` line makes it possible the create a NuGet package from this project. We won't be using this project as a package in this example, but this is a good thing to have for a class library.
Also, the `<BlazorLinkOnBuild>false</BlazorLinkOnBuild>` line deactivates the Mono Linker for this project. We haven't talked much about the linker in this workshop, but what's important to know about this is that the linker applies to apps not to libraries. Since this is a library we want it off.
Next, the lines that look like `<EmbeddedResource ... />` give the class library project special handling of content files that should be included in the project. This makes it easier to do multi-project development with static assests, and to redistibute libraries containing static assets. We saw this in action already in the previous step.
Lastly the two `<PackageReference />` elements add package references to the Blazor runtime libraries and the Blazor build support.
Lastly the `<PackageReference />` element adds a package references to the Blazor component model.
## Writing a templated dialog
......@@ -68,7 +65,7 @@ Let's think about how a *reusable dialog* should work. We would expect a dialog
Blazor happens to have a feature that works for exactly this case, it's similar to how a layout works. Recall that a layout has a `Body` parameter, and the layout gets to place other content *around* the `Body`. In a layout, the `Body` parameter is of type `RenderFragment` which is a delegate type that the runtime has special handling for. The good news is that this feature is not limited to layouts. Any component can declare a parameter of type `RenderFragment`.
Let's get started on this new dialog component. Create a new component file named `TemplatedDialog.cshtml` in the `BlazingComponents` project. Put the following markup inside `TemplatedDialog.cshtml`:
Let's get started on this new dialog component. Create a new component file named `TemplatedDialog.razor` in the `BlazingComponents` project. Put the following markup inside `TemplatedDialog.razor`:
```html
<div class="dialog-container">
......@@ -118,33 +115,31 @@ Do build and make sure that everything compiles at this stage. Next we'll get do
Before we can use this component in the `BlazingPizza.Client` project, we will need to add a project reference. Do this by adding a project reference from `BlazingPizza.Client` to `BlazingComponents`.
Once that's done, there's one more minor step. Open the `_ViewImports.cshtml` in the topmost directory of `BlazingPizza.Client` and add this line at the end:
Once that's done, there's one more minor step. Open the `_Imports.razor` in the topmost directory of `BlazingPizza.Client` and add this line at the end:
```html
@addTagHelper "*, BlazingComponents"
@using BlazingComponents
```
`@addTagHelper` is there to inform the compiler that it should consider the `BlazingComponents` library as a source for components that can be used. This is a very temporary thing that's currently needed. We expect this to be removed in the future.
Now that the project reference has been added, do a build again to verify that everything still compiles.
## Another refactor
We're also going to do another slight refactor to decouple the `ConfigurePizzaDialog` from `OrderState`. This is an idea that we discussed after step 4, and we want to try it to see if it feels better. This will also help work around an issue we discovered while writing this section - Blazor is still under development after all.
Let's add back the `Pizza`, `OnCancel`, and `OnConfirm` parameters. Also move the `AddTopping` and `RemoveTopping` from `OrderState`. The result of all of this is that the `@functions` block of `ConfigurePizzaDialog.cshtml` should look the same as it did after completing step 2.
Let's add back the `Pizza`, `OnCancel`, and `OnConfirm` parameters. Also move the `AddTopping` and `RemoveTopping` from `OrderState`. The result of all of this is that the `@functions` block of `ConfigurePizzaDialog.razor` should look the same as it did after completing step 2.
```html
@functions {
List<Topping> toppings { get; set; }
[Parameter] Pizza Pizza { get; set; }
[Parameter] Action OnCancel { get; set; }
[Parameter] Action OnConfirm { get; set; }
[Parameter] EventCallback OnCancel { get; set; }
[Parameter] EventCallback OnConfirm { get; set; }
protected async override Task OnInitAsync()
{
toppings = await HttpClient.GetJsonAsync<List<Topping>>("/toppings");
toppings = await HttpClient.GetJsonAsync<List<Topping>>("toppings");
}
void ToppingSelected(UIChangeEventArgs e)
......@@ -170,13 +165,13 @@ Let's add back the `Pizza`, `OnCancel`, and `OnConfirm` parameters. Also move th
}
```
Now we can remove `@inject OrderState OrderState` from the top of `ConfigurePizzaDialog.cshtml`.
Now we can remove `@inject OrderState OrderState` from the top of `ConfigurePizzaDialog.razor`.
Lastly, update the rest of this code to use the parameters and remove the lingering usage of `OrderState` from `ConfigurePizzaDialog.cshtml`. At this point the code should compile, but it will not run correctly because we haven't updated `Index.cshtml` yet.
Lastly, update the rest of this code to use the parameters and remove the lingering usage of `OrderState` from `ConfigurePizzaDialog.razor`. At this point the code should compile, but it will not run correctly because we haven't updated `Index.razor` yet.
----
Recall that our `TemplatedDialog` contains a few `div`s. Well, this duplicates some of the structure of `ConfigurePizzaDialog`. Let's clean that up. Open `ConfigurePizzaDialog.cshtml`; it currently looks like:
Recall that our `TemplatedDialog` contains a few `div`s. Well, this duplicates some of the structure of `ConfigurePizzaDialog`. Let's clean that up. Open `ConfigurePizzaDialog.razor`; it currently looks like:
```html
<div class="dialog-container">
......@@ -212,7 +207,7 @@ We should remove the outermost two layers of `div` elements since those are now
## Using the new dialog
We'll use this new templated component from `Index.cshtml`. Open the `Index.cshtml` and find the block of code that looks like:
We'll use this new templated component from `Index.razor`. Open the `Index.razor` and find the block of code that looks like:
```html
@if (OrderState.ShowingConfigureDialog)
......@@ -240,13 +235,13 @@ At this point it should be possible to run the code and see that the new dialog
## A more advanced templated component
Now that we've done a basic templated dialog, we're going to try something more sophisticated. Recall that the `MyOrders.cshtml` page has shows a list of orders, but it also contains three-state logic (loading, empty list, and showing items). If we could extract that logic into a reusable component, would that be useful? Let's give it a try.
Now that we've done a basic templated dialog, we're going to try something more sophisticated. Recall that the `MyOrders.razor` page has shows a list of orders, but it also contains three-state logic (loading, empty list, and showing items). If we could extract that logic into a reusable component, would that be useful? Let's give it a try.
Start by creating a new file `TemplatedList.cshtml` in the `BlazingComponents` project. We want this list to have a few features:
Start by creating a new file `TemplatedList.razor` in the `BlazingComponents` project. We want this list to have a few features:
1. Async-loading of any type of data
1. 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.cshtml`.
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`.
```html
@typeparam TItem
......@@ -271,7 +266,7 @@ Now that we've defined by a generic type parameter we can use it in a parameter
}
```
Since we have the data, we can now add the structure of each of the states we need to handle. Add the following markup to `TemplatedList.cshtml`:
Since we have the data, we can now add the structure of each of the states we need to handle. Add the following markup to `TemplatedList.razor`:
```html
@if (items == null)
......@@ -332,9 +327,9 @@ else
The `ItemContent` accepts a parameter, and the way to deal with this is just to invoke the function. The result of invoking a `RenderFragment<T>` is another `RenderFragment` which can be rendered directly.
The new component should compile at this point, but there's still one thing we want to do. We want to be able to style the `<div class="list-group">` with another class, since that's what `MyOrders.cshtml` is doing. Adding small extensibiliy points to plug in additional css classes can go a long way for reusability.
The new component should compile at this point, but there's still one thing we want to do. We want to be able to style the `<div class="list-group">` with another class, since that's what `MyOrders.razor` is doing. Adding small extensibiliy points to plug in additional css classes can go a long way for reusability.
Let's add another `string` parameter, and finally the functions block of `TemplatedList.cshtml` should look like:
Let's add another `string` parameter, and finally the functions block of `TemplatedList.razor` should look like:
```html
@functions {
......@@ -353,7 +348,7 @@ Let's add another `string` parameter, and finally the functions block of `Templa
}
```
Lastly update the `<div class="list-group">` to contain `<div class="list-group @ListGroupClass">`. The complete file of `TemplatedList.cshtml` should now look like:
Lastly update the `<div class="list-group">` to contain `<div class="list-group @ListGroupClass">`. The complete file of `TemplatedList.razor` should now look like:
```html
@typeparam TItem
......@@ -396,7 +391,7 @@ else
## Using TemplatedList
To use the new `TemplatedList` component, we're going to edit `MyOrders.cshtml`.
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.OnInitAsync` and changing the method signature. The `@functions` block should look something like:
......@@ -404,7 +399,7 @@ First, we need to create a delegate that we can pass to the `TemplatedList` that
@functions {
Task<List<OrderWithStatus>> LoadOrders()
{
return HttpClient.GetJsonAsync<List<OrderWithStatus>>("/orders");
return HttpClient.GetJsonAsync<List<OrderWithStatus>>("orders");
}
}
```
......@@ -468,7 +463,7 @@ The `ItemContent` parameter is a `RenderFragment<T>` - which accepts a parameter
</TemplatedList>
```
Now we want to include all of the existing content from `MyOrders.cshtml`, so putting it all together should look more like the following:
Now we want to include all of the existing content from `MyOrders.razor`, so putting it all together should look more like the following:
```html
<TemplatedList Loader="@LoadOrders" ListGroupClass="orders-list">
......@@ -497,7 +492,7 @@ Now we want to include all of the existing content from `MyOrders.cshtml`, so pu
</TemplatedList>
```
Notice that we're also setting the `ListGroupClass` parameter to add the additional styling that was present in the original `MyOrders.cshtml`.
Notice that we're also setting the `ListGroupClass` parameter to add the additional styling that was present in the original `MyOrders.razor`.
There were a number of steps and new features to introduce here. Run this and make sure that it works correctly now that we're using the templated list.
......
......@@ -14,7 +14,7 @@ This is a rough guide of what topics are best to introduce with each section.
## 01 Components and layout
- Introduce @page - explain the difference between routable and non-routable
- Show the Router component in App.cshtml
- Show the Router component in App.razor
- Introduce @functions - this is an old feature but isn't commonly used in Razor. 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
......@@ -23,7 +23,7 @@ This is a rough guide of what topics are best to introduce with each section.
- Introduce http + JSON in Blazor (`GetJsonAsync`)
- Talk about async and the interaction with rendering
- Introduce `OnInitAsync` and the common pattern of starting async work
- Introduce @layout - mention that `_ViewImports.cshtml` is the most common way to hook it up
- Introduce @layout - mention that `_Imports.razor` is the most common way to hook it up
- Introduce NavLink and talk about various `NavLinkMatch` options
## 02 Customize a pizza
......
......@@ -2,17 +2,18 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<OutputType>Exe</OutputType>
<LangVersion>7.3</LangVersion>
<RazorLangVersion>3.0</RazorLangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Blazor.Browser" Version="0.7.0" />
<PackageReference Include="Microsoft.AspNetCore.Blazor.Build" Version="0.7.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Blazor" Version="3.0.0-preview5-19227-01" />
<PackageReference Include="Microsoft.AspNetCore.Blazor.Build" Version="3.0.0-preview5-19227-01" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Blazor.DevServer" Version="3.0.0-preview5-19227-01" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>