サービスを追加
フェイクデータを表示するHeroesコンポーネントを作成しました。
リファクタリングします。
なにゆえにサービスクラスを作成するのか
テストのしやすさから考えると、アプリを構成する部品はそれぞれ関心が分離している方がよいです。
コンポーネントがデータの保存方法について関知しないようにします。
プロジェクトを分けるか分けないか
サービスクラスを作成するにあたり、プロジェクトをどのようにするかが悩ましいです。
今は以下の2つのプロジェクトがあります。
- BlazorTourOfHeroes WASMプロジェクト
- BlazorTourOfHeroes.Tests 上記のユニットテストプロジェクト
WASMプロジェクト以外からも使うのであれば、別プロジェクトを用意しないとですね。
今はプロジェクトを分けずに進めます。
HeroServiceを作成する
テスト時にモック化しやすいように、インターフェースとコンクリートクラスを作成します。
namespace BlazorTourOfHeroes.Service
{
public interface IHeroService
{
}
}
namespace BlazorTourOfHeroes.Service
{
public class HeroService : IHeroService
{
}
}
ヒーローデータを返すメソッドを作成する
実際には、WebサービスやLocalStorageからデータを返します。
ここではモックです。
List<Hero> GetHeroes();
public List<Hero> GetHeroes() => MockHeroes.Create();
DIコンテナへ登録する
コンポーネントヘインジェクトできるように、DIコンテナへ登録します。
DIコンテナへの登録は、Program.csファイルの中で行います。
builder.Services.AddSingleton<IHeroService, HeroService>();
Heroesコンポーネントクラスを更新する
作成したサービスを使うようにコンポーネントを更新します。
インスタンス変数を宣言のみにします。
private List<Hero> heroes;
HeroServiceをインジェクトする
DIコンテナへ登録したHeroServiceをコンポーネントで使用できるようにします。
@using BlazorTourOfHeroes.Service
@inject IHeroService HeroService
@inject の引数ひとつめが型、ふたつめが変数名です。
GetHeroesメソッドを追加する
サービスからヒーロー達を読み込むメソッドを追加します。
private void GetHeroes()
{
heroes = HeroService.GetHeroes();
}
作成したメソッドをOnInitializedメソッドから呼び出すようにする
コンポーネントの準備が整うと OnInitialized メソッドが呼ばれます。このなかでヒーロー達を読み込むようにします。
protected override void OnInitialized()
{
GetHeroes();
}
非同期化
現実世界のアプリでは、ヒーロー達の読み込みがいつ終わるかわかりません。
いつ終わるかわからないものを待つことはできないので、非同期化します。
HeroServiceを非同期にする
GetHeroesメソッドを変更してTaskを返すようにします。
Task<List<Hero>> GetHeroes();
public Task<List<Hero>> GetHeroes() =>
Task.FromResult(MockHeroes.Create());
awaitする
GetHeroesをGetHeroesAsyncに書き換えて、awaitするようにします。
private async Task GetHeroesAsync()
{
heroes = await HeroService.GetHeroes();
}
OnInitializedAsyncに書き換える
OnInitializedの非同期版であるOnInitialiedAsyncを使うようにします。
protected override async Task OnInitializedAsync()
{
await GetHeroesAsync();
}
メッセージを表示する
ここでは次のことを行います。
- Messageコンポーネントを作成し、画面下部にアプリケーションからのメッセージを表示します。
-
MessageServiceを作成します。 -
MessageServiceをHeroServiceヘインジェクトします。 -
HeroServiceがヒーロ達を読み込んだら、メッセージを表示するようにします。
Messageコンポーネントを作成する
dotnetコマンドを使ってMessageコンポーネントを作成します。
dotnet new razorcomponent -o BlazorTourOfHeroes/Shared -n Message
Indexページを編集して、Messageコンポーネントを表示するようにします。
<h1>Tour of Heroes</h1>
<Heroes></Heroes>
<Message></Message>
MessageServiceを作成する
インターフェースとコンクリートクラスを作成します。
using System;
using System.Collections.Generic;
namespace BlazorTourOfHeroes.Service
{
public interface IMessageService
{
IEnumerable<string> Messages { get; }
void Add(string message);
void Clear();
}
}
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コンテナへ登録します。
builder.Services.AddSingleton<IMessageService, MessageService>();
HeroServiceヘインジェクトする
コンポーネントではない場合は、コンストラクタインジェクションになります。
private readonly IMessageService messageService;
public HeroService(IMessageService messageService)
{
this.messageService = messageService;
}
HeroServiceからメッセージを送る
GetHeroesメソッドを変更します。
public Task<List<Hero>> GetHeroes()
{
// TODO: ヒーロー達を取得した __後で__ メッセージを送るようにする
messageService.Add("HeroService: fetched heroes");
return Task.FromResult(MockHeroes.Create());
}
HeroServiceからのメッセージを表示する
Messageコンポーネントを変更してメッセージを表示するようにします。
OnInitializedの中で、MessageServiceの状態変化を受け取るハンドラStateHasChangedを登録しています。
登録を解除できるように、IDisposableを実装しています。
@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コンポーネントにメッセージを追加する
ヒーロー選択時に、メッセージを追加するようにします。
private void OnSelect(Hero hero)
{
selectedHero = hero;
MessageService.Add($"HeroesComponent: Selected hero id={hero.Id}");
}
HeroServiceの状態変化をコンポーネントで受け取る
Heroesコンポーネントからメッセージを追加していますが、Messageコンポーネントに反映されません。
これを解決するには、2つやることがあります。
-
MessageServiceから状態変化を通知する。 - Messageコンポーネントで
MessageServiceの状態変化通知を受け取る。
HeroServiceから状態変化を通知する
状態変化を通知するイベントを定義します。
event Action OnChange;
状態が変化したとき、イベントハンドラを起動するようにします。
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のなかで登録解除します。
@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;
}
}
