サービスを追加
フェイクデータを表示する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;
}
}