Skip to content

Commit

Permalink
3.0 GA updates (#156)
Browse files Browse the repository at this point in the history
* Update to 3.0 GA versions

* Updates to authorization + routing

* Updates to section 06

* Use InvokeVoidAsync in Map.razor

* Use InvokeVoidAsync in LocalStorage.cs

* Updates to section 06

* Stop using `Content` as a suffix

We decided against doing this in Blazor's codebase so we shouldn't show
it here.

* Updates to section 08

* Remove inconsistent blank-line-ness

* Update docs/07-javascript-interop.md

Co-Authored-By: Steve Sanderson <[email protected]>

* Update docs/06-authentication-and-authorization.md

Co-Authored-By: Steve Sanderson <[email protected]>

* Update docs/08-templated-components.md

Co-Authored-By: Steve Sanderson <[email protected]>
  • Loading branch information
rynowak and SteveSandersonMS authored Sep 30, 2019
1 parent b19f57e commit be6f1f3
Show file tree
Hide file tree
Showing 31 changed files with 226 additions and 184 deletions.
6 changes: 3 additions & 3 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<AspNetCoreVersion>3.0.0-rc1.19457.4</AspNetCoreVersion>
<BlazorVersion>3.0.0-preview9.19457.4</BlazorVersion>
<EntityFrameworkVersion>3.0.0-rc1.19456.14</EntityFrameworkVersion>
<AspNetCoreVersion>3.0.0</AspNetCoreVersion>
<BlazorVersion>3.0.0-preview9.19465.2</BlazorVersion>
<EntityFrameworkVersion>3.0.0</EntityFrameworkVersion>
</PropertyGroup>
</Project>
54 changes: 33 additions & 21 deletions docs/06-authentication-and-authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ public void ConfigureServices(IServiceCollection services)
}
```

To flow the authentication state information through your app, you need to add one more component. In `App.razor`, surround the `<Router>` with a `<CascadingAuthenticationState>`:
To flow the authentication state information through your app, you need to add one more component. In `App.razor`, surround the entire `<Router>` with a `<CascadingAuthenticationState>`:

```html
<CascadingAuthenticationState>
<Router AppAssembly="typeof(Program).Assembly">
<NotFoundContent>Page not found</NotFoundContent>
<Router AppAssembly="typeof(Program).Assembly" Context="routeData">
...
</Router>
</CascadingAuthenticationState>
```
Expand Down Expand Up @@ -193,7 +193,7 @@ If you're now logged in, you'll be able to place orders and see order status. Bu

To fix this, let's make the UI prompt the user to log in (if necessary) as part of placing an order.

In the `Checkout` page component, add some logic to `OnInitializedAsync` to check whether the user is currently authenticated. If they aren't, send them off to the login endpoint.
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.

```cs
@code {
Expand Down Expand Up @@ -307,30 +307,42 @@ So, go to `MyOrders`, and and put the following directive at the top (just under
@attribute [Authorize]
```

Now, logged in users can reach the *My orders* page, but logged out users will see the message *Not authorized* instead. Verify you can see this working.

Finally, let's be a bit friendlier to logged out users. Instead of just saying *Not authorized*, we can customize this to display a link to sign in. Go to `App.razor`, and pass the following `<NotAuthorizedContent>` and `<AuthorizingContent>` parameters to the `<Router>`:
The `[Authorize]` functionality is part of the routing system, and we'll need to make some changes there. In `App.razor`, replace `<RouteView ../>` with `<AuthorizeRouteView .../>`.

```html
<CascadingAuthenticationState>
<Router AppAssembly="typeof(Program).Assembly">
<NotFoundContent>Page not found</NotFoundContent>

<NotAuthorizedContent>
<div class="main">
<h2>You're signed out</h2>
<p>To continue, please sign in.</p>
<a class="btn btn-danger" href="user/signin">Sign in</a>
</div>
</NotAuthorizedContent>

<AuthorizingContent>
Please wait...
</AuthorizingContent>
<Router AppAssembly="typeof(Program).Assembly" Context="routeData">
<Found>
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
</Found>
...
</Router>
</CascadingAuthenticationState>
```

The `AuthorizeRouteView` component is like `RouteView` in that it can display a routable component and it's layout, but also integrates with `[Authorize]`.

---

Now, logged in users can reach the *My orders* page, but logged out users will see the message *Not authorized* instead. Verify you can see this working.

Finally, let's be a bit friendlier to logged out users. Instead of just saying *Not authorized*, we can customize this to display a link to sign in. Go to `App.razor`, and pass the following `<NotAuthorized>` and `<Authorizing>` parameters to the `<AuthorizeRouteView>`:

```html
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)">
<NotAuthorized>
<div class="main">
<h2>You're signed out</h2>
<p>To continue, please sign in.</p>
<a class="btn btn-danger" href="user/signin">Sign in</a>
</div>
</NotAuthorized>
<Authorizing>
Please wait...
</Authorizing>
</AuthorizeRouteView>
```

Now if you're logged out and try to go to *My orders*, you'll get a much nicer outcome:

![image](https://user-images.githubusercontent.com/1101362/51807840-11225180-2284-11e9-81ed-ea9caacb79ef.png)
Expand Down
12 changes: 8 additions & 4 deletions docs/07-javascript-interop.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,23 @@ Open *Map.razor* and take a look at the code:

protected async override Task OnAfterRenderAsync(bool firstRender)
{
await JSRuntime.InvokeAsync<object>(
await JSRuntime.InvokeVoidAsync(
"deliveryMap.showOrUpdate",
elementId,
Markers);
}
}
```

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 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 *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 *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? 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.
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.

The final link is for the page hosting the Blazor client app to include the desired files (in our case `.js` and `.css`). The `index.html` includes these files using relative URIs like `_content/BlazingPizza.ComponentsLibrary/localStorage.js`. This is the general pattern for references files bundled with a Blazor class library - `_content/<library name>/<file path>`.

---

If you start typing in `Map`, you'll notice that the editor doesn't offer completion for it. This is because the binding between elements and components are governed by C#'s namespace binding rules. The `Map` component is defined in the `BlazingPizza.ComponentsLibrary.Map` namespace, which we don't have an `@using` for.

Expand Down
92 changes: 50 additions & 42 deletions docs/08-templated-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,28 @@

Let's refactor some of the original components and make them more reusable. Along the way we'll also create a separate library project as a home for the new components.

## Creating a component library (command line)
We're going to create a new project using the Razor Class Library template.

## Creating a component library (Visual Studio)

Using Visual Studio, right click the solution explorer and choose `Add->New Project`.

Then, select the Razor Class Library template.

We're going to create a new project using the **dotnet** cli in this step since the Razor Class Library template in Visual Studio does not yet have all of the settings we want.
![image](https://user-images.githubusercontent.com/1430011/65823337-17990c80-e209-11e9-9096-de4cb0d720ba.png)

Enter the project name `BlazingComponents` and click *Create*.

## Creating a component library (command line)

To make a new project using **dotnet** run the following commands from the directory where your solution file exists.

```
dotnet new razorclasslib -o BlazingComponents --support-pages-and-views false
dotnet new razorclasslib -o BlazingComponents
dotnet sln add BlazingComponents
```

This should create a new project called `BlazingComponents` and add it to the solution file. This is the same template used to create libraries of standlone Razor Pages - but with an option to use default settings for Blazor and components.
This should create a new project called `BlazingComponents` and add it to the solution file.

## Understanding the library project

Expand All @@ -30,8 +40,8 @@ It looks like:
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Components" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="3.0.0" />
</ItemGroup>

</Project>
Expand All @@ -51,7 +61,7 @@ We are going to revisit the dialog system that is part of `Index` and turn it in

Let's think about how a *reusable dialog* should work. We would expect a dialog component to handle showing and hiding itself, as well as maybe styling to appear visually as a dialog. However, to be truly reusable, we need to be able to provide the content for the inside of the dialog. We call a component that accepts *content* as a parameter a *templated component*.

Blazor happens to have a feature that works for exactly this case, and 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`.
Blazor happens to have a feature that works for exactly this case, and 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`. We've also used this feature extensively in `App.razor`. All of the components used to handle routing and authorization are templated components.

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`:

Expand Down Expand Up @@ -242,38 +252,36 @@ Now, these are our three states of the dialog, and we'd like accept a content pa
Here's an example of the three parameters to add:

```C#
[Parameter] public RenderFragment LoadingContent { get; set; }
[Parameter] public RenderFragment EmptyContent { get; set; }
[Parameter] public RenderFragment<TItem> ItemContent { get; set; }
[Parameter] public RenderFragment Loading{ get; set; }
[Parameter] public RenderFragment Empty { get; set; }
[Parameter] public RenderFragment<TItem> Item { get; set; }
```

note: naming a `RenderFragment` parameter with the suffix *Content* is just a convention.

Now that we have some `RenderFragment` parameters, we can start using them. Update the markup we created earlier to plug in the correct parameter in each place.

```html
@if (items == null)
{
@LoadingContent
@Loading
}
else if (items.Count == 0)
{
@EmptyContent
@Empty
}
else
{
<div class="list-group">
@foreach (var item in items)
{
<div class="list-group-item">
@ItemContent(item)
@Item(item)
</div>
}
</div>
}
```

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 `Item` 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.razor` is doing. Adding small extensibiliy points to plug in additional css classes can go a long way for reusability.

Expand All @@ -284,9 +292,9 @@ Let's add another `string` parameter, and finally the functions block of `Templa
List<TItem> items;

[Parameter] public Func<Task<List<TItem>>> Loader { get; set; }
[Parameter] public RenderFragment LoadingContent { get; set; }
[Parameter] public RenderFragment EmptyContent { get; set; }
[Parameter] public RenderFragment<TItem> ItemContent { get; set; }
[Parameter] public RenderFragment Loading { get; set; }
[Parameter] public RenderFragment Empty { get; set; }
[Parameter] public RenderFragment<TItem> Item { get; set; }
[Parameter] public string ListGroupClass { get; set; }

protected override async Task OnParametersSetAsync()
Expand All @@ -303,19 +311,19 @@ Lastly update the `<div class="list-group">` to contain `<div class="list-group

@if (items == null)
{
@LoadingContent
@Loading
}
else if (items.Count == 0)
{
@EmptyContent
@Empty
}
else
{
<div class="list-group @ListGroupClass">
@foreach (var item in items)
{
<div class="list-group-item">
@ItemContent(item)
@Item(item)
</div>
}
</div>
Expand All @@ -325,9 +333,9 @@ else
List<TItem> items;

[Parameter] public Func<Task<List<TItem>>> Loader { get; set; }
[Parameter] public RenderFragment LoadingContent { get; set; }
[Parameter] public RenderFragment EmptyContent { get; set; }
[Parameter] public RenderFragment<TItem> ItemContent { get; set; }
[Parameter] public RenderFragment Loading { get; set; }
[Parameter] public RenderFragment Empty { get; set; }
[Parameter] public RenderFragment<TItem> Item { get; set; }
[Parameter] public string ListGroupClass { get; set; }

protected override async Task OnParametersSetAsync()
Expand Down Expand Up @@ -394,29 +402,29 @@ For our `TemplatedList` here's an example that sets each parameter to some dummy
```html
<div class="main">
<TemplatedList Loader="@LoadOrders">
<LoadingContent>Hi there!</LoadingContent>
<EmptyContent>
<Loading>Hi there!</Loading>
<Empty>
How are you?
</EmptyContent>
<ItemContent>
</Empty>
<Item>
Are you enjoying Blazor?
</ItemContent>
</Item>
</TemplatedList>
</div>
```

The `ItemContent` parameter is a `RenderFragment<T>` - which accepts a parameter. By default this parameter is called `context`. If we type inside of `<ItemContent> </ItemContent>` then it should be possible to see that `@context` is bound to a variable of type `OrderStatus`. We can rename the parameter by using the `Context` attribute:
The `Item` parameter is a `RenderFragment<T>` - which accepts a parameter. By default this parameter is called `context`. If we type inside of `<Item> </Item>` then it should be possible to see that `@context` is bound to a variable of type `OrderStatus`. We can rename the parameter by using the `Context` attribute:

```html
<div class="main">
<TemplatedList Loader="@LoadOrders">
<LoadingContent>Hi there!</LoadingContent>
<EmptyContent>
<Loading>Hi there!</Loading>
<Empty>
How are you?
</EmptyContent>
<ItemContent Context="item">
</Empty>
<Item Context="item">
Are you enjoying Blazor?
</ItemContent>
</Item>
</TemplatedList>
</div>
```
Expand All @@ -426,12 +434,12 @@ Now we want to include all of the existing content from `MyOrders.razor`, so put
```html
<div class="main">
<TemplatedList Loader="@LoadOrders" ListGroupClass="orders-list">
<LoadingContent>Loading...</LoadingContent>
<EmptyContent>
<Loading>Loading...</Loading>
<Empty>
<h2>No orders placed</h2>
<a class="btn btn-success" href="">Order some pizza</a>
</EmptyContent>
<ItemContent Context="item">
</Empty>
<Item Context="item">
<div class="col">
<h5>@item.Order.CreatedTime.ToLongDateString()</h5>
Items:
Expand All @@ -447,7 +455,7 @@ Now we want to include all of the existing content from `MyOrders.razor`, so put
Track &gt;
</a>
</div>
</ItemContent>
</Item>
</TemplatedList>
</div>
```
Expand All @@ -458,7 +466,7 @@ There were a number of steps and new features to introduce here. Run this and ma

To prove that the list is really working correctly we can try the following:
1. Delete the `pizza.db` from the `Blazor.Server` project to test the case where there are no orders
1. Add an `await Task.Delay(3000);` to `LoadOrders` to test the case where we're still loading
2. Add an `await Task.Delay(3000);` to `LoadOrders` to test the case where we're still loading

## Summary

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ public static class LocalStorage
public static ValueTask<T> GetAsync<T>(IJSRuntime jsRuntime, string key)
=> jsRuntime.InvokeAsync<T>("blazorLocalStorage.get", key);

public static ValueTask<object> SetAsync(IJSRuntime jsRuntime, string key, object value)
=> jsRuntime.InvokeAsync<object>("blazorLocalStorage.set", key, value);
public static ValueTask SetAsync(IJSRuntime jsRuntime, string key, object value)
=> jsRuntime.InvokeVoidAsync("blazorLocalStorage.set", key, value);

public static ValueTask<object> DeleteAsync(IJSRuntime jsRuntime, string key)
=> jsRuntime.InvokeAsync<object>("blazorLocalStorage.delete", key);
public static ValueTask DeleteAsync(IJSRuntime jsRuntime, string key)
=> jsRuntime.InvokeVoidAsync("blazorLocalStorage.delete", key);
}
}
Loading

0 comments on commit be6f1f3

Please sign in to comment.