課題
Dependency Injection(DI・依存性の注入・依存オブジェクト注入)は、MVVMに必須ではないのですが、MVVMと相性がよく、使わないと不便な場合も多いです。
典型的なのが、MessageBoxを表示する処理です。
ModelやViewModelで
MessageBox.Show("message");
を行うとしましょう。
この場合の課題は、
- これは、Viewを表示する処理。従って、MVVMとしては不適。という考え方があります。
- UnitTestを実行しても、ここで処理がストップするので、UnitTestができない。
以上の課題を、まるっと解決するのが、DIコンテナでMessageBoxの表示をサービス化する方法です。
サンプルプロジェクト
.NET 8
この記事は、次のMessageBox for WPF の仕組みの解説を兼ねています。
サービス化の方法
ここでは、DIコンテナに、CommunityToolkit.Mvvm.DependencyInjection
を利用してみましょう。
アプリの起動時、IocのコンテナにDialogService
をIDialogService
とセットで登録します。
Ioc.Default.ConfigureServices(
new ServiceCollection()
.AddTransient<IDialogService, DialogService>()
.BuildServiceProvider());
DialogService
public class DialogService : IDialogService
{
public string WindowGuid { get; set; } = Guid.NewGuid().ToString("N");
public MessageBoxResult Show(string message, string caption, MessageBoxButton messageBoxButton, MessageBoxImage messageBoxImage)
{
return MessageBox.Show(message, caption, messageBoxButton, messageBoxImage);
}
public MessageBoxResult ShowOnWindow(string message, string caption, MessageBoxButton messageBoxButton, MessageBoxImage messageBoxImage)
{
var w = GetWindow();
if (w is not null)
{
w.Opacity = 0.7;
var r = MessageBox.Show(w, message, caption, messageBoxButton, messageBoxImage);
w.Opacity = 1;
return r;
}
else
return MessageBox.Show(message, caption, messageBoxButton, messageBoxImage);
}
private Window? GetWindow()
{
if (!string.IsNullOrEmpty(WindowGuid))
{
foreach (Window window in Application.Current.Windows)
{
if (window.Tag?.ToString() == WindowGuid)
return window;
}
}
return null;
}
}
WindowGuid { get; set; }
GetWindow()
は、Model, ViewModelで、MessageBoxを表示する際、指定したWindowをOwnerにするために利用します。
IDialogService
public interface IDialogService
{
string WindowGuid { get; set; }
MessageBoxResult Show(string message, string caption, MessageBoxButton messageBoxButton, MessageBoxImage messageBoxImage);
MessageBoxResult ShowOnWindow(string message, string caption, MessageBoxButton messageBoxButton, MessageBoxImage messageBoxImage);
}
使い方
MainModel
public class MainModel : NotifyBase, IDisposable
{
#region DialogService
private readonly IDialogService _dialogService = Ioc.Default.GetRequiredService<IDialogService>();
public string GuidOfDialogService
{
get => _dialogService.WindowGuid;
set => _dialogService.WindowGuid = value;
}
#endregion
(・・・)
public void ShowMessage2OnModel()
{
_dialogService.Show("Show Message on the Model", "view", MessageBoxButton.OK, MessageBoxImage.Information);
}
public double Add(double a, double b)
{
var c = a + b;
var result = _dialogService.ShowOnWindow($"{a} + {b} = {c}", "Result", MessageBoxButton.OK, MessageBoxImage.None);
if (result != MessageBoxResult.OK) return 0;
return c;
}
GuidOfDialogService
は、OwnerのWindowを指定するためのプロパティなので、必要なければ、削除して良いです。
OwnerのWindowを指定する仕組み
GetWindow()
では、
- DialogServiceのプロパティWindowGuid
- アプリケーションで表示しているWindowのTag(Guidがセットされている)
これらが一致するWindowを、検索する。
private Window? GetWindow()
{
if (!string.IsNullOrEmpty(WindowGuid))
{
foreach (Window window in Application.Current.Windows)
{
if (window.Tag?.ToString() == WindowGuid)
return window;
}
}
return null;
}
使うための準備
- Window(View)を表示する時に、WindowのTagに、Guidをセットする。
- Model, ViewModel, Viewの、_dialogServiceのプロパティWindowGuidは、同じGuidにする。
すると、ShowOnWindowメソッドの中で、GetWindow()を実行し、WindowGuidと一致するWindowを取得する。
ここでは、Guidを使っているが、ユニークな値ならなんでも良い。
UnitTest
MainModelの、 public double Add(double a, double b) をMainWindowで実行すると、メッセージボックスが表示されます。このままでは、ユニットテストができません。
しかし、DIでMessageBoxをShowする機能(サービス)を、TestDialogService
に入れ替えると、ユニットテストができるようになります。
TestDialogService
internal class TestDialogService : IDialogService
{
public string WindowGuid { get; set; } = "";
public MessageBoxResult Show(string message, string caption, MessageBoxButton messageBoxButton, MessageBoxImage messageBoxImage)
{
return MessageBoxResult.OK;
}
public MessageBoxResult ShowOnWindow(string message, string caption, MessageBoxButton messageBoxButton, MessageBoxImage messageBoxImage)
{
return MessageBoxResult.OK;
}
}
IDialogServiceのインターフェイス
を実装したTestDialogService
を作成します。
Show, ShowOnWindowも、メッセージボックスを表示せず、MessageBoxResult.OK
だけを返す処理に変更しました。
UnitTest1
public class UnitTestFixture
{
public UnitTestFixture()
{
Ioc.Default.ConfigureServices(
new ServiceCollection()
.AddTransient<IDialogService, TestDialogService>()
.BuildServiceProvider());
}
}
public class UnitTest1 : IClassFixture<UnitTestFixture>
{
private readonly UnitTestFixture fixture;
public UnitTest1(UnitTestFixture fixture)
{
//https://xunit.net/xunit.analyzers/rules/xUnit1033
this.fixture = fixture;
}
[Theory]
[InlineData(1, 1, 2)]
[InlineData(2, 3, 5)]
public void CaluculateTest(double a, double b, double expected)
{
var cls = new MainModel();
var actual = cls.Add(a, b);
Assert.Equal(expected, actual);
}
}
- Iocのコンテナで、
DialogService
ではなく、TestDialogService
にします。 -
cls.Add(a, b)
の内部で、_dialogService.ShowOnWindow
が実行されますが、TestDialogService
のShowOnWindow
が実行されるので、MessageBoxResult.OK
が返るだけです。
サービスとは何か?
曖昧な理解でしたので、改めて調べました。
ここでは、Wikipediaのこの解説に即して理解することにします。
ソフトウェアアーキテクチャ、サービス指向(英語版)、およびサービス指向アーキテクチャの文脈で、サービス(英: service)という用語は、異なるクライアントがさまざまな目的のために再利用できる、目的を持ったソフトウェア機能または一連のソフトウェア機能(例: 指定した情報の検索や、一連の操作の実行)並びに、その使用を制御すべき方針(例: サービスを要求するクライアントのIDに基づく)を言う。
MVVMでは、次のようなものをサービス化することが多いです。
- MessageBox メッセージボックスの表示
- FileDialog 保存ダイアログの表示やファイル選択
MVVMの原則に馴染まない処理をカプセル化し、サービスを利用する本体のアプリケーションのコードがMVVMの原則から外れないようにできます。
GitHubで検索すると上記の例が数多くあります。
また別の例として、主にUI処理を行うためにDevExpressでのServiceがあります。
MVVM以外でのサービスの例
Webサービス
参考
サービスについての解説。考え方が色々あり、混乱します。