新しもの好きプログラマの耳より情報ブログ

仕事でもあるプログラミングについて役に立ちそうな情報を発信していこうというブログです。役に立たなそうな情報はfacebookで。

WPFにGenericHostを入れて便利に最新機能を使うための、最小限の組み込み方法

概要

.NETの最近の新機能を入れようとすると、サンプルで当たり前のようにDI(Dependency Injection)やGenericHost用のExtensionsが出てきます。しかしWPFのテンプレートは真っ白で、そんな物は影も形もありません。

GenericHostを組み込んでしまえばそうした問題はまとめて解決出来て、DIもできますしLoggerなども使えるようになって便利になります。あれこれの機能の紹介は別の記事に譲るとして、まずはWPFへの最小限の組み込み方法を紹介します。

最初に結論まとめ

Template Studio for WPFを使うか、次のGitリポジトリのWpfApp1プロジェクトのようにすることで手動で最小限の組み込みができます。

https://github.com/suusanex/sample_wpf_generichost

説明

WPFでGenericHostを使いたい場合、大きくは2つの選択肢があります。

  1. Template Studio for WPF

  2. 手動で組み込み

Template Studio for WPF

1のTemplate Studio for WPFは、VisualStudioのテンプレートに追加してプロジェクト生成するだけなので、とても楽です。

ただ、最小限以上に色々入っていたりしますし、テンプレートなので頻繁に更新されるわけでもありません。.NET 6で止まっていてNullableなどに非対応だったりします。

こちらは特に説明するほどの内容でも無いので、興味がある方は次のURLからインストールしてみてください。

https://marketplace.visualstudio.com/items?itemName=TemplateStudio.TemplateStudioForWPF

手動で組み込み

2の手動組み込みは、文字通り手動ですが、最低限必要な物だけを入れることができます。この記事ではこちらのやり方を紹介します。

テンプレートから生成したWPFプロジェクトに、まずはGenericHostを追加します。NuGetで次のように追加です。

<ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />
</ItemGroup>

テンプレートだと、最初のウインドウを表示するまでの処理を省略出来るように、App.xamlにStartupUriが設定されています。

これを外して、Startupイベントハンドラを追加します。このStartupでホストを生成していきます。

<Application x:Class="WpfApp1.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:WpfApp1"
             Startup="OnStartup">

追加したStartupで、ホストを作ります。作るだけあればとても簡単なコードで、ここに必要な物を足していくことになります。

private IHost? _host;

private async void OnStartup(object sender, StartupEventArgs e)
{
    _host = Host.CreateDefaultBuilder(e.Args)
        .Build();

    await _host.StartAsync();
}

続いて、メインのサービスとなるクラスを追加します。ホスト上でこのサービスが起動する形になるので、このサービスからWPFのウインドウ表示を行っていく使い方になります。サービスはIHostedServiceを継承すれば良いので、最小限だとこんな感じで、開始と停止のイベントハンドラだけになります。

    public class ApplicationHostService : IHostedService
    {
        public async Task StartAsync(CancellationToken cancellationToken)
        {
        }

        public async Task StopAsync(CancellationToken cancellationToken)
        {
        }
    }

これを、先ほど作ったホストのDIコンテナにサービスとして登録します。ConfigureServicesメソッドが増えていまして、ここがDIコンテナへの登録処理になります。

  _host = Host.CreateDefaultBuilder(e.Args)
      .ConfigureServices(collection =>
      {
          collection.AddHostedService<ApplicationHostService>();
      })
      .Build();

ここまでの実装で、アプリを起動するとApplicationHostService.StartAsyncが呼ばれる状態になりました。まだウインドウは表示されません。ここに、WPFのメインウインドウ表示処理を入れます。

DIコンテナに、メインウインドウを追加します。

_host = Host.CreateDefaultBuilder(e.Args)
    .ConfigureServices(collection =>
    {
        collection.AddHostedService<ApplicationHostService>();
        collection.AddTransient<MainWindow>();
    })
    .Build();

このインスタンスを、サービス開始時に起動して表示します。これは、普通にApp.Startupでメインウインドウを作る処理と同じです。WPFの標準設定だと最初に表示されたウインドウがMainWindowになるので、表示するだけで処理を終えてOKです。

public class ApplicationHostService(IServiceProvider _serviceProvider) : IHostedService
{
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        _MainWindow = _serviceProvider.GetRequiredService<MainWindow>();
        _MainWindow.Show();
        await Task.CompletedTask;
    }

    private Window? _MainWindow;

上のコードでは、まずコンストラクタでDIコンテナから生成するためのIServiceProviderを受け取り、それを使ってGetRequiredServiceメソッドでMainWindowのインスタンスを生成し、それをShowしています。

これで最小限のGenericHost組み込みは完成です!このまま実行すると、WPFのメインウインドウが表示されると思います。

普通のWPFと同じ動きですが、Extensionsの追加もできますし、すでにAppクラス以外はDIコンテナ経由で生成されている状態なので、 ILogger<T> を受け取ったり、任意のクラスインスタンスをDIコンテナに追加して受け取ったりができる状態となっています。

全体のコードを見たい人は、次のGitリポジトリのWpfApp1プロジェクトを見ると分かりやすいと思います。

https://github.com/suusanex/sample_wpf_generichost

まとめ

WPFへGenericHostを手動で組み込みました。イメージしていたよりも少ないコード変更で簡単に組み込めたのではないでしょうか。

既存のプロジェクトへも同じように組み込めるはずなので、どんどんGenericHost前提の新しいインフラ的機能を導入して、開発効率を上げていきましょう!

ちなみに

こういった話については、先日のイベントで登壇して語りました。これがその時の資料です。

www.docswell.com

そういう登壇型も含めてC# Tokyoで色々イベントやっていますので、よかったらそちらも見てみてください。

csharp-tokyo.connpass.com