しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

既存の WPF アプリケーションを .NET Core 3.0 に移行した

Visual Studio 2019 が RC に到達したという話だったので、検証用に使っているマシンにインストールして既存 WPF アプリの .NET Core 3.0 移行がどのくらいのコストで行えるのか試しました。

にぃにが書いているように .NET Core 3.0 を Visual Studio 2019 RC で使うには、プレビュー版の .NET Core を使うように設定を変える必要があります。

理屈としては分かりますが、まあまあはまりやすいポイントですね。設定を変えると .NET Core 3.0 向けのプロジェクトを読み込んでビルド出来るようになりました。

.NET Core 3.0 の Win Forms / WPF 対応について調べてみると、ほとんど dotnet new wpf とかで作ったコードを動かしていただけだったので、本当にマイグレーション可能なのか怪しく思ってました。

なので、今回は Windows Store にも公開してる以下の WPF アプリを移行対象にしました。

このアプリは WPF だけではなく Win Forms のコントロールを WindowsFormsHost を使ってホストしていて、他にも WinRT の機能を部分的に使っていたりもするので、移行できないかもと思っていました。更に P/Invoke や COM (RCW) も使いまくっています。

結論から言うと、既存のコードを変更することなく全ての機能を移行できました。実際に行った作業内容が気になる方もいると思うので、移行用の Pull Request を作成しておきました。

変更したのは実質 csproj ファイルだけです。Desktop Bridge を使っていると、パッケージング時に Self-contained を指定する必要があったので、wapproj も少し変わっています。

PR を見ると簡単そうに見えると思いますが、作業中は割とはまったので手順を残します。

SDK ベースのプロジェクト化

コードは全て GitHub で管理していたので、ブランチを作成して既存のプロジェクトファイルを直接弄る方法で作業しました。csproj は dotnet new wpf で生成されたものから始めました。

生成される csproj は以下のようにシンプルな SDK ベースとなります。

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

</Project>

上のコマンドを実行すると WPF 向けのプロジェクトになりますが、Win Forms も必要な場合は UseWindowsForms を追加すればよいらしいです。ドキュメントに両方とも設定可能とあります。

csproj を SDK ベースに書き換えた後に Visual Studio でプロジェクトを開くと、自動的に xaml ファイル系を追加してくれました。なので csproj はバッサリと置き換えた方が効率が良いでしょう。

Visual Studio を開いた後は必要な NuGet パッケージをインストールしていきます。既存の WPF アプリケーションは大抵は packages.config を使っていると思いますが、そのまま移行せずにルートとなるパッケージだけ追加するとシンプルになります。*1

(必要であれば)ターゲットプラットフォームを固定

SDK ベースのプロジェクトではデフォルトで Any CPU となるので、プラットフォームに依存した dll を使っている場合に問題となります。なので明示的にプラットフォームを設定しておきました。

<PropertyGroup>
  <Platforms>x86;x64</Platforms>
  <Platform>x64</Platform>
</PropertyGroup>

この設定で x86 と x64 がビルドの対象となります。デフォルトは x64 にしておきました。

NuGet の警告を抑制

まだ .NET Core 3.0 に対応した NuGet パッケージは少ないので、ちょいちょい以下のような警告が出ます。

warning NU1701: パッケージ 'AvalonEdit 5.0.4' はプロジェクトのターゲット フレームワーク '.NETCoreApp,Version=v3.0' ではなく '.NETFramework,Version=v4.6.1' を使用して復元されました。このパッケージは、使用しているプロジェクトとの完全な互換性がない可能性があります。

こちらでは対応できないのでどうしようもないですが、曰く .NET Core 3.0 の WPF は既存のコードと互換性があるらしいので、対応するまでは警告を無視するようにします。

該当のエラーコード NU1701 を NoWarn に指定すれば上の警告は出なくなります。

f:id:shiba-yan:20190302150912p:plain

設定したところ、ソリューションエクスプローラーの依存関係についていた警告が消えました。

f:id:shiba-yan:20190302150255p:plain

使っているパッケージが .NET Core 3.0 に対応したら NoWarn を消せば良いでしょう。*2

WinRT の参照を修正

今回のアプリでは自動起動のために StartupTask を使っていました。もちろん WinRT で提供されている API なので、参照設定が追加で必要になります。今回の作業で一番はまったのがここです。

API が定義されている Windows.winmd は以下のサンプルがあったので簡単でした。

インストールしている Windows SDK は 17763 だったので、パスは少し違いますがやってることは同じです。

これまで通り Visual Studio から参照の追加を行っても良さそうでしたが、その場合には HintPath が相対パスになるのが嫌なので手書きしました。

<ItemGroup>
  <Reference Include="Windows">
    <HintPath>$(MSBuildProgramFiles32)\Windows Kits\10\UnionMetadata\10.0.17763.0\Windows.winmd</HintPath>
    <Private>False</Private>
  </Reference>
</ItemGroup>

これで WinRT の API は参照できるようになりますが、使うためには System.Runtime.WindowsRuntime.dll を参照に追加しないといけません。このライブラリには IAsyncOperation<T> の Awaiter 実装が入っているので、追加しないと await が使えないままです。

調べると大体は Reference Assemblies 内にある dll を参照しろと書いてあるのですが、この dll を使うと Desktop Bridge でパッケージングした際にアプリが起動しなくなります。

$(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5\System.Runtime.WindowsRuntime.dll

パッケージング前だと動くので原因特定に苦労しました。エラーメッセージも良くわからない感じだったので、参照を一つずつ外して特定しました。

解決方法としては NuGet で .NET Core 3.0 に対応したバージョンが公開されているので、こっちをインストールすれば Desktop Bridge でパッケージングしても問題なく動きました。

ちなみに Windows Community Toolkit では NuGet 版が参照されているので、インストールすれば特に気にすることなく await などが使える状態になります。Windows Community Toolkit 自体は 6.0.0-preview から .NET Core 3.0 への対応が行われています。

ここまでの対応で .NET Core 3.0 への移行は完了となります。WinRT を使っていなければもっと楽でした。

Desktop Bridge プロジェクトを修正

Desktop Bridge に使うプロジェクトは大した変更点はないですが、Self-contained に対応した設定になっていないといけないので、一度アプリケーションを参照し直すと簡単に対応できるはずです。

プロパティには自己完結という設定が増えているので true になっていることを確認します。

f:id:shiba-yan:20190302150504p:plain

この状態でビルドすると RuntimeIdentifiers が必要というエラーになるので、WPF アプリケーション側の csproj に以下のようにターゲットとなる RID を追加します。

現実的には win-x86win-x64 の固定値で良いと思います。

<RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>

再度ビルドを行うと今度は成功するので、Visual Studio から直接デバッグ実行も行えるようになります。

サイドロードに使う appxbundle と Store 提出に使う appxupload もこれまで通り生成できます。

f:id:shiba-yan:20190302150321p:plain

ランタイムは x86 と x64 の両方が含まれているので appxupload のサイズは 85MB ぐらいになりましたが、ランタイムをインストールしていない PC で実行まで行えました。

WinRT 周りでつまずきましたが、全体的に見るとスムーズに移行が行えたと思います。Visual Studio 2019 RC は Go Live が付いているので、そのまま Store に提出しても問題なさそうですが RTM を待ちます。 Go Live が付いたのは VS だけで .NET Core 3.0 はプレビューのままなのでそもそも無理でした。

*1:事前に PackageReference に移行しておくのが楽かもしれない

*2:消さなくても実害は全くないけど