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

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

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

```csharp
MessageBox.Show("message");
```

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

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

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

## サンプルプロジェクト
![20240921 233029 MainWindow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132059/abd777f3-9169-0a82-18d4-20e168a79dc9.png)

https://github.com/mikihiro-t/DIStudy

.NET 8

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

https://qiita.com/hiro_t/items/5a2637179d6f580738de

---

## サービス化の方法

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

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

```csharp
Ioc.Default.ConfigureServices(
      new ServiceCollection()
      .AddTransient<IDialogService, DialogService>()
      .BuildServiceProvider());
```

##### DialogService
```csharp
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
```csharp
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
```csharp
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を、検索する。

```csharp
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132059/865fdac1-7f36-7174-5ec1-c325670c3b56.png)

しかし、DIでMessageBoxをShowする機能（サービス）を、`TestDialogService`に入れ替えると、ユニットテストができるようになります。


##### TestDialogService
```csharp
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

```csharp
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に基づく）を言う。

[サービス (システムアーキテクチャ)](https://ja.wikipedia.org/wiki/%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9_(%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E3%82%A2%E3%83%BC%E3%82%AD%E3%83%86%E3%82%AF%E3%83%81%E3%83%A3))

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

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

MVVMの原則に馴染まない処理を[カプセル化](https://ja.wikipedia.org/wiki/%E3%82%AB%E3%83%97%E3%82%BB%E3%83%AB%E5%8C%96)し、サービスを利用する本体のアプリケーションのコードがMVVMの原則から外れないようにできます。

GitHubで検索すると上記の例が数多くあります。
また別の例として、主にUI処理を行うために[DevExpressでのService](https://docs.devexpress.com/WPF/17414/mvvm-framework/services)があります。

#### MVVM以外でのサービスの例
Webサービス　

#### 参考
サービスについての解説。考え方が色々あり、混乱します。
- [Difference between a service class and a Helper class](https://softwareengineering.stackexchange.com/q/132067)
- [What is the service class in programming?](https://stackoverflow.com/q/29715105)