WPFアプリでライトテーマ・ダークテーマに対応するライブラリー#2 .NET Core 3.1対応

前回MetroRadianceをForkし、いくつかの修正を加えて、MetroRadiance.Forkとしてリリースしたことを投稿しました。今回は、それを拡張します。

MetroRadiance.Forkライブラリー

前回の投稿で、MetroRadiance2.4をベースにした、MetroRadiance.Fork 2.5をnugetに公開したことを報告しました。ソースコードもgithubで公開しています。

このフォーク版MetroRadiance.Forkでは、本家の実装から名前空間・クラス名・メソッド名・アセンブリ名などは変更していません。そのため、今まで本家のMetroRadianceライブラリーを使っていた場合は、ソースコードの修正は必要ありません。ライブラリーを置き換えるのみで使用できます。

MetroRadiance.Forkのnuget情報(2020年7月19日時点)

欲しかった機能

MetroRadiance.Fork2.5.0では、MetroRadiance2.4.0をベースに、MetroRadianceのgithubに登録されていたIssuesやPull Requestsで、容易に取り込むことができるものを取り込みました。また、Visual Studio 2019への対応もしました。

しかし、これだけではやりたいことができていません。

このライブラリーは、.NET Framework 4.5以降のWPFアプリを対象しています。しかし、.NET Core 3.1以降のWPFアプリをサポートできていません。

この.NET Core 3.1以降のWPFアプリをサポートすることが、MetroRadianceの派生のMetroRadiance.Forkを作成したモチベーションでもあります。

しかし、いきなり大変更を加えるのは負担が大きいため、まずは、2.5.0として、.NET Framework 4.5以降のみを対象とした修正版をリリースしました。

そこで、今回、本来やりたかったことである

  • .NET Core 3.1対応

をします。

実は、本家のMetroRadianceのIssuesに以下のものが登録されています。

Issues
 - Add dotNET 4.8 Support
 - Doesn't work with .net core 3.1

前者は、MetroRadiance.Fork2.5.0で対応しました。今回、後者の.NET Core 3.1のIssueに対応することになります。

なお、.NET Core 3.1に対応するといっても、.NET Framework 4.5の対応をやめるわけではありません。.NET Core 3.1/.NET Framework 4.5の両対応のライブラリーへと作り変えます。

.NET Core 3.1対応

さて、.NET Frameworkのライブラリーを.NET Core 3.1に対応するにはどうしたらよいのでしょうか?

大まかには以下の作業を行います。

  • プロジェクトファイルのスタイルをSDKスタイルに変更
  • 複数のターゲットフレームワークの設定
  • 参照するライブラリー群を変更
  • ライブラリーの変更に伴うコード変更
  • AssemblyInfo.csファイルの修正

この投稿では、上記について大まかな手順について説明します。詳細は、マイクロソフトのサイト(.NET Core への WPF アプリの移行)に記載されていますので参照してください。

プロジェクトファイルのスタイルをSDKスタイルに変更

.NET Coreを利用するには、プロジェクトファイル(csprojファイル)のスタイルを変更する必要があります。.NET CoreのWPFアプリが利用できるプロジェクトファイルはSDKスタイルのみです。従来からの.NET FrameworkのWPFアプリが利用するプロジェクトスタイルとは異なります。そのため、プロジェクトファイルを旧形式からSDKスタイルに変更します。なお、SDKスタイルのプロジェクトファイルは、.NET Coreと.NET Frameworkの両方のWPFアプリをで利用できます。

旧形式のプロジェクトファイル

旧形式では、下記のように<Project ToolsVersion="15.0" というエレメントから設定が始まります。

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <TargetFrameworkVersion>v4.5.0</TargetFrameworkVersion>
    <OutputType>library</OutputType>
    <RootNamespace>MetroRadiance</RootNamespace>

SDKスタイルのプロジェクトファイル

SDKスタイルでは、下記のように<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop" というエレメントから設定が始まります。

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
  <PropertyGroup>
    <TargetFrameworks>net45;netcoreapp3.1</TargetFrameworks>
    <UseWPF>true</UseWPF>
    <RootNamespace>MetroRadiance</RootNamespace>

Project要素にSdk属性を設定することにより、SDKスタイルのプロジェクトファイルになります。

SDKスタイルのプロジェクトファイルはシンプル

SDKスタイルのプロジェクトでは、デフォルト値から変更のある項目のみ記載する形式となります。デフォルト値のままでよいものは記載しません。また、ビルド対象のファイルの設定にはデフォルト値として**/*.csが指定されています。そのため、プロジェクトファイルがあるフォルダー以下の.csファイルは自動的にビルド対象となります。

旧形式のプロジェクトファイルでは、ビルド対象のファイルは、すべてプロジェクトファイル内に列挙していました。SDKスタイルのプロジェクトファイルでは、ビルド対象のファイルはほとんど設定する必要はありません。そのため、SDKスタイルのプロジェクトファイルは設定内容が簡素化されます。

実際にMetroRadiance.Fork.Coreのプロジェクトファイルの行数は、変更前の2.5.0では95行だったのが、変更後の3.0.0-alpha00では24行になりました。

複数のターゲットフレームワークの設定

.NET Framework 4.5/.NET Core 3.1の両対応にするためには、ターゲットフレームワークに複数のフレームワークを設定する必要があります。この設定は、ビルド対象のフレームワークの設定にとどまらず、nugetライブラリーの作成の設定にも反映されます。この設定をすることにより、.NET Framework/.NET Coreの両対応のnugetパッケージも作成できます。

SDKスタイルのプロジェクトでは以下のようにターゲットフレームワークを設定します。

一つのターゲットフレームワーク

一つのターゲットフレームワークを指定するときは、<TargetFramework>netcoreapp3.1</TargetFramework>のようにTargetFramework(単数形)タグを使います。通常、プロジェクトを作成したときは、TargetFrameworkのタグを使ったプロジェクトファイルが作成されます。この設定は、Visual Studio 2019のGUIでも変更できます。

複数のターゲットフレームワーク

一つのプロジェクトファイルで複数のターゲットフレームワークを扱うときは、<TargetFrameworks>net45;netcoreapp3.1</TargetFrameworks>のように、TargetFrameworks(複数形)タグを使います。ターゲットフレームワークをセミコロン(;)で区切り複数の設定ができます。ターゲットフレームワークとして設定する値は、マイクロソフトのサイト(英語/日本語)に情報があります。なお、現在のVisual Studio 2019では、複数のターゲットフレームワークを設定するGUIは設けられていません。変更するにはcsprojファイルをXMLエディターで直接編集します。

参照するライブラリー群を変更

プロジェクトファイルの形式をSDKスタイルに変更が完了したら、参照しているプロジェクトおよびライブラリーを再設定します。

プロジェクト参照に関しては、以前と同じように設定します。

ライブラリー参照に関しては、参照するライブラリー群を.NET Core 3.1に対応しているものに変更します。外部ライブラリーの多くは、最新版では.NET Core/.NET Frameworkの両対応になっているので、最新版を参照するのみで完了します。.NET Frameworkのデフォルトのライブラリーを参照していた場合は注意が必要です。.NET Frameworkでは、フレームワークのデフォルトライブラリーとして使えたものが、.NET Coreでは、フレームワークのデフォルトライブラリーとして含まれていないことがあるからです。

MetroRadiance.Forkでは、System.ServiceModel.dllの参照が該当しました。System.ServiceModel.dll内のSystem.UriTemplateクラスが、.NET Core側にはありませんでした。最終的に.NET Core 3.1側は、nugetパッケージのTavis.UriTemplatesを参照しました。ただし、実装されているメソッドが異なり、実装を変更する必要がありました。.NET Framework側もTavis.UriTemplatesに変更することはできますが、参照モジュールが多くなってしまうため、.NET Framework側は、System.ServiceModel.dllへの参照としました。

参照内容を、.NET Core側と.NET Framework側で分けたい場合は、条件を付けることにより可能となります。具体的には、以下のようにします。

  <ItemGroup>
    <PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.0.30" />
  </ItemGroup>

  <ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp3.1' ">
    <PackageReference Include="Tavis.UriTemplates" Version="1.1.1" />
  </ItemGroup>

  <ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
    <Reference Include="System.ServiceModel" />
  </ItemGroup>

一つ目のItemGroupは条件(Condition)がない参照設定となります。そのため、.NET Core 3.1と.NET Framework 4.5の両方に対して有効となります。

二つ目のItemGroupは、netcoreapp3.1に対する条件が付いています。そのため、.NET Core 3.1のみに対する参照設定となります。

二つ目のItemGroupは、net45に対する条件が付いています。そのため、.NET Framework 4.5のみに対する参照設定となります。

ライブラリー変更に伴うコード変更

ライブラリー変更に伴うコード修正が必要な場合は、ビルドエラー、もしくは、新たなビルド警告になることがほとんどです。そのため、ビルドエラー・警告を中心にコードを修正します。

ただ、ライブラリー変更に伴うコード変更が必要ないこともあります。

実際、MetroRadiance.Fork.CoreおよびMetroRadiance.Fork.Chromeに対しては、コード変更は必要ありませんでした。変更が必要となったのは、MetroRadiance.Forkのみです。

これは、先ほど述べた、利用しているクラスがSystem.UriTemplateクラスからTavis.UriTemplatesのクラスに変更になったことに対する修正です。Tavis.UriTemplatesパッケージを利用することになった.NET Core 3.1側のコードのみを修正しました。

コード内では、プラットフォームに応じた自動的に設定される定義値を使って、コードを分けています。例えば以下のようにコードを分けます。

#if NETCOREAPP
    return _templateTable.Match(uri)?.Key == "theme";
#else
    return themeTemplate.Match(templateBaseUri, uri) != null;
#endif

.NET Core系の場合は、前者の行のソースコードが有効になり、それ以外(結果として.NET Framework系)では後者の行のソースコードが有効になります。

自動的に定義される定義値は、マイクロソフトのサイト(マルチターゲットを設定する方法)に情報があります。抜粋すると以下の定義値が利用できます。

ターゲット フレームワークSymbols
.NET FrameworkNETFRAMEWORK, NET20, NET35, NET40, NET45, NET451, NET452, NET46, NET461, NET462, NET47, NET471, NET472, NET48
.NET StandardNETSTANDARDNETSTANDARD1_0NETSTANDARD1_1NETSTANDARD1_2NETSTANDARD1_3NETSTANDARD1_4NETSTANDARD1_5NETSTANDARD1_6NETSTANDARD2_0NETSTANDARD2_1
.NET CoreNETCOREAPP, NETCOREAPP1_0, NETCOREAPP1_1, NETCOREAPP2_0, NETCOREAPP2_1, NETCOREAPP2_2, NETCOREAPP3_0, NETCOREAPP3_1
#if ディレクティブで使用できるターゲットフレームワークに関するプリプロセッサ シンボル(2020年7月時点)

このように必要となったコード修正を終わらせます。

AssemblyInfo.csファイルの修正

最後に、AssemblyInfo.csファイルを修正します。

SDKスタイルのプロジェクトファイルに形式変更するとAssemblyInfo.csファイルに定義していた情報の多くが、プロジェクトファイル側に設定できるからです。

デフォルト設定(GenerateAssemblyInfoがtrue)のままでは、同じ情報が両方に定義されていると、二重定義のエラーとなります。そのため、AssemblyInfo.csファイルに定義していた多くの情報をプロジェクトファイル側に移し、AssemblyInfo.csファイルからは削除します。プロジェクトファイル側に移した情報は、ビルドされたモジュールのバージョン情報とnugetパッケージのメタ情報の両方に使われます。

もし、AssemblyInfo.csファイルを変更することなくそのまま使いたい場合は、プロジェクトファイルに<GenerateAssemblyInfo>false</GenerateAssemblyInfo>の設定を追加します。この場合、プロジェクトファイルに設定したバージョン情報などは、ビルドされるモジュールファイルには自動的に反映されません。プロジェクトファイル側の設定を変更したときは、AssemblyInfo.csファイルの情報も忘れず変更するようにします。

プロジェクトの変換完了

以上が.NET Framework アプリのプロジェクトを.NET Core/.NET Framework両用アプリのプロジェクトに修正する方法の概要です。

モジュール署名とパッケージ署名

近年、一般公開するアプリ、モジュール、インストーラーなどに電子署名をすることは当たり前になってきています。

電子署名の主な役割は二つあります。

  • 発行者を明確にする
  • モジュールが破損していないことを保証する

この二つの役割は、モジュールを使用する側で電子署名を確認することにより、検証できます。電子署名が無効であれば、不正な証明書を使って署名されたもの(発行者が不明)か、もしくは、電子署名時からファイルが変更されている(壊れている)ときです。

電子署名が有効であれば、電子署名に使われた証明書を確認することにより、発行者が確認できます。また、電子署名をしたときからファイルが変更されていないことも保証されます。

MetroRadiance.Forkのモジュールとパッケージへの電子署名

今回、プロジェクト構成を大きく見直すにあたって、モジュール署名とパッケージ署名をすることにしました。なお、モジュール署名はMetroRadiance.Fork 2.5.0でもしていました。

MetroRadiance.Forkでは、.NETアセンブリの仕組みの一つである、厳密な名前のアセンブリとするためのアセンブリ署名はしません。対応するのは、あくまで、モジュール署名とパッケージ署名のみです。

電子署名をするタイミングは以下の通りにしたいです。

  • 通常のビルド時には電子署名はしない
    通常のビルド時に電子署名をしてしまうと、ソースコードをクローンした人が、電子証明を持たないためビルドに失敗することになります。これを避けるため、通常のビルド時には電子署名はしません。
  • nugetパッケージ作成時にのみ電子署名をする
    ソースコードをクローンした人でもnugetパッケージ作成までするひとは多くないため問題なしと判断しました。

SDKスタイルのプロジェクトファイルに変更すると、ビルドメニューのパッケージ(pack)コマンドから、nugetパッケージを作成できます。しかし、このコマンドを使用すると、パッケージ作成ときのみにモジュール署名がしたいのですが、それができないように思われました。

そのため、署名付きパッケージ作成専用のスクリプトで対応することにしました。この結果、ビルドメニューのパッケージコマンドには影響することがなくなり、ソースコードをクローンした人も署名なしのパッケージは作成できることになります。

署名付きパッケージ作成スクリプト

モジュールおよびパッケージ署名付きnugetパッケージを作成するには以下の手順で行います。

  • プロジェクトをRelease版でビルドする
  • モジュール署名をする
  • nugetパッケージを作成する
  • nugetパッケージにパッケージ署名をする

dotnet.exe / signtool.exe / nuget.exeなどを利用して実現します。

以下に実現方法の例を示しますが、これが唯一の方法ではなく、この他にもいろいろな実現方法が存在します。

プロジェクトをRelease版でビルドする

各プロジェクトをRelease版でビルドします。各プロジェクトをもれなくビルドするために、プロジェクトの依存関係を利用せず、各々のプロジェクトをビルドします。

プロジェクトのビルドには、dotnet.exebuildコマンドを使用します。dotnet.exeは、Microsoft .NET Core SDK(または、Microsoft .NET SDK)に同梱されています。Microsoft .NET Core SDK(または、Microsoft .NET SDK)がインストールされている環境のPCであれば、”C:\Program Files\dotnet\”のサブフォルダー内に見つかると思います。

dotnet.exe build -c Release --no-incremental --no-dependencies MetroRadiance.Core\MetroRadiance.Core.csproj

のように、プロジェクトファイルを指定してビルドのみします。

モジュール署名をする

モジュール署名はsigntool.exesignコマンドを使用します。signtool.exeは、Windows SDKに同梱されています。Windows SDKがインストールされている環境のPCであれば、”C:\Program Files (x86)\Windows Kits\”のサブフォルダー内に見つかると思います。

signtool.exe sign /a /fd sha256 /n "YourCertSubjectName" /d "SignDesc" /tr "http://timestamp.digicert.com?alg=sha256" moduleFiles

のように、モジュールに署名します。

YourCertSubjectNameには、署名に使用する証明書のCommonNameを指定します。MetroRadiance.Forkでは、”nishy software”を指定しています。

SignDescには、何のための署名なのかわかる名称を指定します。製品名を指定することが多いと思います。世の中の署名をしているモジュールでもSignDescを設定していないことが多いです。ユーザーに分かりやすくするものであるので忘れず設定しましょう。MetroRadiance.Forkでは、MetroRadianceを指定しています。ただし、今後、MetroRadiance.Forkに変更する予定です。

nugetパッケージを作成する

モジュール署名が終わったら、各プロジェクトのnugetパッケージを作成します。パッケージ時にプロジェクトのビルドをすることもできますが、署名したモジュールが上書きされてしまうため、ビルドはしません。nugetパッケージの作成のみをします。

パッケージ作成には、dotnet.exepackコマンドを使用します。dotnet.exeは、Microsoft .NET Core SDK(または、Microsoft .NET SDK)に同梱されています。Microsoft .NET Core SDK(または、Microsoft .NET SDK)がインストールされている環境のPCであれば、”C:\Program Files\dotnet\”のサブフォルダー内に見つかると思います。

dotnet.exe pack -c Release --no-build -o "%WORK_FOLDER%" MetroRadiance.Core\MetroRadiance.Core.csproj

のように、プロジェクトファイルを指定してパッケージ作成のみします。%WORK_FOLDER%は、作成したパッケージの出力先フォルダーを指定します。

nugetパッケージにパッケージ署名をする

nugetパッケージの作成が終わったら、各プロジェクトのパッケージにパッケージ署名をします。

パッケージ署名は、nuget.exesignコマンドを使用します。nuget.exeは、nuget.orgのサイトからダウンロードできます。

nuget.exe sign "%PACKAGE_FILE%" -Verbosity quiet -CertificateSubjectName "%CERT_SUBJECT_NAME%"  -Timestamper "http://timestamp.digicert.com?alg=sha256" 

のように、パッケージに署名します。%PACKAGE_FILE%は、署名するパッケージを指定します。

MetroRadiance.Fork用に用意した署名付きパッケージ作成スクリプト

前述したコマンドをコマンドラインで入力すれば署名付きパッケージは作成できます。しかし、毎回、同じコマンドを入力するのは大変なので、MetroRadiance.Fork用のスクリプトを作成しました。スクリプトはgithubで公開しているレポジトリのpackageフォルダーに存在します。

  • BuildPackages.cmd
  • SignPackages.cmd

の二つのスクリプトがあります。

BuildPackages.cmdは、前述した一連の作業を行い署名付きパッケージを作成します。このスクリプトを実行するときは、事前に環境変数SIGNTOOLに、モジュール署名用のスクリプトを設定しておく必要があります。

SET SIGNTOOL=SignModuleFiles.bat

などのように、署名用のスクリプトのパスを環境変数SIGNTOOLに設定します。SignTool.exeを使用する場合、SignModuleFiles.batの内容は、一番シンプルな状態では、以下のようになります。

SignTool.exe sign /a /fd sha256 /n "YourCertSubjectName" /d "SignDesc" /tr "http://timestamp.digicert.com?alg=sha256" %*

SignPackages.cmdは、BuildPackages.cmdの最後の過程(パッケージに署名する)が、タイムアウトで失敗することがあり、この工程のみを再度実行できるスクリプトとなります。

MetroRadiance.Fork 3.0.0-alpha00

というわけで、MetroRadiance.Forkライブラリーの.NET Core 3.1対応版をnugetに公開しました。バージョンは、3.0.0-alpha00としました。ソースコードもgithubで公開しています。

この更新版では、.NET Coreに対応をしたバージョンであることがすぐにわかるように、メジャーバージョンを上げています。しかし、2.5.0から名前空間・クラス名・メソッド名・アセンブリ名などは変更していません。

今までのMetroRadiance/MetroRadiance.Coreライブラリーを使っていた.NET Framework WPFアプリの場合は、ソースコードの修正は必要ありません。ライブラリーを置き換えるのみで使えます。

また、本バージョンの主目的である.NET Core 3.1のWPFアプリでも問題なくパッケージ参照できます。

MetroRadiance.Forkのnuget情報(2020年7月25日時点)

今回の投稿は、nugetに公開した.NET Core対応版のMetroRadiance.Forkの紹介でした。

“WPFアプリでライトテーマ・ダークテーマに対応するライブラリー#2 .NET Core 3.1対応” への2件の返信

コメントを残す