Prism:実行時にモジュールを動的に発見・ロード
Prismを使用してWPFアプリケーションを開発する場合は、モジュールをロードする多くの方法をすでにご存知でしょう。
モジュールの読み込みは、モジュールカタログと呼ばれるものから始まります。 モジュールがモジュールカタログに追加されていない限り、モジュールを読み込むことはできません。 モジュールがModuleCatalogに追加されると、Prismがモジュールアセンブリの読み込みを代行してくれます。 Prismにはモジュールカタログもいくつか付属しており、Prismアプリケーションでモジュール登録の柔軟性を持てます。 モジュールカタログはコード、XAMLから、app.configのXMLやディレクトリから入力できます。 さらに、これらすべてのオプションを組み合わせてモジュールカタログを埋めることも可能です。
私が公開イベントでプリズム講演をしたり、社内でのランチ&ラーニングで話したりするときは、モジュールの読み込み方法やどのカタログを使うべきかを必ず説明します。 ここからが本当に面白くなってくる問いの始まりです。 これらの質問の中で最も一般的なのはDirectoryModuleCatalogに関するものです。 この特定のカタログでは、モジュールを読み込むフォルダパスを指定できます。 さて、興味深い質問です...「でも、新しいモジュールアセンブリがフォルダに入ったらどうなるの? 実行中は自動的にアプリに読み込まれますか?」 それは素晴らしい質問で、答えは「いいえ」です。 DirectoryModuleCatalogはディレクトリを一度スキャンし、見つけたすべてのモジュールを読み込みます。 新しいモジュールアセンブリをディレクトリに置くと、アプリケーションが再起動されるまで読み込まれません。 さて、続編の質問です...「では、モジュールを動的に発見してディレクトリから読み込むことは可能ですか?」 答える;もちろんそうだよ。 MEFを使っているなら簡単です。 Unityのようなコンテナを使う場合は、自分でコードを書く必要があります。 「ええと、MEFは使わないから、どうやって使うか教えてくれない?」 ここで私の返答はいつも同じで、「簡単なウェブ検索(GoogleやBing)で探しているものが見つかるはずです」と言います。
しかし、実際にはそうではありませんでした。 UnityのようなDIコンテナを使ってモジュールの動的な発見や読み込みを行うコードについて、誰もブログを書いたり共有したりしていないようです。 私が見つけられなかったし、見せてほしいと頼まれている人たちも見つけられなかった。 これがこの投稿につながります。 私がそのようなシナリオを支援するために用いてきたアプローチをお見せします。 実は2つのアプローチを紹介します。 一つは「手早くてダーティな方法」です。 基本的には、目標を達成するために最も簡単なサンプルをまとめます。 次に「A Better Way」をご紹介します。これは、この機能をカスタムModuleCatalogにカプセル化し、すべてを処理してくれます。
こちらが私たちがコードのテストに使っているPrismアプリです。

これは、単一のリージョンを持つシェルと、1つのビューを含むモジュールを含むPrismアプリケーションです。 モジュールが正しく積み込まれたときに、これが最終的な結果となります。

間に合わせの
「手早くて汚い」方法は、まあ......間に合わせの。 まず、新しいモジュールアセンブリがモジュールディレクトリに追加されたことを検出するために使う仕組みを決める必要があります。 これは迷うまでもない選択です。 FileSystemWatcherクラスを使用します。 FileSystemWatcherは指定されたディレクトリの変更を監視し、変更が発生したことをイベントを通じて通知します。 例えば、ディレクトリにファイルが追加されるなどです。 このクラスのインスタンスをBootstrapperコンストラクタで作成し、そのCreatedイベントを受信します。
public Bootstrapper()
{
// we need to watch our folder for newly added modules
FileSystemWatcher fileWatcher = new FileSystemWatcher(Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "Modules"), "*.dll");
fileWatcher.Created += fileWatcher_Created;
fileWatcher.EnableRaisingEvents = true;
}
FileSystemWatcherのコンストラクタでは、監視したいディレクトリの位置と、フィルターを指定するための2つ目のパラメータが取られています。 この場合、私たちが気にするのはDLLだけです。 ディレクトリの監視を開始するには、FileSystemWatcher.EnableRaisingEvents = trueを設定する必要があります。 新しいDLLがディレクトリに追加されるたびに、イベントハンドラが実行されます。 イベントハンドラーをチェックしましょう
void fileWatcher_Created(object sender, FileSystemEventArgs e)
{
if (e.ChangeType == WatcherChangeTypes.Created)
{
//get the Prism assembly that IModule is defined in
Assembly moduleAssembly = AppDomain.CurrentDomain.GetAssemblies().First(asm => asm.FullName == typeof(IModule).Assembly.FullName);
Type IModuleType = moduleAssembly.GetType(typeof(IModule).FullName);
//load our newly added assembly
Assembly assembly = Assembly.LoadFile(e.FullPath);
//look for all the classes that implement IModule in our assembly and create a ModuleInfo class from it
var moduleInfos = assembly.GetExportedTypes()
.Where(IModuleType.IsAssignableFrom)
.Where(t => t != IModuleType)
.Where(t => !t.IsAbstract).Select(t => CreateModuleInfo(t));
//create an instance of our module manager
var moduleManager = Container.Resolve<IModuleManager>();
foreach (var moduleInfo in moduleInfos)
{
//add the ModuleInfo to the catalog so it can be loaded
ModuleCatalog.AddModule(moduleInfo);
//now load the module using the Dispatcher because the FileSystemWatcher.Created even occurs on a separate thread
//and we need to load our module into the main thread.
var d = Application.Current.Dispatcher;
if (d.CheckAccess())
moduleManager.LoadModule(moduleInfo.ModuleName);
else
d.BeginInvoke((Action)delegate { moduleManager.LoadModule(moduleInfo.ModuleName); });
}
}
}
private static ModuleInfo CreateModuleInfo(Type type)
{
string moduleName = type.Name;
var moduleAttribute = CustomAttributeData.GetCustomAttributes(type).FirstOrDefault(cad => cad.Constructor.DeclaringType.FullName == typeof(ModuleAttribute).FullName);
if (moduleAttribute != null)
{
foreach (CustomAttributeNamedArgument argument in moduleAttribute.NamedArguments)
{
string argumentName = argument.MemberInfo.Name;
if (argumentName == "ModuleName")
{
moduleName = (string)argument.TypedValue.Value;
break;
}
}
}
ModuleInfo moduleInfo = new ModuleInfo(moduleName, type.AssemblyQualifiedName)
{
InitializationMode = InitializationMode.OnDemand,
Ref = type.Assembly.CodeBase,
};
return moduleInfo;
}
このコードは新たに追加されたアセンブリをアプリケーションに読み込みます。 次にアセンブリ内でIModuleを実装するすべてのクラスを検索します。IModuleはPrismモジュールを表すインターフェースです。 次に、見つけたすべてのモジュールをループさせてModuleCatalogに追加します。 モジュールカタログに登録されていないモジュールを読み込めないため、これを行う必要があります。 今はIModuleManagerを使ってディスパチャーを使ってモジュールを読み込みます。 FileSystemWatcher.Createdイベントは別のスレッドでリスニングされるため、モジュールをメインスレッドに読み込む必要があるため、Dispatcherを使う必要があります。 Dispatchersは、モジュールを別のスレッドからメインスレッドにプッシュすることを可能にします。 では、アプリケーションを実行してModuleA.DLLをアプリケーションのモジュールフォルダディレクトリにコピーし、どうなるか見てみましょう。
Before:

アプリケーションを実行し、アプリケーションの/Modulesディレクトリの位置とModuleAのBin/Debug/ModuleA.dllファイルの位置を開きます。 ご覧の通り、アプリケーション用のモジュールは読み込まれておらず、Prismアプリケーションは空のシェルを示しています。
After:

次に、モジュールのBin/DebugディレクトリからPrismアプリケーションの/ModulesディレクトリにModuleA.dllをコピーします。 コピー操作が完了するとすぐにModuleA.dll組立が読み込まれ、モジュールAViewがシェルに注入されます。S領域。 その間ずっとアプリが動いている間に。 アプリをシャットダウンして再起動する必要はありません。
それが手早く粗い方法だった。 では、デフォルトのPrism DirectoryModuleCatalogのようにディレクトリからモジュールを読み込むだけでなく、実行時に新たに追加されたモジュールのディレクトリを監視できるカスタムModuleCatalogを作る方法を見てみましょう。
A Better Way
数行のコードで動的にモジュールを発見し読み込むことができるのを見たばかりです。 次に、既存のモジュールをディレクトリから登録・読み込みするだけでなく、実行時にそのディレクトリで新たに追加されたモジュールを監視するカスタムModuleCatalogクラスを作成しましょう。 このクラスはもう少し安定していて、適切なアプリケーションドメインや証拠の作成、メモリリフレクションを行い、実際に必要な時までアセンブリをメインのアプリドメインに読み込むことはありません。 また、ディスパッチャへの依存を排除し、代わりにSynchronizationContextクラスを使用します。 すべてのコードを一通り説明するつもりはありません。 コードを提供するので、読んでください。
public class DynamicDirectoryModuleCatalog : ModuleCatalog
{
SynchronizationContext _context;
/// <summary>
/// Directory containing modules to search for.
/// </summary>
public string ModulePath { get; set; }
public DynamicDirectoryModuleCatalog(string modulePath)
{
_context = SynchronizationContext.Current;
ModulePath = modulePath;
// we need to watch our folder for newly added modules
FileSystemWatcher fileWatcher = new FileSystemWatcher(ModulePath, "*.dll");
fileWatcher.Created += FileWatcher_Created;
fileWatcher.EnableRaisingEvents = true;
}
/// <summary>
/// Rasied when a new file is added to the ModulePath directory
/// </summary>
void FileWatcher_Created(object sender, FileSystemEventArgs e)
{
if (e.ChangeType == WatcherChangeTypes.Created)
{
LoadModuleCatalog(e.FullPath, true);
}
}
/// <summary>
/// Drives the main logic of building the child domain and searching for the assemblies.
/// </summary>
protected override void InnerLoad()
{
LoadModuleCatalog(ModulePath);
}
void LoadModuleCatalog(string path, bool isFile = false)
{
if (string.IsNullOrEmpty(path))
throw new InvalidOperationException("Path cannot be null.");
if (isFile)
{
if (!File.Exists(path))
throw new InvalidOperationException(string.Format("File {0} could not be found.", path));
}
else
{
if (!Directory.Exists(path))
throw new InvalidOperationException(string.Format("Directory {0} could not be found.", path));
}
AppDomain childDomain = this.BuildChildDomain(AppDomain.CurrentDomain);
try
{
List<string> loadedAssemblies = new List<string>();
var assemblies = (
from Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()
where !(assembly is System.Reflection.Emit.AssemblyBuilder)
&& assembly.GetType().FullName != "System.Reflection.Emit.InternalAssemblyBuilder"
&& !String.IsNullOrEmpty(assembly.Location)
select assembly.Location
);
loadedAssemblies.AddRange(assemblies);
Type loaderType = typeof(InnerModuleInfoLoader);
if (loaderType.Assembly != null)
{
var loader = (InnerModuleInfoLoader)childDomain.CreateInstanceFrom(loaderType.Assembly.Location, loaderType.FullName).Unwrap();
loader.LoadAssemblies(loadedAssemblies);
//get all the ModuleInfos
ModuleInfo[] modules = loader.GetModuleInfos(path, isFile);
//add modules to catalog
this.Items.AddRange(modules);
//we are dealing with a file from our file watcher, so let's notify that it needs to be loaded
if (isFile)
{
LoadModules(modules);
}
}
}
finally
{
AppDomain.Unload(childDomain);
}
}
/// <summary>
/// Uses the IModuleManager to load the modules into memory
/// </summary>
/// <param name="modules"></param>
private void LoadModules(ModuleInfo[] modules)
{
if (_context == null)
return;
IModuleManager manager = ServiceLocator.Current.GetInstance<IModuleManager>();
_context.Send(new SendOrPostCallback(delegate(object state)
{
foreach (var module in modules)
{
manager.LoadModule(module.ModuleName);
}
}), null);
}
/// <summary>
/// Creates a new child domain and copies the evidence from a parent domain.
/// </summary>
/// <param name="parentDomain">The parent domain.</param>
/// <returns>The new child domain.</returns>
/// <remarks>
/// Grabs the <paramref name="parentDomain"/> evidence and uses it to construct the new
/// <see cref="AppDomain"/> because in a ClickOnce execution environment, creating an
/// <see cref="AppDomain"/> will by default pick up the partial trust environment of
/// the AppLaunch.exe, which was the root executable. The AppLaunch.exe does a
/// create domain and applies the evidence from the ClickOnce manifests to
/// create the domain that the application is actually executing in. This will
/// need to be Full Trust for Composite Application Library applications.
/// </remarks>
/// <exception cref="ArgumentNullException">An <see cref="ArgumentNullException"/> is thrown if <paramref name="parentDomain"/> is null.</exception>
protected virtual AppDomain BuildChildDomain(AppDomain parentDomain)
{
if (parentDomain == null) throw new System.ArgumentNullException("parentDomain");
Evidence evidence = new Evidence(parentDomain.Evidence);
AppDomainSetup setup = parentDomain.SetupInformation;
return AppDomain.CreateDomain("DiscoveryRegion", evidence, setup);
}
private class InnerModuleInfoLoader : MarshalByRefObject
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
internal ModuleInfo[] GetModuleInfos(string path, bool isFile = false)
{
Assembly moduleReflectionOnlyAssembly =
AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().First(
asm => asm.FullName == typeof(IModule).Assembly.FullName);
Type IModuleType = moduleReflectionOnlyAssembly.GetType(typeof(IModule).FullName);
FileSystemInfo info = null;
if (isFile)
info = new FileInfo(path);
else
info = new DirectoryInfo(path);
ResolveEventHandler resolveEventHandler = delegate(object sender, ResolveEventArgs args) { return OnReflectionOnlyResolve(args, info); };
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += resolveEventHandler;
IEnumerable<ModuleInfo> modules = GetNotAllreadyLoadedModuleInfos(info, IModuleType);
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= resolveEventHandler;
return modules.ToArray();
}
private static IEnumerable<ModuleInfo> GetNotAllreadyLoadedModuleInfos(FileSystemInfo info, Type IModuleType)
{
List<FileInfo> validAssemblies = new List<FileInfo>();
Assembly[] alreadyLoadedAssemblies = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies();
FileInfo fileInfo = info as FileInfo;
if (fileInfo != null)
{
if (alreadyLoadedAssemblies.FirstOrDefault(assembly => String.Compare(Path.GetFileName(assembly.Location), fileInfo.Name, StringComparison.OrdinalIgnoreCase) == 0) == null)
{
var moduleInfos = Assembly.ReflectionOnlyLoadFrom(fileInfo.FullName).GetExportedTypes()
.Where(IModuleType.IsAssignableFrom)
.Where(t => t != IModuleType)
.Where(t => !t.IsAbstract).Select(t => CreateModuleInfo(t));
return moduleInfos;
}
}
DirectoryInfo directory = info as DirectoryInfo;
var files = directory.GetFiles("*.dll").Where(file => alreadyLoadedAssemblies.
FirstOrDefault(assembly => String.Compare(Path.GetFileName(assembly.Location), file.Name, StringComparison.OrdinalIgnoreCase) == 0) == null);
foreach (FileInfo file in files)
{
try
{
Assembly.ReflectionOnlyLoadFrom(file.FullName);
validAssemblies.Add(file);
}
catch (BadImageFormatException)
{
// skip non-.NET Dlls
}
}
return validAssemblies.SelectMany(file => Assembly.ReflectionOnlyLoadFrom(file.FullName)
.GetExportedTypes()
.Where(IModuleType.IsAssignableFrom)
.Where(t => t != IModuleType)
.Where(t => !t.IsAbstract)
.Select(type => CreateModuleInfo(type)));
}
private static Assembly OnReflectionOnlyResolve(ResolveEventArgs args, FileSystemInfo info)
{
Assembly loadedAssembly = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().FirstOrDefault(
asm => string.Equals(asm.FullName, args.Name, StringComparison.OrdinalIgnoreCase));
if (loadedAssembly != null)
{
return loadedAssembly;
}
DirectoryInfo directory = info as DirectoryInfo;
if (directory != null)
{
AssemblyName assemblyName = new AssemblyName(args.Name);
string dependentAssemblyFilename = Path.Combine(directory.FullName, assemblyName.Name + ".dll");
if (File.Exists(dependentAssemblyFilename))
{
return Assembly.ReflectionOnlyLoadFrom(dependentAssemblyFilename);
}
}
return Assembly.ReflectionOnlyLoad(args.Name);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
internal void LoadAssemblies(IEnumerable<string> assemblies)
{
foreach (string assemblyPath in assemblies)
{
try
{
Assembly.ReflectionOnlyLoadFrom(assemblyPath);
}
catch (FileNotFoundException)
{
// Continue loading assemblies even if an assembly can not be loaded in the new AppDomain
}
}
}
private static ModuleInfo CreateModuleInfo(Type type)
{
string moduleName = type.Name;
List<string> dependsOn = new List<string>();
bool onDemand = false;
var moduleAttribute = CustomAttributeData.GetCustomAttributes(type).FirstOrDefault(cad => cad.Constructor.DeclaringType.FullName == typeof(ModuleAttribute).FullName);
if (moduleAttribute != null)
{
foreach (CustomAttributeNamedArgument argument in moduleAttribute.NamedArguments)
{
string argumentName = argument.MemberInfo.Name;
switch (argumentName)
{
case "ModuleName":
moduleName = (string)argument.TypedValue.Value;
break;
case "OnDemand":
onDemand = (bool)argument.TypedValue.Value;
break;
case "StartupLoaded":
onDemand = !((bool)argument.TypedValue.Value);
break;
}
}
}
var moduleDependencyAttributes = CustomAttributeData.GetCustomAttributes(type).Where(cad => cad.Constructor.DeclaringType.FullName == typeof(ModuleDependencyAttribute).FullName);
foreach (CustomAttributeData cad in moduleDependencyAttributes)
{
dependsOn.Add((string)cad.ConstructorArguments[0].Value);
}
ModuleInfo moduleInfo = new ModuleInfo(moduleName, type.AssemblyQualifiedName)
{
InitializationMode =
onDemand
? InitializationMode.OnDemand
: InitializationMode.WhenAvailable,
Ref = type.Assembly.CodeBase,
};
moduleInfo.DependsOn.AddRange(dependsOn);
return moduleInfo;
}
}
}
/// <summary>
/// Class that provides extension methods to Collection
/// </summary>
public static class CollectionExtensions
{
/// <summary>
/// Add a range of items to a collection.
/// </summary>
/// <typeparam name="T">Type of objects within the collection.</typeparam>
/// <param name="collection">The collection to add items to.</param>
/// <param name="items">The items to add to the collection.</param>
/// <returns>The collection.</returns>
/// <exception cref="System.ArgumentNullException">An <see cref="System.ArgumentNullException"/> is thrown if <paramref name="collection"/> or <paramref name="items"/> is <see langword="null"/>.</exception>
public static Collection<T> AddRange<T>(this Collection<T> collection, IEnumerable<T> items)
{
if (collection == null) throw new System.ArgumentNullException("collection");
if (items == null) throw new System.ArgumentNullException("items");
foreach (var each in items)
{
collection.Add(each);
}
return collection;
}
}
これが、Prism Bootstrapperで新しく作成されたDynamicDirectoryModuleCatalogの使い方です。
protected override IModuleCatalog CreateModuleCatalog()
{
DynamicDirectoryModuleCatalog catalog = new DynamicDirectoryModuleCatalog(Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "Modules"));
return catalog;
}
{
DynamicDirectoryModuleCatalog catalog = new DynamicDirectoryModuleCatalog(Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, “Modules”));
return catalog;
}
ご存じないかもしれませんが、Prismアプリケーションの複数のインスタンスが同じディレクトリを監視し、同じモジュールを読み込むことも可能です。

なかなかかいいですよね? 今では実行時に動的にプリズムモジュールを発見・ロードできるようになりました。
いつも通り、私のブログでご連絡いただくか、Twitter(@brianlagunas)でつながるか、ご質問やご意見があれば下のコメント欄にお寄せください。