.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を指定しておきます。
<ItemGroup>
<Page Include="App.xaml" />
</ItemGroup>
MainWindow
MainWindowも適当ですが、DIを試したいのでコンストラクタインジェクションのコードを追加します。ロガーの動作確認用に、Windowを閉じたときにログを出力するようにしました。
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を参照してください。
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に実装します。
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
を追加しています。
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アプリケーションをホスト上で動かすためのコードを記述します。
public static IHostBuilder ConfigureWpfInvoker(this IHostBuilder builder) =>
builder.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
Controllers/MainWindowController
Webリクエストを受け付けてMainWindowを表示する処理を行います。App
クラスのDispatcher
上で実行しないといけないのはWPF的ないつもの事情です。Controllerの書き方はMSDocsを参照して下さい。リクエストからメッセージを受け取り画面に表示させています。
[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リクエストで停止するコードを書きます。現実のアプリケーションではこんな機能あっちゃいけない気もしますが、今回は実験なので。
[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を参照してください。
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
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リクエストが絡むフローで用いたり、便利に使えそうです。
ソースコード全体はこちら