4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MVVMに適した方法で、DIでMessageBoxをサービスにし、テストもする。

Posted at

課題

Dependency Injection(DI・依存性の注入・依存オブジェクト注入)は、MVVMに必須ではないのですが、MVVMと相性がよく、使わないと不便な場合も多いです。

典型的なのが、MessageBoxを表示する処理です。
ModelやViewModelで

MessageBox.Show("message");

を行うとしましょう。
この場合の課題は、

  • これは、Viewを表示する処理。従って、MVVMとしては不適。という考え方があります。
  • UnitTestを実行しても、ここで処理がストップするので、UnitTestができない。

以上の課題を、まるっと解決するのが、DIコンテナでMessageBoxの表示をサービス化する方法です。

サンプルプロジェクト

20240921 233029 MainWindow.png

.NET 8


この記事は、次のMessageBox for WPF の仕組みの解説を兼ねています。


サービス化の方法

ここでは、DIコンテナに、CommunityToolkit.Mvvm.DependencyInjectionを利用してみましょう。

アプリの起動時、IocのコンテナにDialogServiceIDialogServiceとセットで登録します。

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で実行すると、メッセージボックスが表示されます。このままでは、ユニットテストができません。

image.png

しかし、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が実行されますが、TestDialogServiceShowOnWindowが実行されるので、MessageBoxResult.OKが返るだけです。

サービスとは何か?

曖昧な理解でしたので、改めて調べました。
ここでは、Wikipediaのこの解説に即して理解することにします。

ソフトウェアアーキテクチャ、サービス指向(英語版)、およびサービス指向アーキテクチャの文脈で、サービス(英: service)という用語は、異なるクライアントがさまざまな目的のために再利用できる、目的を持ったソフトウェア機能または一連のソフトウェア機能(例: 指定した情報の検索や、一連の操作の実行)並びに、その使用を制御すべき方針(例: サービスを要求するクライアントのIDに基づく)を言う。

サービス (システムアーキテクチャ)

MVVMでは、次のようなものをサービス化することが多いです。

  • MessageBox メッセージボックスの表示
  • FileDialog 保存ダイアログの表示やファイル選択

MVVMの原則に馴染まない処理をカプセル化し、サービスを利用する本体のアプリケーションのコードがMVVMの原則から外れないようにできます。

GitHubで検索すると上記の例が数多くあります。
また別の例として、主にUI処理を行うためにDevExpressでのServiceがあります。

MVVM以外でのサービスの例

Webサービス 

参考

サービスについての解説。考え方が色々あり、混乱します。

4
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?