前提
シングル EXE ファイルで動作する WPF アプリを作る方法は、以下が有名です。
この方法はいくつかのパターンで動作しません。
元記事では DLL から DLL の関数を呼ぶパターンが紹介されています。WPFToolkit 等が該当するそうです。System.Data.SQLite なども同じパターンに該当します。これらのパターンに該当する場合の対処方法は…………わかりません!!!(ドン)
このページで解決する問題は、もう1つの動作しないパターンであるサテライトアセンブリを含むパターンの場合です。元記事の方法ではサテライトアセンブリも含めて DLL を EXE に埋め込み、正しく動作させることができません。
サテライトアセンブリとは?
サテライトアセンブリとは国際化対応の際に作成される特殊なアセンブリです。
言語毎のディレクトリに「ベースのアセンブリ名.resources.dll」という名前で作成されます。
例えば「ExampleProject」というアセンブリ名でアプリや DLL を作っていて、その中に日本語(ja-JP)や英語(en-US)のリソース(*.resx ファイル群)を作っていたとしましょう。
その場合、以下のようなサテライトアセンブリが作成されます。
- ja-JP/ExampleProject.resources.dll
- en-US/ExampleProject.resources.dll
以上に心当たりがない方は、このページを読む必要はないでしょう。
解決方法
では、サテライトアセンブリを含めた DLL を EXE に埋め込み、正しく動作させる方法を紹介します。
サテライトアセンブリを出力するプロジェクトの種類が「Windows アプリケーション」なのか「クラスライブラリ」なのかによって使い分け、もしくは組み合わせが必要です。
状況ごとの対応方法を詳しく説明します。
Windows アプリケーション(=WPF アプリ本体)のサテライトアセンブリを EXE に埋め込む。
WPF アプリ本体に言語毎のリソースを定義している場合の対応方法です。
こちらの対応方法はめちゃくちゃ簡単です。
以下の NuGet パッケージの参照を WPF アプリ本体のプロジェクトに追加してください。
こちらはコンパイル時に WPF アプリ本体のサテライトアセンブリの埋め込みとサテライトアセンブリの読み込み処理の埋め込みを自動で行ってくれます。
なので、後はコンパイルするだけです!
※ 逆コンパイルしてみるとサテライトアセンブリが埋め込まれ、AppDomain.CurrentDomain.AssemblyResolve に独自処理が追加されているのがわかります。
クラスライブラリのサテライトアセンブリを EXE に埋め込む。
WPF アプリ本体とは別にクラスライブラリとしてサテライトアセンブリを定義している場合の対応方法です。
「Windows アプリケーション(=WPF アプリ本体)のサテライトアセンブリを EXE に埋め込む。」で紹介した Resource.Embedder は、残念ながら WPF アプリ本体のサテライトアセンブリしか処理してくれません。
そのため以下の方法をちょっと修正して使います。
StartUp クラスのコードを以下で置き換えてください。その他の手順は一緒です。
using System;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.IO;
namespace StartUpCode //名前空間は使いやすいものに変えてください
{
public class StartUp
{
[STAThread]
static void Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
App.Main(); // Run WPF startup code.
}
private static Assembly OnResolveAssembly(object sender, ResolveEventArgs e)
{
var thisAssembly = Assembly.GetExecutingAssembly();
// Get the Name of the AssemblyFile
var assembly = new AssemblyName(e.Name);
var dllName = assembly.Name + ".dll";
// Load satelite assembly
if (dllName.EndsWith("resources.dll") && !CultureInfo.InvariantCulture.Equals(assembly.CultureInfo))
{
var sateliteAssembly = LoadAssemblyFromResource(thisAssembly, assembly.CultureInfo.Name + @"\" + dllName);
if (sateliteAssembly != null)
{
return sateliteAssembly;
}
}
// Load normal assembly
return LoadAssemblyFromResource(thisAssembly, dllName);
}
private static Assembly LoadAssemblyFromResource(Assembly thisAssembly, string dllName)
{
// Load from Embedded Resources - This function is not called if the Assembly is already
// in the same folder as the app.
var resourceName = thisAssembly.GetManifestResourceNames().Where(
s => s.EndsWith(dllName, true, null)).FirstOrDefault();
if (resourceName != null)
{
using (var stream = thisAssembly.GetManifestResourceStream(resourceName))
{
if (stream == null)
{
return null;
}
try
{
var block = new byte[stream.Length];
stream.Read(block, 0, block.Length);
return Assembly.Load(block);
}
catch (IOException)
{
return null;
}
catch (BadImageFormatException)
{
return null;
}
}
}
// in the case the resource doesn't exist, return null.
return null;
}
}
}
参照元記事との違いは resources.dll だったらディレクトリ指定でサテライトアセンブリを読みに行くよう改良してある点です。それだけです。
※ ちなみに以上の方法は Windows アプリケーションには使えません。理由は WPF アプリ本体のサテライトアセンブリ自体を EXE に埋め込むことができないからです。逆アセンブルして見てみると埋め込まれていないのがわかります。
EXE の出力と同じタイミングでサテライトアセンブリが出力されるからだと想像しています。
同じタイミングで出力されるものだから EXE に簡単には埋め込めないんじゃないかなって。
それをやってくれるのが Resource.Embedder だと思っています。簡単にできる方法がもしあれば Resource.Embedder はいらなくなるかもしれません。
Windows アプリケーションにもクラスライブラリにもサテライトアセンブリがある場合
両方やれば OK です!