Prism: 런타임에 모듈을 동적으로 발견하고 불러옵니다.
Prism을 사용하여 WPF 응용 프로그램을 개발하는 경우 모듈을 로드할 수 있는 여러 가지 방법을 이미 알고 있을 것입니다.
모듈 로드는 ModuleCatalog라고 하는 것으로 시작됩니다. ModuleCatalog에 추가되지 않은 경우 모듈을 로드할 수 없습니다. 모듈이 ModuleCatalog에 추가되면, Prism이 모듈 어셈블리를 로드하는 작업을 담당합니다. Prism은 Prism 애플리케이션에 모듈을 등록하는 방법에 유연성을 제공하기 위해 몇 가지 모듈 카탈로그와 함께 제공됩니다. 코드, XAML, app.config의 XML 또는 디렉터리에서 모듈 카탈로그를 채울 수 있습니다. 젠장, 이러한 모든 옵션의 조합을 사용하여 모듈 카탈로그를 채울 수도 있습니다.
공개 행사나 내부 점심 식사에서 Prism 강연을 하고 회사에서 배울 때 모듈을 로드하는 다양한 방법과 사용할 카탈로그를 모두 설명할 것입니다. 이 시기는 질문이 정말 흥미로워지기 시작하는 시기입니다. 이러한 질문 중 가장 일반적인 질문은 DirectoryModuleCatalog에 대한 것입니다. 이 특정 카탈로그를 사용하면 모듈을 로드할 폴더 경로를 지정할 수 있습니다. 이제 흥미로운 질문 ... "하지만 새 모듈 어셈블리를 폴더에 끌어다 놓으면 어떻게 될까요? 앱이 실행되는 동안 자동으로 로드되나요?" 그것은 훌륭한 질문이며 대답은 NO 입니다. DirectoryModuleCatalog는 디렉터리를 한 번 검색한 다음 검색된 모든 모듈을 로드합니다. 새 모듈 어셈블리를 디렉터리에 놓으면 응용 프로그램이 다시 시작될 때까지 로드되지 않습니다. 이제 후속 질문 ... "글쎄, 모듈을 동적으로 발견하고 디렉토리에서도로드 할 수 있습니까?" 대답; 글쎄요, 물론입니다. MEF를 사용하는 경우 쉽습니다. Unity와 같은 컨테이너를 사용하는 경우 직접 처리할 코드를 작성해야 합니다. "음, 우리는 MEF를 사용하지 않는데, 어떻게 하는지 알려주실 수 있으신가요?" 이것은 내 대답이 항상 같은 곳입니다, "간단한 웹 검색 (Google 또는 Bing)은 당신이 찾고있는 것을 찾는 데 도움이 될 것입니다".
글쎄, 그것은 사실이 아니라는 것이 밝혀졌습니다. Unity와 같은 DI 컨테이너를 사용하여 모듈의 동적 검색 및로드를 처리하는 코드에 대해 블로그를 작성하거나 공유한 사람은 아무도 없는 것 같습니다. 내가 찾을 수 없었고, 나에게 보여달라고 요청하는 사람도 찾을 수 없었습니다. 이 게시물로 연결됩니다. 이러한 시나리오를 지원하기 위해 사용한 접근 방식을 보여 드리겠습니다. 실제로 두 가지 접근 방식을 제시 할 것입니다. 하나는 "빠르고 더러운" 방식입니다. 기본적으로, 나는 목표를 달성하기 위해 가장 간단한 샘플을 함께 던질 것이다. 그런 다음 이 기능을 모든 것을 처리할 사용자 지정 ModuleCatalog에 캡슐화하는 "더 나은 방법"을 보여 드리겠습니다.
다음은 코드를 테스트하는 데 사용하는 Prism 앱입니다.

단일 영역이 있는 Shell과 단일 보기를 포함하는 하나의 모듈을 포함하는 Prism 애플리케이션입니다. 모듈이 제대로 로드되면 이것이 최종 결과가 됩니다.

대강대강
"빠르고 더러운"방법은 글쎄요 .... 대강대강. 먼저 새 모듈 어셈블리가 modules 디렉토리에 추가되었을 때 감지하기 위해 사용할 메커니즘을 결정해야 합니다. 이것은 생각할 필요가 없습니다. 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의 생성자에서는 모니터링하려는 디렉토리의 위치와 필터를 지정할 수 있는 두 번째 매개 변수를 사용합니다. 이 경우 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을 구현하는 모든 클래스를 검색하며, 이는 Prism 모듈을 나타내는 인터페이스입니다. 다음으로 검색된 모든 모듈을 반복하여 ModuleCatalog에 추가합니다. 모듈 카탈로그에 등록되지 않은 모듈은 로드할 수 없기 때문에 이 작업을 수행해야 합니다. 이제 IModuleManager를 사용하여 Dispatcher를 사용하여 모듈을 로드합니다. FileSystemWatcher.Created 이벤트는 별도의 스레드에서 수신 대기하기 때문에 Dispatcher를 사용해야 하고 주 스레드에서 모듈을 로드해야 합니다. Dispatchers를 사용하면 다른 스레드에서 메인 스레드로 모듈을 푸시할 수 있습니다. 이제 응용 프로그램을 실행하고 ModuleA.DLL 응용 프로그램의 Modules 폴더 디렉토리에 복사하고 어떤 일이 발생하는지 확인하겠습니다.
Before:

응용 프로그램을 실행하고 응용 프로그램의 /Modules 디렉토리 위치와 ModuleA의 Bin/Debug/ModuleA.dll 파일 위치를 엽니다. 보시다시피 응용 프로그램에 대해 로드된 모듈이 없으며 Prism 응용 프로그램에 빈 셸이 표시됩니다.
After:

이제 모듈의 Bin/Debug 디렉토리에서 Prism 애플리케이션의 /Modules 디렉토리로 ModuleA.dll 복사합니다. 복사 작업이 완료되는 즉시 ModuleA.dll 어셈블리가 로드되고 ModuleAView가 Shell에 주입됩니다. s 영역. 앱이 실행되는 동안 모두. 앱을 종료하고 다시 시작할 필요가 없습니다.
그래서 그것은 빠르고 더러운 방법이었습니다. 이제 기본 Prism DirectoryModuleCatalog와 같이 디렉토리에서 모듈을 로드할 뿐만 아니라 런타임에 새로 추가된 모듈의 디렉토리를 모니터링할 수 있는 사용자 정의 ModuleCatalog를 만드는 방법을 살펴보겠습니다.
A Better Way
방금 몇 줄의 코드로 모듈을 동적으로 검색하고 로드하는 방법을 살펴보았습니다. 이제 디렉터리에서 기존 모듈을 등록 및 로드할 뿐만 아니라 런타임에 새로 추가된 모듈에 대해 동일한 디렉터리를 모니터링하는 사용자 지정 ModuleCatalog 클래스를 만들어 보겠습니다. 이 클래스는 좀 더 안정적이어야하며 실제로 필요할 때까지 어셈블리를 주 앱 도메인에로드하지 않고 적절한 앱 도메인 및 증거 생성 및 메모리 리플렉션을 수행해야합니다. 또한 Dispatcher에 대한 종속성을 제거하고 대신 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 부트스트래퍼에서 새로 생성된 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 애플리케이션의 여러 인스턴스가 동일한 디렉토리를 모니터링하고 동일한 모듈을 로드하도록 할 수도 있습니다.

꽤 멋지죠? 이제 런타임에 Prism 모듈을 동적으로 검색하고 로드할 수 있습니다.
항상 그렇듯이 제 블로그로 저에게 연락하거나, Twitter(@brianlagunas)에서 저에게 연락하거나, 질문이나 의견이 있는 경우 아래에 의견을 남겨주세요.