[Microsoft] 4. サービスを追加 - Angularチュートリアル Tour of Heroes を Blazor で再実装する

# サービスを追加

[フェイクデータを表示するHeroesコンポーネントを作成](https://qiita.com/sengoku/items/08bd0aa23adf88a6bdb8)しました。
リファクタリングします。

----


# なにゆえにサービスクラスを作成するのか

テストのしやすさから考えると、アプリを構成する部品はそれぞれ関心が分離している方がよいです。
コンポーネントがデータの保存方法について関知しないようにします。

# プロジェクトを分けるか分けないか

サービスクラスを作成するにあたり、プロジェクトをどのようにするかが悩ましいです。

今は以下の2つのプロジェクトがあります。

* BlazorTourOfHeroes  WASMプロジェクト
* BlazorTourOfHeroes.Tests 上記のユニットテストプロジェクト

WASMプロジェクト以外からも使うのであれば、別プロジェクトを用意しないとですね。
今はプロジェクトを分けずに進めます。

# `HeroService`を作成する

テスト時にモック化しやすいように、インターフェースとコンクリートクラスを作成します。

```cs:Service/IHeroService.cs
namespace BlazorTourOfHeroes.Service
{
    public interface IHeroService
    {
    }
}
```

```cs:Service/HeroService.cs
namespace BlazorTourOfHeroes.Service
{
    public class HeroService : IHeroService
    {
    }
}
```


## ヒーローデータを返すメソッドを作成する

実際には、WebサービスやLocalStorageからデータを返します。
ここではモックです。

```cs:IHeroService.cs
List<Hero> GetHeroes();
```

```cs:HeroService.cs
public List<Hero> GetHeroes() => MockHeroes.Create();
```



# DIコンテナへ登録する

コンポーネントヘインジェクトできるように、DIコンテナへ登録します。

DIコンテナへの登録は、`Program.cs`ファイルの中で行います。

```cs:Program.cs
builder.Services.AddSingleton<IHeroService, HeroService>();
```

# Heroesコンポーネントクラスを更新する

作成したサービスを使うようにコンポーネントを更新します。

インスタンス変数を宣言のみにします。

```cs:Heroes.razor
private List<Hero> heroes;
```


## `HeroService`をインジェクトする

DIコンテナへ登録した`HeroService`をコンポーネントで使用できるようにします。

```razor:Heroes.razor
@using BlazorTourOfHeroes.Service
@inject IHeroService HeroService
```

`@inject` の引数ひとつめが型、ふたつめが変数名です。

## `GetHeroes`メソッドを追加する

サービスからヒーロー達を読み込むメソッドを追加します。

```cs:Heroes.razor
private void GetHeroes()
{
    heroes = HeroService.GetHeroes();
}
```

## 作成したメソッドを`OnInitialized`メソッドから呼び出すようにする

コンポーネントの準備が整うと [OnInitialized](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitialized?view=aspnetcore-3.1) メソッドが呼ばれます。このなかでヒーロー達を読み込むようにします。

```cs:Hero.razor
protected override void OnInitialized()
{
    GetHeroes();
}
```


# 非同期化

現実世界のアプリでは、ヒーロー達の読み込みがいつ終わるかわかりません。
いつ終わるかわからないものを待つことはできないので、非同期化します。

## `HeroService`を非同期にする

`GetHeroes`メソッドを変更して`Task`を返すようにします。

```cs:IHeroService.cs
Task<List<Hero>> GetHeroes();
```

```cs:HeroService.cs
public Task<List<Hero>> GetHeroes() =>
    Task.FromResult(MockHeroes.Create());
```

## `await`する

`GetHeroes`を`GetHeroesAsync`に書き換えて、`await`するようにします。

```cs:Heroes.razor
private async Task GetHeroesAsync()
{
    heroes = await HeroService.GetHeroes();
}
```

## `OnInitializedAsync`に書き換える

`OnInitialized`の非同期版である`OnInitialiedAsync`を使うようにします。

```cs:Heroes.razor
protected override async Task OnInitializedAsync()
{
    await GetHeroesAsync();
}
```

# メッセージを表示する

ここでは次のことを行います。

* Messageコンポーネントを作成し、画面下部にアプリケーションからのメッセージを表示します。
* `MessageService`を作成します。
* `MessageService`を`HeroService`ヘインジェクトします。
* `HeroService`がヒーロ達を読み込んだら、メッセージを表示するようにします。


## Messageコンポーネントを作成する

dotnetコマンドを使ってMessageコンポーネントを作成します。

```console
dotnet new razorcomponent -o BlazorTourOfHeroes/Shared -n Message
```

Indexページを編集して、Messageコンポーネントを表示するようにします。

```razor:Index.razor
<h1>Tour of Heroes</h1>

<Heroes></Heroes>
<Message></Message>
```

## `MessageService`を作成する

インターフェースとコンクリートクラスを作成します。

```cs:IMessageService.cs
using System;
using System.Collections.Generic;

namespace BlazorTourOfHeroes.Service
{
    public interface IMessageService
    {
        IEnumerable<string> Messages { get; }
        void Add(string message);
        void Clear();
    }
}
```

```cs:MessageService.cs
using System;
using System.Collections.Generic;

namespace BlazorTourOfHeroes.Service
{
    public class MessageService : IMessageService
    {
        private readonly List<string> messages = new List<string>();

        public IEnumerable<string> Messages
        {
            get
            {
                return messages;
            }
        }

        public void Add(string message)
        {
            messages.Add(message);
        }

        public void Clear()
        {
            messages.Clear();
        }
    }
}
```

## `MessageService`をDIコンテナへ登録する

DIコンテナへ登録します。

```cs:Program.cs
builder.Services.AddSingleton<IMessageService, MessageService>();
```

## `HeroService`ヘインジェクトする


コンポーネントではない場合は、コンストラクタインジェクションになります。

```cs:HeroService.cs
private readonly IMessageService messageService;

public HeroService(IMessageService messageService)
{
    this.messageService = messageService;
}
```

## `HeroService`からメッセージを送る

`GetHeroes`メソッドを変更します。

```cs:HeroesService.cs
public Task<List<Hero>> GetHeroes()
{
    // TODO: ヒーロー達を取得した __後で__ メッセージを送るようにする
    messageService.Add("HeroService: fetched heroes");
    return Task.FromResult(MockHeroes.Create());
}
```

## `HeroService`からのメッセージを表示する

Messageコンポーネントを変更してメッセージを表示するようにします。

`OnInitialized`の中で、`MessageService`の状態変化を受け取るハンドラ`StateHasChanged`を登録しています。
登録を解除できるように、`IDisposable`を実装しています。

```razor:Message.razor
@using BlazorTourOfHeroes.Service
@inject IMessageService MessageService

@if (MessageService.Messages.Count() > 0) {
    <h2>Messages</h2>
    <button class="clear" @onclick="MessageService.Clear">Clear</button>
    @foreach (var message in MessageService.Messages)
    {
        <div>@message</div>
    }
}
```

# Heroコンポーネントにメッセージを追加する

ヒーロー選択時に、メッセージを追加するようにします。

```cs:Heroes.razor
private void OnSelect(Hero hero)
{
    selectedHero = hero;
    MessageService.Add($"HeroesComponent: Selected hero id={hero.Id}");
}
```

## `HeroService`の状態変化をコンポーネントで受け取る

Heroesコンポーネントからメッセージを追加していますが、Messageコンポーネントに反映されません。
これを解決するには、2つやることがあります。

* `MessageService`から状態変化を通知する。
* Messageコンポーネントで`MessageService`の状態変化通知を受け取る。


## `HeroService`から状態変化を通知する

状態変化を通知するイベントを定義します。

```cs:IMessageService.cs
event Action OnChange;
```

状態が変化したとき、イベントハンドラを起動するようにします。

```cs:MessageService.cs
public event Action OnChange;

public void Add(string message)
{
    messages.Add(message);
    NotifyChange();
}

public void Clear()
{
    messages.Clear();
    NotifyChange();
}

// ハンドラが登録されていれば変更を通知
private void NotifyChange() => OnChange?.Invoke();
```

## Messageコンポーネントで`MessageService`の状態変化通知を受け取る

`OnInitialized`のなかで、`StateHasChanged`ハンドラを登録します。
コンポーネント廃棄時にハンドラを登録解除するため、`IDisposable`を実装します。
`Dispose`のなかで登録解除します。

```razor:Message.razor
@using BlazorTourOfHeroes.Service
@implements IDisposable
@inject IMessageService MessageService

@if (MessageService.Messages.Count() > 0) {
    <h2>Messages</h2>
    <button class="clear" @onclick="MessageService.Clear">Clear</button>
    @foreach (var message in MessageService.Messages)
    {
        <div>@message</div>
    }
}

@code {
    protected override void OnInitialized()
    {
        MessageService.OnChange += StateHasChanged;
    }

    public void Dispose()
    {
        MessageService.OnChange -= StateHasChanged;
    }
}
```


# こんなんできました



![toh.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59475/e4a8edf3-23db-c51e-5f98-718368561de3.gif)