MVVM Toolkit の Messenger を使うときに購読解除を共通化する

# はじめに
MVVMライブラリを使うときにMessenger機能を使うことがあるかと思います。
本記事ではMessengerを使うときに、購読解除を1箇所にまとめて共通化する方法について書いていきます。
各コードは筆者が作成したアプリのソースコードから抜粋したものをベースにしています。

# 使用するライブラリ
- MVVM Toolkit(※)：MVVMライブラリ
- Microsoft.Xaml.Behaviors.Wpf：イベントをコマンドなどに紐づけるときに使うライブラリ
- Microsoft.Extensions.DependencyInjection：DIコンテナ

※：Microsoft.Toolkit.MvvmとCommunityToolkit.Mvvmの2つがあります。
執筆時点では2つのライブラリは同じ内容ですが、将来的には後者に一本化される予定のようです。

# シナリオ
- MessengerはWeakReferenceMessengerを使う
- Messageを購読するのはView(ウインドウ)
- ウインドウを閉じたときにすべてのMessage購読を解除する

# 0. 本当に購読解除が必要なのか検討する
実際に作る前に、本当に購読解除が必要なのか検討しましょう。
MVVM ToolkitのMessengerのドキュメントによると、WeakReferenceMessengerを使う場合は購読解除は必須ではないものの、グッドプラクティスとのことです。また、StrongReferenceMessengerを使う場合は必須です。
今回はWeakReferenceMessengerを使いますが、グッドプラクティスならやっておこうということで、購読解除を実装します。

# 1. 購読解除のヘルパークラスを作る
まずは購読解除をまとめたヘルパークラスを作ります。
購読を解除したいオブジェクトを引数で受け取って、UnregisterAll()の引数に入れてやります。
```cs:UnregisterMessageHelper.cs 
using System;
using CommunityToolkit.Mvvm.Messaging;

namespace TestApp.Helpers
{
    public class UnregisterMessageHelper
    {
        public void Unregister(object sender, EventArgs e)
        {
            WeakReferenceMessenger.Default.UnregisterAll(sender);
        }
    }
}
```
実装したUnregister()の引数については後で説明します。

# 2. ウインドウを閉じたときにヘルパークラスを呼び出す
Windows Forms時代のようにコードビハインドにイベントハンドラーを書いて、そこから呼び出しても実現することはできます。
ですが、もう少し工夫してXAMLから呼び出せるようにします。

## 2.1. ロケーターにヘルパークラスを登録する
MVVM Toolkitの前世と呼んでよいであろうMVVM Light Toolkitでは、NuGetでインストールするとViewModelsフォルダーに「ViewModelLocator」というクラスを自動で作ってくれました。
これを真似てロケーターを作り、そこに1.のヘルパークラスを登録します。
```cs:Locator.cs
using System;
using TestApp.Helpers;
using TestApp.ViewModels;
using Microsoft.Extensions.DependencyInjection;

namespace TestApp
{
    public class Locator
    {
        private readonly IServiceProvider serviceProvider;

        public Locator()
        {
            ServiceCollection services = new();

            services.AddTransient<ChildWindowViewModel>();

            services.AddTransient<UnregisterMessageHelper>();

            serviceProvider = services.BuildServiceProvider();
        }

        public ChildWindowViewModel ClassifyAsSettingWindowViewModel => serviceProvider.GetRequiredService<ChildWindowViewModel>();

        public UnregisterMessageHelper UnregisterMessageHelper => serviceProvider.GetRequiredService<UnregisterMessageHelper>();
    }
} 
```
上記のコードだけであればDIは不要ですが、実際のアプリではModelなどと一緒にDIコンテナに登録しています。

## 2.2. アプリのリソースにロケーターを登録する
ロケーターをViewのXAMLから利用できるようにするため、リソースとして登録します。
```xml:App.xaml 
<Application x:Class="TestApp.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:TestApp"
             StartupUri="Views/MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <local:Locator x:Key="Locator" />
        </ResourceDictionary>
    </Application.Resources>
</Application>
```

## 2.3. Viewにヘルパークラスの呼び出しを追加する
メソッドを呼び出したいのでCallMethodActionを使います。
EventTriggerのEventNameに"Closed"を指定して、Closedイベント発生時にヘルパークラスを呼び出します。
```xml:ChildWindow.xaml 
<Window x:Class="TestApp.Views.xml:ChildWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-TestApp.Views"
        xmlns:behaviors="http://schemas.microsoft.com/xaml/behaviors"
        mc:Ignorable="d"
        DataContext="{Binding Source={StaticResource Locator}, Path=ChildWindowViewModel}">
    <behaviors:Interaction.Triggers>
        <behaviors:EventTrigger EventName="Closed">
            <behaviors:CallMethodAction TargetObject="{Binding Source={StaticResource Locator}, Path=UnregisterMessageHelper}"
                                        MethodName="Unregister" />
        </behaviors:EventTrigger>
    </behaviors:Interaction.Triggers>
</Window>
```

# 3. Voila!
これでウインドウを閉じたときにメッセージの購読を解除することができます。

# Tips
## UnregisterMessageHelper.Unregister()の変数
CallMethodActionで指定するメソッドは、紐づけるイベントのハンドラーと同じ引数を受け取ることができるようです。
今回の購読解除では、Closedイベントが発生したインスタンス=メッセージを購読しているインスタンスを受け取ることができれば目的を達成できるため、イベントハンドラーと同じ引数を受け取るようにしました。
なお、CallMethodActionで指定するメソッドは引数なしでも良いです。

## Microsoft.Xaml.Behaviors.Wpf
資料によってはSystem.Windows.Interactivityが同等の役目をしていることもあるかと思います。
現在(VS2019以降?)はMicrosoft.Xaml.Behaviors.Wpfに変更されています。

# 参考資料
###### 公式ドキュメント
[MVVM-Samples/docs/mvvm/Messenger.md](https://github.com/CommunityToolkit/MVVM-Samples/blob/master/docs/mvvm/Messenger.md)
###### その他
[How to pass a event argument as a parameter in interaction.Trigger when using MVVM?](https://stackoverflow.com/questions/10680222/how-to-pass-a-event-argument-as-a-parameter-in-interaction-trigger-when-using-mv)
[WPF から Windows App SDK に移行を試してみた](https://zenn.dev/okazuki/articles/migrate-to-windowsappsdk-from-wpf)