Prism: Descubra y cargue módulos dinámicamente en tiempo de ejecución
Si desarrolla aplicaciones de WPF con Prism, probablemente ya conozca las muchas formas en que puede cargar un módulo.
La carga de un módulo comienza con lo que se denomina ModuleCatalog. No se puede cargar un módulo a menos que se haya agregado a un ModuleCatalog. Una vez que el módulo se haya agregado a un ModuleCatalog, Prism se encargará de cargar el ensamblaje del módulo por usted. Prism incluso viene con un puñado de catálogos de módulos para darle flexibilidad en la forma en que registra sus módulos con su aplicación Prism. Puede rellenar un catálogo de módulos a partir de código, de XAML, con XML en un archivo app.config o de un directorio. Incluso puede usar una combinación de todas estas opciones para completar su catálogo de módulos.
Cuando doy una charla sobre Prisma en un evento público o en un almuerzo interno y aprendo en una empresa, me aseguro de explicar todas las diferentes formas de cargar sus módulos y qué catálogo usar. Este es el momento en el que las preguntas realmente comienzan a ponerse interesantes. De estas preguntas, la más común es sobre DirectoryModuleCatalog. Este catálogo en particular le permite especificar una ruta de carpeta desde la que cargar sus módulos. Ahora la pregunta interesante... "Pero, ¿qué sucede cuando se coloca un nuevo ensamblaje de módulo en la carpeta? ¿Se cargará automáticamente en la aplicación mientras se está ejecutando?" Esa es una gran pregunta, y la respuesta es NO. DirectoryModuleCatalog realiza un examen único del directorio y, a continuación, carga todos los módulos que encuentra. Si coloca un nuevo ensamblado de módulo en el directorio, no se cargará hasta que se reinicie la aplicación. Ahora la pregunta de seguimiento... "Bueno, ¿es posible descubrir dinámicamente los módulos y cargarlos también desde el directorio?" Respuesta; Bueno, por supuesto que lo es. Si estás usando MEF, es fácil. Si usas un contenedor como Unity, tendrás que escribir el código para manejarlo tú mismo. "Bueno, no usamos MEF, ¿entonces puedes mostrarnos cómo?" Aquí es donde mi respuesta es siempre la misma, "una simple búsqueda en la web (Google o Bing) debería ayudarte a encontrar lo que buscas".
Bueno, resulta que ese no es el caso. Parece que nadie ha blogueado ni compartido ningún código que maneje el descubrimiento dinámico y la carga de módulos utilizando un contenedor DI como Unity. No es que yo haya podido encontrar, ni nadie que me pida que se los muestre ha podido encontrarlos. Lo que me lleva a este post. Voy a mostrarles un enfoque que he utilizado para apoyar tal escenario. De hecho, les voy a dar dos enfoques. Una es la forma "rápida y sucia". Básicamente, juntaré la muestra más simple para lograr el objetivo. A continuación, te mostraré "Una mejor manera" en la que encapsularemos esta funcionalidad en un ModuleCatalog personalizado que se encargará de todo por nosotros.
Aquí está la aplicación Prism que estamos usando para probar nuestro código.

Es una aplicación Prism que contiene un Shell con una sola región y un módulo que contiene una sola vista. Cuando el módulo esté cargado correctamente, este será el resultado final.

Con las patas
La forma "Rápida y Sucia", es bueno.... Con las patas. Primero tenemos que determinar qué mecanismo vamos a utilizar para detectar cuándo se ha añadido un nuevo ensamblaje de módulos a nuestro directorio de módulos. Esto es una obviedad. Usaremos la clase FileSystemWatcher. El FileSystemWatcher supervisa un directorio determinado en busca de cambios y nos notifica a través de eventos que se ha producido un cambio. Por ejemplo, un archivo que se agrega al directorio. Vamos a crear una instancia de esta clase en nuestro constructor Bootstrapper y escuchar su evento 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;
}
Nótese que en el constructor del FileSystemWatcher, toma la ubicación del directorio que queremos monitorear, así como un segundo parámetro que nos permite especificar un filtro. En este caso, solo nos preocupamos por las DLL. También necesitamos establecer el FileSystemWatcher.EnableRaisingEvents = true para iniciar el monitoreo del directorio. Ahora, cada vez que se agregue una nueva DLL a nuestro directorio, se ejecutará nuestro controlador de eventos. Es hora de echar un vistazo a nuestro controlador de eventos
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;
}
Este código toma el ensamblado recién agregado y lo carga en nuestra aplicación. A continuación, buscamos en el ensamblado todas las clases que implementan IModule, que es la interfaz que especifica que representa un módulo Prism. A continuación, recorremos todos los módulos encontrados y los agregamos al ModuleCatalog. Tenemos que hacer esto porque no podemos cargar un módulo que no está registrado en el catálogo de módulos. Ahora usamos IModuleManager para cargar el módulo usando el Dispatcher. Tenemos que usar el Dispatcher porque el evento FileSystemWatcher.Created escucha en un subproceso separado y necesitamos cargar los módulos en el subproceso principal. Los despachadores nos permiten empujar los módulos desde un subproceso diferente al subproceso principal. Ahora sigamos adelante y ejecutemos la aplicación y copiemos el ModuleA.DLL en el directorio de la carpeta Módulos de la aplicación y veamos qué sucede.
Before:

Ejecute la aplicación y abra la ubicación del directorio /Modules de la aplicación y la ubicación del archivo Bin/Debug/ModuleA.dll del ModuleA. Como puede ver, no hay módulos cargados para la aplicación y la aplicación Prism muestra un shell vacío.
After:

Ahora, copie el ModuleA.dll del directorio Bin/Debug del módulo en el directorio /Modules de la aplicación Prism. Tan pronto como se haya completado la operación de copia, se carga el ensamblado ModuleA.dll y ModuleAView se inyecta en el Shell; S región. Todo mientras se ejecuta la aplicación. No es necesario apagar y reiniciar la aplicación.
Así que esa fue la forma rápida y sucia. Ahora veamos cómo podemos hacer un ModuleCatalog personalizado que no solo cargue módulos de un directorio como lo hace el Prism DirectoryModuleCatalog predeterminado, sino que también nos permita monitorear el directorio para los módulos recién agregados en tiempo de ejecución.
A Better Way
Acabamos de ver cómo, con unas pocas líneas de código, podíamos descubrir y cargar módulos de forma dinámica. Ahora, vamos a crear una clase ModuleCatalog personalizada que no solo registrará y cargará los módulos existentes desde un directorio, sino que también supervisará ese mismo directorio para los módulos recién agregados en tiempo de ejecución. Esta clase debe ser un poco más estable y realizar las creaciones correctas de dominios de aplicación y evidencias, así como en la reflexión de memoria, sin cargar los ensamblados en el dominio de aplicación principal hasta que realmente se necesiten. También vamos a eliminar la dependencia de Dispatcher y, en su lugar, usar la clase SynchronizationContext. No voy a recorrer todo el código. Solo voy a proporcionar el código y puedes leerlo.
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;
}
}
Así es como usaría el DynamicDirectoryModuleCatalog recién creado en nuestro Prism Bootstrapper.
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;
}
Es posible que no lo sepa, pero incluso puede hacer que varias instancias de su aplicación Prism supervisen el mismo directorio y carguen los mismos módulos.

Genial, ¿eh? Ahora puede descubrir y cargar dinámicamente sus módulos de Prism en tiempo de ejecución.
Como siempre, no dudes en ponerte en contacto conmigo en mi blog, conéctate conmigo en Twitter (@brianlagunas) o deja un comentario a continuación para cualquier pregunta o comentario que puedas tener.