もくじ
https://tera1707.com/entry/2022/02/06/144447
やりたいこと
↓に上げた実験コードのように、Winui3のPage
とFrame
を使った画面遷移のコードで、
Assembly.LoadFile()
で読み込んだdllのなかのクラスのインスタンスを、frame
でページを指定して画面遷移するためのcontentFrame.Navigate()
の第二引数で渡して、Page
側の、そのPageに遷移してきたときの処理のOnNavigatedTo(NavigationEventArgs e)
でそれを受けるときに、e.Parameter
をそのインスタンスのクラスでキャストすると、下図の例外で落ちる。
ということがあった。
載せた側と受ける側で、同じクラスでキャストしてるはずなのになぜ落ちるのか?が全然わからなかったので、理由を調べたい。
実験① 実際に起きた現象に似せたコードを作ってみる
ClassLibrary1.csprojというdllのプロジェクト
dllに作成したクラス。(中身に特に意味はない)
namespace ClassLibrary1; public class Class1 { public static Class1 CreateInstance() => new Class1(); public int Data1; public int Data2; }
winui3のアプリプロジェクト
メイン画面側で、動的に👆のdllを呼んで、その中のクラスのインスタンスを持つようにした。
(「data 」に入れたものが、「ClassLibrary1.Class1」クラスのインスタンス)
で、そのインスタンスを、画面(Page)遷移時のパラメータとして、contentFrame.Navigate()
の第二引数に渡した。
using Microsoft.UI.Xaml.Media.Animation; using System; using System.Reflection; namespace FileVerUpTool; public sealed partial class MainWindow : Window { public MainWindow() => this.InitializeComponent(); private void nvSample_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args) { var dll = Assembly.LoadFile(@"C:\git\FileVerUpTool\ClassLibrary1\bin\x64\Debug\net6.0\ClassLibrary1.dll"); var type = dll.GetType("ClassLibrary1.Class1"); var obj = Activator.CreateInstance(type); var method = type.GetMethod("CreateInstance", BindingFlags.Static | BindingFlags.Public); var data = method.Invoke(obj, null); if (args.SelectedItemContainer != null) { if ((string)args.SelectedItemContainer?.Tag == "SamplePage1") { contentFrame.Navigate(typeof(BlankPage1), (ClassLibrary1.Class1)data, new SlideNavigationTransitionInfo() { Effect = SlideNavigationTransitionEffect.FromRight }); } } } }
で、ページの中のOnNavigatedTo()
で、👆で渡したインスタンスを受けるのだが、その際にClassLibrary1.Class1
でキャストして受けると、
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Navigation; namespace FileVerUpTool; public sealed partial class BlankPage1 : Page { public BlankPage1() => this.InitializeComponent(); protected override void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); var a = (ClassLibrary1.Class1)e.Parameter; } }
上で挙げた例外が起きる。
これが、なんでおきるのかさっぱりわからなかった。
原因
- `Assembly.LoadFile()で読み込んだ「dll」の中のクラスと、
- プロジェクトの「参照」に追加して読み込んだ「dll」の中のクラスは、
別物扱いだった。
ここに書かれていた。
最初、NavigationViewの「Navigate()」メソッドのパラメータが怪しい?とか思ってたが、そこは関係なかった。
対策
Assembly.LoadFile()
の代わりに、Assembly.LoadFrom()
でdllをロードする。
dllのロード動作
Assembly.LoadFile()
使用時の、出力欄のモジュール読み込みメッセージはコレ。
1回目も2回目も、両方「シンボルが読み込まれました」となっている。
Assembly.LoadFrom()
使用時の、出力欄のモジュール読み込みメッセージはコレ。
どうやら、LoadFrom()のときは、同じdllを2回目によんだときに、アンロードしている様子。
(LoadFileのときは、アンロードは行われず、「シンボルが読み込まれました」となっている)
VisualStudioで「参照」に入れるDLLを、動的にロードしないといけないようなときは、「Assembly.LoadFile()」ではなく「Assembly.LoadFrom()」使えばよいっぽい。
※ただ、そういうことにならないように、どちらか一方(参照に入れるor動的にロードする)だけにした方が自然なのだろうなとは思う。
実験② LoadFileとLoadFromの実験
全く同じDLLを、別のフォルダに置いてみて、その2つをLoadFile、LoadFromをしてみた。
using System.Reflection; namespace ConsoleApp12; internal class Program { private static string path1 = @"C:\Users\masa\source\repos\ConsoleApp12\dll1\ClassLibrary1.dll"; private static string path2 = @"C:\Users\masa\source\repos\ConsoleApp12\dll2\ClassLibrary1.dll"; static void Main(string[] args) { Assembly assembly1 = Assembly.LoadFrom(path1); Assembly assembly2 = Assembly.LoadFrom(path2); assembly1 = Assembly.LoadFile(path1); assembly2 = Assembly.LoadFile(path2); } }
出力
0 'ConsoleApp12.exe' (CoreCLR: clrhost): 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.8\System.Collections.Concurrent.dll' が読み込まれました。シンボルの読み込みをスキップしました。モジュールは最適化されていて、デバッグ オプションの [マイ コードのみ] 設定が有効になっています。 'ConsoleApp12.exe' (CoreCLR: clrhost): 'C:\Users\masa\source\repos\ConsoleApp12\dll1\ClassLibrary1.dll' が読み込まれました。シンボルが読み込まれました。 1 2 'ConsoleApp12.exe' (CoreCLR: clrhost): 'C:\Users\masa\source\repos\ConsoleApp12\dll1\ClassLibrary1.dll' が読み込まれました。シンボルが読み込まれました。 3 'ConsoleApp12.exe' (CoreCLR: clrhost): 'C:\Users\masa\source\repos\ConsoleApp12\dll2\ClassLibrary1.dll' が読み込まれました。シンボルが読み込まれました。
結果、
- LoadFileは、別の同じ名前のファイルを、それぞれ別のAssemblyとして読み込んだ。
- LoadFromは、別の同じ名前のファイルを、1個目のみ読み込んで、2個目は1個目と同じdllを使ったっぽい。
実験③ LoadFileとLoadFromの実験(全く同じdllの一方を無理やりリネームしてみる)
実験②のpath2のほうのdllを無理やり「ClassLibrary1aaa.dll」にリネームして、コード中のpathもそれに合わせて変えてやって、動的に読み込んでみる。
using System.Diagnostics; using System.Reflection; namespace ConsoleApp12; internal class Program { private static string path1 = @"C:\Users\masa\source\repos\ConsoleApp12\dll1\ClassLibrary1.dll"; private static string path2 = @"C:\Users\masa\source\repos\ConsoleApp12\dll2\ClassLibrary1aaa.dll"; static void Main(string[] args) { int ctr = 0; Debug.WriteLine($"{ctr++}"); var assembly1 = Assembly.LoadFrom(path1); Debug.WriteLine($"{ctr++}"); var assembly2 = Assembly.LoadFrom(path2); // ----------------------------- Debug.WriteLine($"{ctr++}"); var assembly11 = Assembly.LoadFile(path1); Debug.WriteLine($"{ctr++}"); var assembly12 = Assembly.LoadFile(path2); } }
出力
0 'ConsoleApp12.exe' (CoreCLR: clrhost): 'C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\PrivateAssemblies\Runtime\Microsoft.VisualStudio.Debugger.Runtime.NetCoreApp.dll' が読み込まれました。シンボルの読み込みをスキップしました。モジュールは最適化されていて、デバッグ オプションの [マイ コードのみ] 設定が有効になっています。 'ConsoleApp12.exe' (CoreCLR: clrhost): 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.8\netstandard.dll' が読み込まれました。シンボルの読み込みをスキップしました。モジュールは最適化されていて、デバッグ オプションの [マイ コードのみ] 設定が有効になっています。 'ConsoleApp12.exe' (CoreCLR: clrhost): 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.8\System.Collections.Concurrent.dll' が読み込まれました。シンボルの読み込みをスキップしました。モジュールは最適化されていて、デバッグ オプションの [マイ コードのみ] 設定が有効になっています。 'ConsoleApp12.exe' (CoreCLR: clrhost): 'C:\Users\masa\source\repos\ConsoleApp12\dll1\ClassLibrary1.dll' が読み込まれました。シンボルが読み込まれました。 1 2 'ConsoleApp12.exe' (CoreCLR: clrhost): 'C:\Users\masa\source\repos\ConsoleApp12\dll1\ClassLibrary1.dll' が読み込まれました。シンボルが読み込まれました。 3 'ConsoleApp12.exe' (CoreCLR: clrhost): 'C:\Users\masa\source\repos\ConsoleApp12\dll2\ClassLibrary1aaa.dll' が読み込まれました。シンボルが読み込まれました。
結果、
- ファイル名が別であっても、中身が同じであれば、LoadFromは同じdllとして、2つ目のLoadFromでは、読み込みを行わなかった。
参考
アセンブリの読み込みのベスト プラクティス
1 つのアセンブリを複数のコンテキストに読み込まない
Assembly.LoadFile()
は、別のpathに置かれた同じdllを読むけど、
Assembly.LoadFrom()
は、それを読まない、と書いてるっぽい。
Difference between LoadFile and LoadFrom with .NET Assemblies?
👆のMSdocsが何かいてるかわからん!という記事。同意...