[Microsoft] 5. ルーティング を用いたアプリ内での画面遷移 - Angularチュートリアル Tour of Heroes を Blazor で再実装する

# ルーティング を用いたアプリ内での画面遷移



* _ダッシュボード_ビューを追加します。
* _ヒーロー_ビューと、_ダッシュボード_ビュー間で画面遷移させます。
* ヒーロー名クリックで詳細画面へ遷移させます。
* アプリの外からアプリのURLを開いたとき、該当の画面を表示させます。

# Blazorのルーティング

Angularには、モジュール内での画面遷移を設定するためのルーティングモジュールがありました。
Blazorは、コンポーネントの先頭に `@page "パス"` と書くだけでルーティングできます。


## `Heroes`コンポーネントをページにする

`Heroes.razor`ファイルを移動します（移動しなくてもよいです）。

```console
git mv BlazorTourOfHeroes/Shared/Heroes.razor BlazorTourOfHeroes/Pages/Heroes.razor
```

ファイルの先頭に`@page`を追加してパスを設定します。

```razor:Heroes.razor
@page "/heroes"
```

# トップページにリンクを追加する

トップページを書き換えて`Heroes`コンポーネントへのリンクを追加します。

リンクは`<NavLink>`タグを使います。

```html:index.razor
@page "/"

<h1>Tour of Heroes</h1>

<nav>
    <NavLink href="heroes">Heroes</NavLink>
</nav>
<Message></Message>
```

# ダッシュボードビューを追加する

Pagesディレクトリ以下にコンポーネントを作成します。

```console
dotnet new razorcomponent -o BlazorTourOfHeroes/Pages -n Dashboard
```

```razor:Dashboard.razor
@page "/dashboard"
@using BlazorTourOfHeroes.Model
@using BlazorTourOfHeroes.Service
@inject IHeroService HeroService

<h3>Top Heroes</h3>
<div class="grid grid-pad">
    @foreach (var hero in heroes)
    {
      <a class="col-1-4">
        <div class="module hero">
        <h4>@hero.Name</h4>
        </div>
    </a>
    }
</div>

@code {
    private IEnumerable<Hero> heroes;

    protected override async Task OnInitializedAsync()
    {
        await GetHeroesAsync();
    }

    private async Task GetHeroesAsync()
    {
        heroes = (await HeroService.GetHeroes()).Take(5);
    }
}
```

内容は`Heroes`コンポーネントととほぼほぼ同じです。

LINQを使用して、先頭5つのみを取得するようにしています。

```c#
private async Task GetHeroesAsync()
{
    heroes = (await HeroService.GetHeroes()).Take(5);
}
```

## ダッシュボードへのパス

先頭にある`@page`で決めています。

```razor
@page "/dashboard"
```

## トップページにリンクを追加する

トップページを書き換えて、ダッシュボードへのリンクを追加します。

```html:index.razor
<h1>Tour of Heroes</h1>

<nav>
    <NavLink href="dashboard">Dashboard</NavLink>
    <NavLink href="heroes">Heroes</NavLink>
</nav>
<Message></Message>
```


# ヒーロー詳細へのナビゲーション

以下の箇所からヒーロー詳細を表示できるようにします。

* ダッシュボードのヒーロー
* ヒーロー一覧のヒーロー
* アドレスバーへ入力したURL

## `Heroes`コンポーネントからヒーロー詳細を削除する

`Heroes`コンポーネントから`<HeroDetail>`タグを削除します。

## ヒーロー詳細のパスを決める

`HeroDetail`コンポーネントを移動します（しなくてもよいです）。

```console
git mv BlazorTourOfHeroes/Shared/HeroDetail.razor BlazorTourOfHeroes/Pages/HeroDetail.razor
```

`@page`を追加します。`Id:int`として、int型以外を受け取らないようにします。

```razor:HeroDetail.razor
@page "/detail/{Id:int}"
```

## `Dashboard`コンポーネントからのリンク

URLにパラメータを含めるようにします。

```razor:Pages/Dashboard.razor
@foreach (var hero in heroes)
{
  <NavLink href="@("detail/" + hero.Id)" class="col-1-4">
    <div class="module hero">
    <h4>@hero.Name</h4>
    </div>
  </NavLink>
}
```


## `Heroes`コンポーネントからのリンク

`Heroes`コンポーネントのヒーロー要素は、以下のようにクリックイベント付きの`<li>`になっています。置き換えます。

```razor:Pages/Heroes.razor
@foreach (var hero in heroes)
{
    <li @onclick="@(_ => OnSelect(hero))"
        class="@(selectedHero == hero ? "selected" : "")"
        ><span class="badge">@hero.Id</span> @hero.Name
    </li>    
}
```

`foreach`と`<li>`はそのままにリンクにします。

```razor:Pages/Heroes.razor
@foreach (var hero in heroes)
{
    <li>
        <NavLink href="@("detail/" + hero.Id)">
        <span class="badge">@hero.Id</span> @hero.Name
        </NavLink>
    </li>    
}
```

### 不要になったコードを削除する（しなくてもよいです）

`Heroes`コンポーネントの`onSelect`周りの処理は不要になったので削除します。

```c#:Pages/Heroes.razor
private List<Hero> heroes;

private async Task GetHeroesAsync()
{
    heroes = await HeroService.GetHeroes();
}

protected override async Task OnInitializedAsync()
{
    await GetHeroesAsync();
}
```


# `HeroDetail`コンポーネントへルーティングできるようにする

`Heroes`コンポーネントから `HeroDetail`コンポーネントの`hero`プロパティをセットしてヒーローを表示するようにしていました。
`HeroDetail`コンポーネントを変更し、URL中のID値からヒーローを表示するようにします。

`HeroDetail`コンポーネントで`HeroService`を使えるようにします。

```razor:Pages/HeroDetail.razor
@using BlazorTourOfHeroes.Service
@inject IHeroService HeroService
```

## ルートパラメータからIDを受け取る

プロパティに`[Parameter]`をつけると、ルートパラメータを受け取れます。

```c#:HeroDetail.razor
[Parameter]
public int Id { get; set; }
```

受け取ったIDからヒーローを読み込みます。

```c#:Pages/HeroDetail.razor
Hero Hero { get; set; }

protected override async Task OnInitializedAsync()
{
    await GetHeroAsync();
}

private async Task GetHeroAsync()
{
    this.Hero = await HeroService.GetHeroAsync(this.Id);
}
```

## `HeroService`に`GetHeroAsync`メソッドを追加する

IDから単一のヒーローを返すメソッドを追加します。

```c#:Service/HeroService.cs
public Task<Hero> GetHeroAsync(int id)
{
    // TODO: ヒーロー達を取得した __後で__ メッセージを送るようにする
    messageService.Add($"HeroService: fetched hero id={id}");
    var hero = MockHeroes.Create().FirstOrDefault(x => x.Id == id);
    return Task.FromResult(hero);
}
```

### こんなんできました

ブラウザのアドレスバーに `http://localhost:5000/detail/11` と入力します。

Dr. Niceが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59475/1949b508-083e-4a21-8867-3e3710a183d1.png)

## 前画面に戻れるようにする

詳細から一覧やダッシュボードに戻れるようにします。

「戻る」ボタンを追加します。

```razor:Pages/HeroDetail.razor
<button @onclick="GoBackAsync">戻る</button>
```

[AngularのようなLocationサービスはない](https://github.com/dotnet/aspnetcore/issues/12838)ようなので、いつものJavaScript `window.history.back(-1)` を呼ぶようにします。

`JSRuntime`をインジェクトします。

```razor:Pages/HeroDetail.razor
@inject IJSRuntime JSRuntime
```

`window.history.back(-1)`を呼ぶようにします。

```c#:Pages/HeroDetail.razor
private async void GoBackAsync()
{
    await JSRuntime.InvokeVoidAsync("history.back", -1);
}
```