Prism のプロジェクト テンプレートに少し前から Prism Full App (.NET Core) という名前のプロジェクト テンプレートが追加されています。
このプロジェクト テンプレートを使うと Prism を開発している人が、特に何も制約が無ければこんな風にプロジェクトをわけて作るといいんじゃないかという形になっていると思うので、これを見て構造を理解して自分がプロジェクトを作るときの参考にしてみましょう。
もし、この構成に異論がない場合は、この Prism Full App プロジェクト テンプレートを自分が開発するときの開始地点として使うのもいいですね。
では見ていきましょう。
では早速 Prism Full App (.NET Core) をもとにプロジェクトを新規作成してみましょう。今回は WpfSampleFullApp という名前でプロジェクトを作りました。
このプロジェクトを作るだけで以下のように 6 個のプロジェクトが作られます。
WpfSampleFullApp をスタートアップ プロジェクトにして実行すると以下のように まさに Hello world のような見た目のアプリが起動します。
生成されたプロジェクト
では、1 つ 1 つプロジェクトを見ていきます。
WpfSampleFullApp プロジェクト
このプロジェクトがエントリーポイントになります。中身は非常にシンプルで App クラスと MainWindow と MainWindowViewModel があるだけです。
ここは特にいうところはないですね。
WpfSampleFullApp プロジェクトが何を参照しているか見ていきましょう。以下のプロジェクトを参照しています。
- WpfSampleFullApp.Core
- WpfSampleFullApp.modules.ModuleName
- WpfSampleFullApp.Services
- WpfsampleFullApp.Services.Interfaces
端的に言うと全部って感じですね。では、この App クラスでどのようにモジュールを追加したり DI コンテナにクラスを登録しているか確認してみましょう。App.xaml.cs は以下のような感じになっています。
using Prism.Ioc;
using WpfSampleFullApp.Views;
using System.Windows;
using Prism.Modularity;
using WpfSampleFullApp.Modules.ModuleName;
using WpfSampleFullApp.Services.Interfaces;
using WpfSampleFullApp.Services;
namespace WpfSampleFullApp
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App
{
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterSingleton<IMessageService, MessageService>();
}
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
moduleCatalog.AddModule<ModuleNameModule>();
}
}
}
最初に CreateShell メソッドを見てみます。これは特に説明するまでもないですが MainWindow がアプリケーションのシェルだよってことで単純に MainWindow のインスタンスを生成しています。
次に、ConfigureModuleCatalog メソッドを見てみましょう。単純に、今回のプロジェクトテンプレートで生成されている唯一のモジュールのプロジェクトを AddModule で追加していますね。
最後に RegisterTypes を見ていきます。
ここでは WpfSampleFullApp.Services.Interfaces で定義されたサービスのインターフェースと WpfSampleFullApp.Services で定義された実装クラスの紐づけですね。単純に DI コンテナに登録しています。
ということでまとめると、この WpfSampleFullApp プロジェクトは、ざっくりと以下のことをしています。
- モジュールの登録
- サービスのインターフェースと実装の紐づけ
- メインの Window の作成
WpfSampleFullApp.Core プロジェクト
このプロジェクトはモジュールクラスで皆が使う共通的なクラスが定義されています。ほかのプロジェクトには依存しないプロジェクトです。そして以下のクラスが定義されています。
- RegionNames クラス
- Mvvm/ViewModelBase クラス
- Mvvm/RegionViewModelBase クラス
RegionNames クラス
これは単純に MainWindow に定義された Region の名前を定義した定数です。画面遷移するときなどに Region の名前を指定するのですがハードコーディングしないように、ここに定数として定義されていて、他のプロジェクトからはこれを参照するようになっています。
新たな Region を定義したら名前はここに定数として追加しましょう。
Mvvm/ViewModelBase クラス
BindableBase を継承して IDestructible インターフェースを実装しているだけの、ほぼ空っぽのクラスです。
Mvvm/RegionViewModelBase クラス
これは名前の通り Region に設定する ViewModel の基本クラスです。
ViewModelBase を継承して INavigationAware, IConfirmNavigationRequest の 2 つのインターフェースを実装しています。
Region に設定する ViewModel は画面遷移関係の処理を書くことが多いので画面遷移前と後のコールバックなどを提供する INavigationAware と、画面遷移前の確認処理を書く IConfirmNavigationRequest インターフェースは、よく実装するやつなので、ここで実装して空実装を提供している感じですね。
他にここに追加すると思われるもの
例えばダイアログとして表示する View の ViewModel の基本クラスなんかも追加することになるでしょうね。
あとは EventAggregator でやり取りする PubSubEvent の継承クラスなんかも、ここに追加することになるでしょう。
WpfSampleFullApp.Services.Interfaces プロジェクト
ここにアプリケーションのモデル レイヤーのサービスが定義される感じです。依存しているプロジェクトはありません。もっというと Prism いも依存していません。
端的に言うと色々なモジュールの ViewModel レイヤーから使われるインターフェースの定義ですね。なのでモデル レイヤーが ViewModel に提供するインターフェースだけが定義される感じですね。
もし自分のモデルレイヤーはユースケースというものを提供するんだ!みたいな感じだとしたら WpfSampleFullApp.UseCases.Interfaces みたいになるのかな。
後は、混ぜるか別のプロジェクトに定義するのか微妙なラインですが、モデル レイヤーがモデル レイヤー以外に提供するインターフェースを定義するプロジェクトにするなら、DB や WebAPI にアクセスするクラスが実装するための Repository 系のインターフェースもここに定義することになるでしょう。文脈としては GetAll とか GetById とか Update, Save のような DB からみた文脈のインターフェースではなく、あくまでモデルが外部リソースに求める文脈で名前が付けられたインターフェースが定義されるような場所ですね。
デフォルトでは、何かメッセージを提供するだけの IMessageService インターフェースが定義されています。
WpfSampleFullApp.Services プロジェクト
ここに WpfSampleFullApp.Services.Interfaces プロジェクトで定義されたサービスのインターフェースの実装クラスが定義されます。これも Prism に依存していません。MessageService があるだけです。
参照しているプロジェクトは先ほどの WpfSampleFullApp.Services.Interfaces プロジェクトになります。
ここにモデル レイヤーのコア ロジック系が来る感じですね。
WpfSampleFullApp.Modules.ModuleName プロジェクト
これがモジュールのプロジェクトで、View や ViewModel が定義されています。ModuleName という残念な感じのプロジェクト名になっているので、実際にはこいつは消して Prism Module (.NET Core) プロジェクト テンプレートをベースに作り直すことになるでしょう。
ポイントとしては、WpfSampleApp.Modules.Services.Interfaces プロジェクトに依存しているだけで WpfSampleApp.Modules.Services プロジェクトには依存していないところです。あくまで依存先はインターフェースで実装ではないという点がポイントです。
実装とインターフェースの紐づけを知っているのは WpfSampleFullApp プロジェクトだけになります。
まとめてみよう
ということでこんな感じの依存関係になっています。
Web API 呼んでみよう
Web API というほど大したものじゃないですけどメッセージを Web 経由でとってくるようにしてみたいと思います。
具体的には以下の URL から返される JSON の message プロパティを今表示されている Hello from the Message Service のかわりに表示するようにします。
返される JSON
{
"message": "Hello from GitHub"
}
MessageService に直接 HttpClient 使った処理とか書いてもいいですが、テスト的にはいまいちなので外部リソースアクセスは Repository レイヤーを作りましょう。
WpfSampleFullApp.SErvices.Interfaces プロジェクトに以下のようなリポジトリ用インターフェースを定義します。
using System.Threading.Tasks;
namespace WpfSampleFullApp.Services.Interfaces.Repositories
{
public interface IMessageRepository
{
ValueTask<string> GetMessageAsync();
}
}
IMessageService も非同期になるので、あわせて変更しておきましょう。
using System.Threading.Tasks;
namespace WpfSampleFullApp.Services.Interfaces
{
public interface IMessageService
{
ValueTask<string> GetMessageAsync();
}
}
モジュールのプロジェクトの ViewAViewModel クラスを書き換えて以下のようにしました。コンストラクタでデータ読み込みとかはあまりしないと思いますが、今回はもとがそうなってたので最小限のコードの変更でということで以下のようにしました。
using WpfSampleFullApp.Core.Mvvm;
using WpfSampleFullApp.Services.Interfaces;
using Prism.Regions;
using System.Threading.Tasks;
namespace WpfSampleFullApp.Modules.ModuleName.ViewModels
{
public class ViewAViewModel : RegionViewModelBase
{
private string _message;
public string Message
{
get { return _message; }
set { SetProperty(ref _message, value); }
}
public ViewAViewModel(IRegionManager regionManager, IMessageService messageService) :
base(regionManager)
{
Message = "Loading...";
messageService.GetMessageAsync().AsTask().ContinueWith(x =>
{
Message = x.Result; // エラーはとりあえず考えない
});
}
public override void OnNavigatedTo(NavigationContext navigationContext)
{
//do something
}
}
}
そして WpfSampleFullApp.Repositories というプロジェクトを作って WpfSampleFullApp.Services.Interfaces プロジェクトへの参照を追加します。ここで先ほどの IMessageRepository の実装を行います。System.Text.Json への参照を追加してさくっと実装してしまいましょう。
using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using WpfSampleFullApp.Services.Interfaces.Repositories;
namespace WpfSampleFullApp.Repositories
{
public class MessageRepository : IMessageRepository
{
private readonly HttpClient _httpClient;
public MessageRepository(HttpClient httpClient)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
public async ValueTask<string> GetMessageAsync()
{
using var jsonStream = await _httpClient.GetStreamAsync(
"https://raw.githubusercontent.com/runceel/mockapi/master/message.json");
var result = await JsonSerializer.DeserializeAsync<MessageResult>(jsonStream,
new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
}
);
return result.Message;
}
}
public class MessageResult
{
public string Message { get; set; }
}
}
App.xaml.cs でリポジトリ クラスの実装を DI コンテナに追加する処理を追加します。WpfSampleFullApp プロジェクトに WpfSampleFullApp.Repositories プロジェクトへの参照を追加するのも忘れないようにしましょう。
using Prism.Ioc;
using WpfSampleFullApp.Views;
using System.Windows;
using Prism.Modularity;
using WpfSampleFullApp.Modules.ModuleName;
using WpfSampleFullApp.Services.Interfaces;
using WpfSampleFullApp.Services;
using System.Net.Http;
using WpfSampleFullApp.Services.Interfaces.Repositories;
using WpfSampleFullApp.Repositories;
namespace WpfSampleFullApp
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App
{
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
// 以下 2 行を追加
containerRegistry.RegisterSingleton<HttpClient>();
containerRegistry.RegisterSingleton<IMessageRepository, MessageRepository>();
containerRegistry.RegisterSingleton<IMessageService, MessageService>();
}
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
moduleCatalog.AddModule<ModuleNameModule>();
}
}
}
ここまで出来たら実行してみましょう。以下のように表示されるはずです。
いい感じですね。プロジェクトの依存関係的には以下のようになりました。
単体テスト
単体テストプロジェクトは、デフォルトだとモジュール向けのものが作られています。Services 系のプロジェクトもテストする場合は必要に応じてモデル レイヤー向けの単体テスト プロジェクトを追加するといいでしょう。
まとめ
Prism Full App (.NET Core) プロジェクトテンプレートを眺めてみて、ちょっとだけ処理追加したりしてみましたが、こいつをベースに色々追加していくのは個人的にはアリかなと思いました。
プロジェクト テンプレートが吐き出すコードにちょっと処理追加しただけですが、一応今回のコードは GitHub にあげてます。