Windows フォームアプリケーションで MessagePipe を利用する


## このドキュメントの内容

先日リリースされた `MessagePipe` をWindowsフォームアプリケーションで利用する方法を説明します。
Windowsフォームアプリケーションでは DI が利用されるケースは多くはないため、DI に関する記事が少ないのが現状です。
`MessagePipe` そのものの説明ではなく、`MessagePipe` を利用するために DI 環境を整える方法になります。

## プロジェクトを作成する

* `MessagePipe` がサポートするフレームワークは .NETStandard2.0 または .NET5.0 です。プロジェクトテンプレートは `Windows Forms App (.NET)` を使用するとよいでしょう。

## MessagePipe と関連するライブラリをインストールする

Nuget で次のパッケージをインストールしてください。

* MessapePipe
* Microsoft.Extensions.DependencyInjection

## エントリポイントに DI の初期処理を実装する

* Microsoft.Extensions.DependencyInjection では、DI でインスタンスを生成したい型をあらかじめ ServiceCollection に登録しておく必要があります。ServiceCollection から生成された ServiceProvider を通じてインスタンスを生成することができるようになります。

* MessagePipe で提供される拡張メソッド AddMessagePipe を呼び出せば標準的な型が登録されますが、Windowsフォームアプリケーションの場合は IPublisher などの型は登録されません。使用したい型を登録する必要があります。
  * MessagePipe/src/MessagePipe/ServiceCollectionExtensions.cs の内容を参照してください。

* フォームのコンストラクタに DI で生成したインスタンスを渡したい場合、フォームも ServiceCollection に登録します。
  * フォームに ServiceProvider を渡し、フォーム内でそのインスタンスを生成するような設計も考えられます。なお、ServiceProvider をそのまま渡すよりもコンテキストのような管理クラス内に内包させて渡したほうがよいと思います。

```c#:Program.cs
using MessagePipe;
using Microsoft.Extensions.DependencyInjection;

namespace WindowsFormsApp1
{
    static class Program
    {
        /// <summary>
        ///  アプリケーションのエントリポイント
        /// </summary>
        [STAThread]
        static void Main(string[] args)
        {
            // MessagePipe を使用するためのサービスを生成する
            ServiceCollection services = CreateMessagePipeServices();

            // フォームのインスタンスをDIで生成する場合はアプリケーションのフォームを登録する
            services.AddTransient<Form1>();

            // サービスプロバイダーを生成する
            ServiceProvider provider = services.BuildServiceProvider();

            // Windowsフォームアプリケーションの既定の初期処理
            Application.SetHighDpiMode(HighDpiMode.SystemAware);
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            // これではDIを利用できない
            // 後述するどちらかの方法でフォームのインスタンスを生成してアプリケーションを開始する
            // Application.Run(new Form1());

            // サービスプロバイダーからフォームのインスタンスを生成する
            // コンストラクタの引数に定義された MessagePipe のインスタンスが注入される
            Application.Run(provider.GetRequiredService<Form1>());

            // サービスプロバイダーを内包したコンテキストを渡す
            // フォーム内で MessagePipe のインスタンスを生成する
            Application.Run(new Form1(new MyContext(provider)));
        }

        /// <summary>
        /// MessagePipe を使用するためのサービスを生成します。
        /// </summary>
        /// <returns></returns>
        static ServiceCollection CreateMessagePipeServices()
        {
            ServiceCollection services = new ServiceCollection();

            // MessagePipe の標準サービスを登録する
            services.AddMessagePipe(options =>
            {
                // 全てのメッセージに適用したいフィルタはグローバルフィルタとして定義するとよい
                options.AddGlobalMessageHandlerFilter(typeof(SampleFilter<>));
            }
            );

            // 使用するメッセージを登録する
            services.AddSingleton(typeof(MessagePipe.IPublisher<,>), typeof(MessageBroker<,>));
            services.AddSingleton(typeof(MessagePipe.ISubscriber<,>), typeof(MessageBroker<,>));

            return services;
        }
    }

    /// <summary>
    /// サービスプロバイダを内包したコンテキスト
    /// </summary>
    internal class MyContext
    {
        internal MyContext(ServiceProvider serviceProvider)
        {
            m_ServiceProvider = serviceProvider;
        }

        private readonly ServiceProvider m_ServiceProvider;

        public IPublisher<TKey, TMessage> CreatePublisher<TKey, TMessage>()
        {
            return m_ServiceProvider.GetRequiredService<IPublisher<TKey, TMessage>>();
        }

        public ISubscriber<TKey, TMessage> CreateSubscriber<TKey, TMessage>()
        {
            return m_ServiceProvider.GetRequiredService<ISubscriber<TKey, TMessage>>();
        }
    }

    /// <summary>
    /// サンプルフィルタ
    /// </summary>
    /// <typeparam name="T"></typeparam>
    internal class SampleFilter<T> : MessageHandlerFilter<T>
    {
        public override void Handle(T message, Action<T> next)
        {
            System.Diagnostics.Debug.WriteLine("subscribing...");
            next(message);
            System.Diagnostics.Debug.WriteLine("subscribed.");
        }
    }

}
```

## フォームを実装する

```c#:Form1.cs
using MessagePipe;

namespace WindowsFormsApp1
{
    internal partial class Form1 : Form
    {
        #region ctor

        /// <summary>
        /// デフォルトコンストラクタ
        /// </summary>
        private Form1()
        {
            InitializeComponent();
        }

        /// <summary>
        /// サービスプロバイダから呼び出されるコンストラクタ
        /// </summary>
        /// <param name="publisher">パブリッシャ</param>
        /// <param name="subscriber">サブスクライバ</param>
        /// <remarks>
        /// DIでフォームのインスタンスを生成した場合はこのコンストラクタが呼び出され、
        /// publisher/subscriber にインスタンスが注入される。
        /// コンストラクタのスコープは public でなくてはならない。
        //// 型のスコープは public でなくてもよい。
        /// </remarks>
        public Form1(
            IPublisher<string, SampleMassage> publisher
            , ISubscriber<string, SampleMassage> subscriber
        ) : this()
        {
            m_Publisher = publisher;
            m_Subscriber = subscriber;
            InitializePubSub();
        }

        /// <summary>
        /// ServiceProviderを内包したコンテキストを受け取るコンストラクタ
        /// </summary>
        /// <param name="context">コンテキスト</param>
        /// <remarks>
        /// ServiceProviderを利用して publisher/subscriber のインスタンスを生成する。
        /// </remarks>
        internal Form1(MyContext context) : this()
        {
            m_Context = context;
            m_Publisher = context.CreatePublisher<string, SampleMassage>();
            m_Subscriber = context.CreateSubscriber<string, SampleMassage>();
            InitializePubSub();
        }

        #endregion

        /// <summary>
        /// Pub/Sub に関する初期処理
        /// </summary>
        private void InitializePubSub()
        {
            // キーが "form1" であるメッセージを購読する
            m_Releaser = m_Subscriber?.Subscribe(
                "form1"
                , x =>
                {
                    System.Diagnostics.Debug.WriteLine($"subscribe: {x.Message}");
                }
            );
        }

        private readonly MyContext? m_Context;
        private readonly MessagePipe.IPublisher<string, SampleMassage>? m_Publisher;
        private readonly MessagePipe.ISubscriber<string, SampleMassage>? m_Subscriber;
        private IDisposable? m_Releaser;

        private void button1_Click(object sender, EventArgs e)
        {
            // メッセージを発行する
            // 前述のサブスクライバには一つめのメッセージのみが送られる
            m_Publisher?.Publish("form1"
                , new SampleMassage($"message to form1 at {DateTime.Now}")
            );
            m_Publisher?.Publish("form2"
                , new SampleMassage($"message to form2 at {DateTime.Now}")
            );
        }

    }
}
```

```c#:SampleMassage.cs
namespace WindowsFormsApp1
{
    internal readonly struct SampleMassage
    {
        internal SampleMassage(string message)
        {
            Message = message;
        }
        public string Message { get; }
    }
}
```

## 汎用ホストを利用する（2022年12月追記）

汎用ホスト（GenericHost）には DI の機能が組み込まれています。ASP.NET などのテンプレートには汎用ホストが利用されていますが、残念ながら Windows フォームアプリケーションのテンプレートは汎用ホストは利用されていません。そこで、汎用ホスト上で Windows フォームアプリケーションを実行するための拡張ライブラリを作成しました。このライブラリを使うと、前述の Program.cs の実装は次のように変わります。

https://qiita.com/mxProject/items/432f7b63b15bf270f842

```c#:Program.cs
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using mxProject.WindowFormHosting;
using MessagePipe;

namespace WindowsFormsApp1;

internal static class Program
{
    /// <summary>
    /// アプリケーションのメイン エントリ ポイントです。
    /// </summary>
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        // ホストビルダーを生成する
        var builder = Host.CreateDefaultBuilder()

            // Windowsフォームアプリケーションを登録する
            // コンテキストとメインフォームの型を指定する
            .AddWindowsFormApp<MyContext, Form1>()

            // アプリケーションに注入するサービスを登録する
            .ConfigureServices((hostContext, services) =>
            {
                // MessagePipe の登録方法は前述と同じ

                // MessagePipe の標準サービスを登録する
                services.AddMessagePipe(options =>
                {
                    // 全てのメッセージに適用したいフィルタはグローバルフィルタとして定義するとよい
                    options.AddGlobalMessageHandlerFilter(typeof(SampleFilter<>));
                }
                );

                // 使用するメッセージを登録する
                services.AddSingleton(typeof(IPublisher<,>), typeof(MessageBroker<,>));
                services.AddSingleton(typeof(ISubscriber<,>), typeof(MessageBroker<,>));
            });

        // 実行する
        builder.Build().Run();
    }
}

/// <summary>
/// サービスプロバイダを内包したコンテキスト
/// </summary>
internal class MyContext : WindowsFormAppContextBase
{
    // DI から呼び出すためコンストラクタのスコープは public
    public MyContext(
        IWindowsFormProvider formProvider,
        ILoggerFactory loggerFactory,
        IServiceProvider serviceProvider
    )
        : base(formProvider, loggerFactory)
    {
        m_ServiceProvider = serviceProvider;
    }

    private readonly IServiceProvider m_ServiceProvider;

    public IPublisher<TKey, TMessage> CreatePublisher<TKey, TMessage>()
    {
        return m_ServiceProvider.GetRequiredService<IPublisher<TKey, TMessage>>();
    }

    public ISubscriber<TKey, TMessage> CreateSubscriber<TKey, TMessage>()
    {
        return m_ServiceProvider.GetRequiredService<ISubscriber<TKey, TMessage>>();
    }
}
```
フォームの実装は次のように変わります。DI からコンストラクタが呼び出されるとき、コンテキストのインスタンスを注入できます。IPublisher, ISubscriber を注入することもできますが、DI から呼び出せるコンストラクタが複数定義されていると「コンストラクタが特定できない」例外がスローされます。どちらかにしましょう。

```c#:Form1.cs
internal partial class Form1 : Form
{
    // DI から呼び出すためコンストラクタのスコープは public
    public Form1(MyContext context)
    {
        InitializeComponent();

        m_Publisher = context.CreatePublisher<string, SampleMassage>();
        m_Subscriber = context.CreateSubscriber<string, SampleMassage>();
        InitializePubSub();
    }

    // DIから呼び出せるコンストラクタが複数定義されていると例外がスローされる
    // public Form1(
    //     IPublisher<string, SampleMassage> publisher,
    //     ISubscriber<string, SampleMassage> subscriber
    //     )
    // {
    //     InitializeComponent();
    //
    //     m_Publisher = publisher;
    //     m_Subscriber = subscriber;
    //     InitializePubSub();
    //}
}
```




## 参考リンク

https://github.com/Cysharp/MessagePipe

https://qiita.com/WiZLite/items/93d36660d09bdbbd43f4

https://qiita.com/WiZLite/items/4b2d1a0ebf6fd433d605

https://qiita.com/yoship1639/items/32ca894bda3b0db54a7b