2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-12-15

.NET汎用ホストには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を指定しておきます。

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

MainWindow

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

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を参照してください。

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を参照してください。DIでコンストラクタ挿入します。今回はWPFアプリケーションが終了すると同時にホストも終了するようにしましたが、ここら辺はアプリケーションの要件だと思います。
若干怪しいのは、ExecuteAsyncメソッドでSTAなスレッドを動かしつつTaskを返さなければならないので、TaskCompletionSourceを使っていること。とりあえず動いてますが、要検証だと思います。

WpfWorkerExtension

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

WpfWorkerExtension.cs
public static IHostBuilder ConfigureWpf(this IHostBuilder hostBuilder) =>
    hostBuilder.ConfigureServices((hostContext, services) =>
        services.AddTransient<MainWindow>()
                .AddHostedService<WPFWorker>());

サービスの登録についてはMSDocsを参照してください。

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を参照してください。JavaScriptから呼ばれることも想定し、UseCorsを追加しています。

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アプリケーションをホスト上で動かすためのコードを記述します。

WpfInvokerExtension.cs
public static IHostBuilder ConfigureWpfInvoker(this IHostBuilder builder) =>
    builder.ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

Controllers/MainWindowController

Webリクエストを受け付けてMainWindowを表示する処理を行います。AppクラスのDispatcher上で実行しないといけないのはWPF的ないつもの事情です。Controllerの書き方はMSDocsを参照して下さい。リクエストからメッセージを受け取り画面に表示させています。

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

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を参照してください。

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からリクエストを投げます。

Invoke-WebRequest -Method Post -Headers @{"Content-type"="application/json"} -Body '{"Message":"Hello WPF"}' http://localhost:5000/api/MainWindow

win.png
MainWindowが表示され、リクエストで渡したメッセージが表示されます。
MainWindowを閉じるとコマンドプロンプトにログが表示されます。

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

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

再度、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リクエストが絡むフローで用いたり、便利に使えそうです。

ソースコード全体はこちら

参考

2
5
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
2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?