🪟

WPFとWinFormsを汎用ホストに簡易に組み込む

2023/01/23に公開

汎用ホスト(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.xamlStartupUriの指定は削っておきます。メインウインドウは後述する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クラスを注入してもらえば十分です。具象クラスで受け取ってしまっていますが、必要があればIApplicationIWindowなどのインターフェイスを定義して注入してもらうようにしましょう。

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;
    }
}

受け取ったApplicationWindowのインスタンスを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を登録し、ユーザー定義のAppMainWindowも登録しておきます。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();
}

実行すればウインドウが表示されます。

リポジトリ

参考に上述のコードをまとめたリポジトリも一応置いておきます。
https://github.com/nin-neko/DesktopAppHosting

Discussion