.NET汎用ホストでWPFを動かしてDIしたりHTTPリクエストを受け付けたりした話

[.NET汎用ホスト](https://docs.microsoft.com/ja-jp/dotnet/core/extensions/generic-host)にはDIとか構成とかログとか便利な仕組みが多いし、Webインタフェースのサポートが手厚い。
ずるい！WPFでも使いたい！！と思って使った話です。

# なにができたか結論だけ先に
* 汎用ホスト上でWPFアプリケーションを動かせた
* 汎用ホストのDIをWPFで活用できた
* WebAPIアプリケーションを同時に動かし、Webリクエストを受け付けるWPFアプリケーションを構築できた

# プロジェクト構成
別に凝る必要はないけど、3つのプロジェクトで分けてそれぞれに役目を持たせました。

```
HostWpf
│  HostWpf.sln
├─WebApplication1：Webリクエスト受付
├─WorkerService1：Mainメソッド呼び出し、ホスト構成
└─WpfLibrary1：WPFアプリケーション
```

# WpfLibrary1
Wpfクラスライブラリテンプレート(`dotnet new wpflib -o WpfLibrary1`)から作成します。中身は以下の感じ

```
WpfLibrary1
  │  App.xaml
  │  App.xaml.cs
  │  MainWindow.xaml
  │  MainWindow.xaml.cs
  │  WpfLibrary1.csproj
  │  WPFWorker.cs
  └─ WpfWorkerExtension.cs
```

## App
Appクラスは、WPFアプリケーションテンプレート(`dotnet new wpf`)で作成されるコードをコピペした内容でOKです。ただしビルドアクションはPageを指定しておきます。

```xml:WpfLibrary1.csproj
<ItemGroup>
  <Page Include="App.xaml" />
</ItemGroup>
```

## MainWindow
MainWindowも適当ですが、DIを試したいのでコンストラクタインジェクションのコードを追加します。ロガーの動作確認用に、Windowを閉じたときにログを出力するようにしました。

```csharp:MainWindow.xaml.cs
private ILogger<MainWindow> logger;
public MainWindow(ILogger<MainWindow> logger)
{
    this.logger = logger;
    InitializeComponent();
}
private void Window_Closed(object sender, EventArgs e)
{
    this.logger.LogInformation("MainWindow was Closed. (Message: {0})", this.Message);
}
```

## WPFWorker
一番のポイントとなる`WPFWorker`クラスはWorkerクラスとして次の実装にします。Workerクラスについては[MSDocs](https://docs.microsoft.com/ja-jp/dotnet/core/extensions/workers)を参照してください。

```csharp:WPFWorker.cs
public class WPFWorker : BackgroundService
{
    private IHostApplicationLifetime hostApplicationLifetime;
    private TaskCompletionSource tcs = new TaskCompletionSource();
    public WPFWorker(IHostApplicationLifetime hostApplicationLifetime)
    {
        this.hostApplicationLifetime = hostApplicationLifetime;
    }
    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        Thread wpfThread = new Thread(new ThreadStart(StartWpf));
        wpfThread.SetApartmentState(ApartmentState.STA);
        wpfThread.Start();
        return this.tcs.Task;
    }
    private void StartWpf()
    {
        var app = new App();
        app.InitializeComponent();
        app.Run();
        this.tcs.SetResult();
        this.hostApplicationLifetime.StopApplication();
    }
}
```

`IHostApplicationLifetime `については、汎用ホストのライフサイクル管理クラスですので[MSDocs](https://docs.microsoft.com/ja-jp/dotnet/core/extensions/generic-host#ihostapplicationlifetime)を参照してください。DIでコンストラクタ挿入します。今回はWPFアプリケーションが終了すると同時にホストも終了するようにしましたが、ここら辺はアプリケーションの要件だと思います。
若干怪しいのは、`ExecuteAsync`メソッドでSTAなスレッドを動かしつつTaskを返さなければならないので、`TaskCompletionSource`を使っていること。とりあえず動いてますが、要検証だと思います。

## WpfWorkerExtension
あとは、このWorkerをホスト上で動かすための処理をWpfWorkerExtensionに実装します。

```csharp:WpfWorkerExtension.cs
public static IHostBuilder ConfigureWpf(this IHostBuilder hostBuilder) =>
    hostBuilder.ConfigureServices((hostContext, services) =>
        services.AddTransient<MainWindow>()
                .AddHostedService<WPFWorker>());
```
サービスの登録については[MSDocs](https://docs.microsoft.com/ja-jp/dotnet/core/extensions/dependency-injection)を参照してください。

# WebApplication1
ただのWPFアプリでは面白くないので、WebAPIでのリクエストを受け付けられるようにします。プロジェクトはASP.NET Core Web APIテンプレートから作ります（`dotnet new webapi -o WebApplication1`)。中身は以下の感じ

```
WebApplication1
 │  appsettings.Development.json
 │  appsettings.json
 │  Startup.cs
 │  WebApplication1.csproj
 │  WpfInvokerExtension.cs
 ├─Controllers
 │      ApplicationController.cs
 │      MainWindowController.cs
 └─Properties
         launchSettings.json
```

## Startup
Webアプリを開始するためのコードを記述します。詳しくは[MSDocs](https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/startup?view=aspnetcore-5.0)を参照してください。JavaScriptから呼ばれることも想定し、`UseCors`を追加しています。

```csharp:Startup.cs
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    }
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        app.UseRouting();
        app.UseCors();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}
```

## WpfInvokerExtension

WebAPIアプリケーションをホスト上で動かすためのコードを記述します。

```csharp:WpfInvokerExtension.cs
public static IHostBuilder ConfigureWpfInvoker(this IHostBuilder builder) =>
    builder.ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });
```
## Controllers/MainWindowController

Webリクエストを受け付けてMainWindowを表示する処理を行います。`App`クラスの`Dispatcher`上で実行しないといけないのはWPF的ないつもの事情です。Controllerの書き方は[MSDocs](https://docs.microsoft.com/ja-jp/aspnet/core/web-api/?view=aspnetcore-5.0)を参照して下さい。リクエストからメッセージを受け取り画面に表示させています。

```csharp:MainWindowController.cs
[Route("api/[controller]")]
[ApiController]
public class MainWindowController : ControllerBase
{
    private IServiceProvider serviceProvider;
    public MainWindowController(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider;
    }
    [HttpPost]
    public ActionResult<object> ShowMainWindow(MainWindowParameter parameter)
    {
        App.Current.Dispatcher.Invoke(() =>
        {
            var mainWin = (MainWindow)this.serviceProvider.GetService(typeof(MainWindow));
            mainWin.Message = parameter.Message;
            mainWin.Show();
        });
        return this.Ok();
    }
}
```
レスポンスコードは`201 Created`だよなあと思いつつ、`Location`に何を渡すか考えるのが面倒だったのでとりあえず`200 OK`です。

## Controllers/ApplicationController

WPFアプリケーションをWebリクエストで停止するコードを書きます。現実のアプリケーションではこんな機能あっちゃいけない気もしますが、今回は実験なので。

```csharp:ApplicationController.cs
[Route("api/[controller]")]
[ApiController]
public class ApplicationController : ControllerBase
{
    [HttpDelete]
    public IActionResult ShutdownApplication()
    {
        App.Current.Dispatcher.Invoke(() => App.Current.Shutdown());
        return NoContent();
    }
}
```

# WorkerService1
いよいよ本丸のホスト部分です。Worker Serviceテンプレート(`dotnet new worker -o WorkerService1`)から作成します。中身は以下の感じ

```
WorkerService1
 │  appsettings.Development.json
 │  appsettings.json
 │  Program.cs
 │  WorkerService1.csproj
 └─Properties
         launchSettings.json
```

## Program
ホストを作成・ビルド・実行します。WPFアプリケーション、WebAPIアプリケーションを実行します。ホストの実行方法は[MSDocs](https://docs.microsoft.com/ja-jp/dotnet/core/extensions/generic-host)を参照してください。

```csharp:Program.cs
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWpf()
            .ConfigureWpfInvoker();
}
```

## 動作確認
コマンドプロンプトからWorkerService1.exeを実行します。

```
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: 
******************\HostWpf\WorkerService1\bin\Debug\net5.0-windows
```

ホストが実行されログが出力されます。
PowerShellからリクエストを投げます。

```powershell
Invoke-WebRequest -Method Post -Headers @{"Content-type"="application/json"} -Body '{"Message":"Hello WPF"}' http://localhost:5000/api/MainWindow
```
![win.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1247638/af90bd82-e3b3-8298-5d54-eb68b6678889.png)
MainWindowが表示され、リクエストで渡したメッセージが表示されます。
MainWindowを閉じるとコマンドプロンプトにログが表示されます。

```
info: WpfLibrary1.MainWindow[0]
      MainWindow was Closed. (Message: Hello WPF)
```

DIでMainWindowに挿入したロガーが適切に動いていることがわかります。

再度、PowerShellからリクエストを投げます。

```powershell
Invoke-WebRequest -Method Delete http://localhost:5000/api/Application
```

するとコマンドプロンプトに下記メッセージが表示されアプリケーションとホストが終了しました。

```
info: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...
```

# まとめ
汎用ホスト上でWPFアプリケーションを動作させることができました。
DIや構成、ロガーといった便利機能をさくっと使えるのは楽でいいなと思います。外部ライブラリとの優位性比較はいろいろあるのでしょうが。
Webリクエストを用いたWPF操作に関しては、ASP.NET Coreのお作法でコードを書いて統合できるのはとても使い勝手がいいと思います。常駐させておいてURI起動の感覚でfetchから呼べたり、Webアプリケーションでは実現できないレベルの操作をデスクトップアプリに移譲したり、OAuth2.0のようなWebリクエストが絡むフローで用いたり、便利に使えそうです。

ソースコード全体は[こちら](https://github.com/miswil/HostWpf)

# 参考
* [.NET での汎用ホスト](https://docs.microsoft.com/ja-jp/dotnet/core/extensions/generic-host)
* [.NET の Worker サービス](https://docs.microsoft.com/ja-jp/dotnet/core/extensions/workers)
* [IHostApplicationLifetime](https://docs.microsoft.com/ja-jp/dotnet/core/extensions/generic-host#ihostapplicationlifetime)
* [.NET での依存関係の挿入](https://docs.microsoft.com/ja-jp/dotnet/core/extensions/dependency-injection)
* [ASP.NET Core でのアプリケーションのスタートアップ](https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/startup?view=aspnetcore-5.0)
* [ASP.NET Core を使って Web API を作成する](https://docs.microsoft.com/ja-jp/aspnet/core/web-api/?view=aspnetcore-5.0)