WPFとWinFormsを汎用ホストに簡易に組み込む
汎用ホスト(GenericHost)は非常に便利なものなので、デスクトップアプリでも使いたいですね。というわけで組み込んでみましょう。
ウインドウ閉じたときのリソース片付けや、タスクトレイ常駐型にも対応しようとするといろいろ込み入ったことやらないといけなさそうなので、今回はハローワールド的なコードを書いてみます。
WPF
プロジェクトを新規作成するところから始めます。
まず何はともあれMicrosoft.Extensions.Hosting
パッケージを追加しましょう。これがないと始まりません。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
</ItemGroup>
</Project>
App.xaml
のStartupUri
の指定は削っておきます。メインウインドウは後述するIHostedService
の内部で指定するためです。
<Application x:Class="Wpf.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Resources>
</Application.Resources>
</Application>
続いてIHostedService
の実装を行います。汎用ホストにユーザーコードを組み込むには、このインターフェイスを実装してやる必要があります。
WPFをひとまず動かすだけなら、Application
クラスとWindow
クラスを注入してもらえば十分です。具象クラスで受け取ってしまっていますが、必要があればIApplication
、IWindow
などのインターフェイスを定義して注入してもらうようにしましょう。
class WpfHostService : IHostedService
{
readonly IHostApplicationLifetime _hostApplicationLifetime;
readonly Application _app;
readonly Window _window;
public WpfHostService(IHostApplicationLifetime hostApplicationLifetime, Application app, Window window)
{
_hostApplicationLifetime = hostApplicationLifetime;
_app = app;
_window = window;
}
}
受け取ったApplication
とWindow
のインスタンスをStartAsync
メソッドでRunします。Run
メソッドを呼び出すと(Application
クラスのデフォルトでは)ウインドウを閉じるまで制御を返しません。
Run
メソッドから制御が返ってきたら、必須ではありませんがIHostApplicationLifetime.StopApplication
メソッドを呼び出しておきます。これを呼び出しておくと、IHostedService.StopAsync
メソッドが呼び出されますので、ここで必要に応じてリソースの片付けや設定の保存処理などを行ってください。
class WpfHostService : IHostedService
{
public Task StartAsync(CancellationToken cancellationToken)
{
_app.Run(_window);
_hostApplicationLifetime.StopApplication();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
}
IHostedService
の実装が済んだらいよいよ汎用ホストを呼び出しましょう。次のようなエントリーポイントを定義します。DIコンテナにWpfHostService
を登録し、ユーザー定義のApp
とMainWindow
も登録しておきます。Application
クラスは1プロセスにつき1つしかインスタンス化できないので、ライフサイクルはシングルトンでよいでしょう。
namespace Wpf;
static class Program
{
[STAThread]
static void Main(string[] args)
=> Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<WpfHostService>();
services.AddSingleton<Application, App>();
services.AddTransient<Window, MainWindow>();
})
.Build()
.Run();
}
最後に定義したProgram
クラスをエントリーポイントとして明示して完成です。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
...
<StartupObject>Wpf.Program</StartupObject>
</PropertyGroup>
</Project>
実行すればウインドウが表示されます。
WinForms
WinFormsもWPFと大まかな流れは同じです。
プロジェクトを新規作成したらMicrosoft.Extensions.Hosting
パッケージを追加します。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
</ItemGroup>
</Project>
IHostedService
を定義します。WinFormsではテキストレンダリング設定や、DPI対応などを行ってくれるApplicationConfiguration.Initialize
メソッドを呼び出す必要があるのですが、これは任意のForm
クラスをインスタンス化する前に行わなければなりません(Form
クラスをインスタンス化した後に呼び出すと例外発生)。
なので、今回はLazy
でnewするタイミングを遅延させるように実装しています。あるいは単純にメイン関数の最初のところで呼び出してしまうのもよいかもしれません。
class WinFormsHostService : IHostedService
{
readonly IHostApplicationLifetime _hostApplicationLifetime;
readonly Lazy<Form> _formFactory;
public WinFormsHostService(IHostApplicationLifetime hostApplicationLifetime, Lazy<Form> formFactory)
{
_hostApplicationLifetime = hostApplicationLifetime;
_formFactory = formFactory;
}
public Task StartAsync(CancellationToken cancellationToken)
{
ApplicationConfiguration.Initialize();
Application.Run(_formFactory.Value);
_hostApplicationLifetime.StopApplication();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
}
IHostedService
の実装が済んだので汎用ホストを呼び出します。WPFの時と同じようにDIコンテナにWinFormsHostService
とユーザー定義のForm1
登録しておきます。
前述のようにForm
クラスはLazy
でラップして返します。
static class Program
{
[STAThread]
static void Main(string[] args)
=> Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<WinFormsHostService>();
services.AddTransient<Form, Form1>();
services.AddTransient<Lazy<Form>>(c => new(() => c.GetService<Form>()!));
})
.Build()
.Run();
}
実行すればウインドウが表示されます。
リポジトリ
参考に上述のコードをまとめたリポジトリも一応置いておきます。
Discussion